Merge pull request #1600 from makeplane/stage-release

promote: stage-release to master
This commit is contained in:
Vamsi Kurama 2023-07-20 19:36:43 +05:30 committed by GitHub
commit 11faf3f810
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
511 changed files with 16173 additions and 7257 deletions

View File

@ -59,8 +59,9 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880 FILE_SIZE_LIMIT=5242880
# GPT settings # GPT settings
OPENAI_API_KEY="" OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
GPT_ENGINE="" OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Github # Github
GITHUB_CLIENT_SECRET="" # For fetching release notes GITHUB_CLIENT_SECRET="" # For fetching release notes

View File

@ -23,7 +23,6 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
- Python version 3.8+ - Python version 3.8+
- Postgres version v14 - Postgres version v14
- Redis version v6.2.7 - Redis version v6.2.7
- pnpm version 7.22.0
### Setup the project ### Setup the project

View File

@ -19,14 +19,14 @@
<p> <p>
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank"> <a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Screen.png?updatedAt=1684942001069" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Screen.png"
alt="Plane Screens" alt="Plane Screens"
width="100%" width="100%"
/> />
</a> </a>
<a href="https://app.plane.so/#gh-dark-mode-only" target="_blank"> <a href="https://app.plane.so/#gh-dark-mode-only" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Screens_Dark_Mode.png?updatedAt=1684942388044" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Screens+Dark+Mode.png"
alt="Plane Screens" alt="Plane Screens"
width="100%" width="100%"
/> />
@ -61,14 +61,6 @@ chmod +x setup.sh
> If running in a cloud env replace localhost with public facing IP address of the VM > If running in a cloud env replace localhost with public facing IP address of the VM
- Export Environment Variables
```bash
set -a
source .env
set +a
```
- Run Docker compose up - Run Docker compose up
```bash ```bash
@ -94,7 +86,7 @@ docker compose up -d
<p> <p>
<a href="https://plane.so" target="_blank"> <a href="https://plane.so" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Views_Dark_Mode.png?updatedAt=1684943050275" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Views+Dark+Mode.png"
alt="Plane Views" alt="Plane Views"
width="100%" width="100%"
/> />
@ -103,7 +95,7 @@ docker compose up -d
<p> <p>
<a href="https://plane.so" target="_blank"> <a href="https://plane.so" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Issue_Detail_Dark_Mode.png?updatedAt=1684943050202" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Issue+Detail+Dark+Mode.png"
alt="Plane Issue Details" alt="Plane Issue Details"
width="100%" width="100%"
/> />
@ -112,7 +104,7 @@ docker compose up -d
<p> <p>
<a href="https://plane.so" target="_blank"> <a href="https://plane.so" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Cycles___Modules_Dark_Mode.png?updatedAt=1684943050281" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Cycles+%26+Modules+Dark+Mode.png"
alt="Plane Cycles and Modules" alt="Plane Cycles and Modules"
width="100%" width="100%"
/> />
@ -121,7 +113,7 @@ docker compose up -d
<p> <p>
<a href="https://plane.so" target="_blank"> <a href="https://plane.so" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Analytics_Dark_Mode.png?updatedAt=1684944596824" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Analytics+Dark+Mode.png"
alt="Plane Analytics" alt="Plane Analytics"
width="100%" width="100%"
/> />
@ -130,7 +122,7 @@ docker compose up -d
<p> <p>
<a href="https://plane.so" target="_blank"> <a href="https://plane.so" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Pages_Dark_Mode.png?updatedAt=1684943050202" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Pages+Dark+Mode.png"
alt="Plane Pages" alt="Plane Pages"
width="100%" width="100%"
/> />
@ -140,7 +132,7 @@ docker compose up -d
<p> <p>
<a href="https://plane.so" target="_blank"> <a href="https://plane.so" target="_blank">
<img <img
src="https://ik.imagekit.io/killbluedog/Plane_Commad_K_Dark_Mode.png?updatedAt=1684943050312" src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Commad+K+Dark+Mode.png"
alt="Plane Command Menu" alt="Plane Command Menu"
width="100%" width="100%"
/> />

View File

@ -49,7 +49,7 @@ USER root
RUN apk --no-cache add "bash~=5.2" RUN apk --no-cache add "bash~=5.2"
COPY ./bin ./bin/ COPY ./bin ./bin/
RUN chmod +x ./bin/takeoff ./bin/worker RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chmod -R 777 /code RUN chmod -R 777 /code
USER captain USER captain

View File

@ -1,2 +1,3 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile - web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info worker: celery -A plane worker -l info
beat: celery -A plane beat -l INFO

5
apiserver/bin/beat Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e
python manage.py wait_for_db
celery -A plane beat -l info

View File

@ -6,4 +6,4 @@ python manage.py migrate
# Create a Default User # Create a Default User
python bin/user_script.py python bin/user_script.py
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --config gunicorn.config.py --max-requests 1200 --max-requests-jitter 1000 --access-logfile - exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -

View File

@ -21,6 +21,7 @@ from .project import (
ProjectIdentifierSerializer, ProjectIdentifierSerializer,
ProjectFavoriteSerializer, ProjectFavoriteSerializer,
ProjectLiteSerializer, ProjectLiteSerializer,
ProjectMemberLiteSerializer,
) )
from .state import StateSerializer, StateLiteSerializer from .state import StateSerializer, StateLiteSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer from .view import IssueViewSerializer, IssueViewFavoriteSerializer
@ -41,6 +42,7 @@ from .issue import (
IssueLinkSerializer, IssueLinkSerializer,
IssueLiteSerializer, IssueLiteSerializer,
IssueAttachmentSerializer, IssueAttachmentSerializer,
IssueSubscriberSerializer,
) )
from .module import ( from .module import (
@ -74,4 +76,7 @@ from .estimate import (
) )
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
from .analytic import AnalyticViewSerializer from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer

View File

@ -19,6 +19,7 @@ from plane.db.models import (
IssueProperty, IssueProperty,
IssueBlocker, IssueBlocker,
IssueAssignee, IssueAssignee,
IssueSubscriber,
IssueLabel, IssueLabel,
Label, Label,
IssueBlocker, IssueBlocker,
@ -461,9 +462,9 @@ class IssueAttachmentSerializer(BaseSerializer):
# Issue Serializer with state details # Issue Serializer with state details
class IssueStateSerializer(BaseSerializer): class IssueStateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state") label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
project_detail = ProjectSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state")
label_details = LabelSerializer(read_only=True, source="labels", many=True) project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True) bridge_id = serializers.UUIDField(read_only=True)
@ -476,7 +477,7 @@ class IssueStateSerializer(BaseSerializer):
class IssueSerializer(BaseSerializer): class IssueSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateSerializer(read_only=True, source="state") state_detail = StateSerializer(read_only=True, source="state")
parent_detail = IssueFlatSerializer(read_only=True, source="parent") parent_detail = IssueFlatSerializer(read_only=True, source="parent")
label_details = LabelSerializer(read_only=True, source="labels", many=True) label_details = LabelSerializer(read_only=True, source="labels", many=True)
@ -530,3 +531,14 @@ class IssueLiteSerializer(BaseSerializer):
"created_at", "created_at",
"updated_at", "updated_at",
] ]
class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
]

View File

@ -106,7 +106,7 @@ class ModuleFlatSerializer(BaseSerializer):
class ModuleIssueSerializer(BaseSerializer): class ModuleIssueSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(read_only=True, source="module") module_detail = ModuleFlatSerializer(read_only=True, source="module")
issue_detail = IssueStateSerializer(read_only=True, source="issue") issue_detail = ProjectLiteSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
@ -151,7 +151,7 @@ class ModuleLinkSerializer(BaseSerializer):
class ModuleSerializer(BaseSerializer): class ModuleSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead") lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members") members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
link_module = ModuleLinkSerializer(read_only=True, many=True) link_module = ModuleLinkSerializer(read_only=True, many=True)

View File

@ -0,0 +1,12 @@
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import Notification
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
class Meta:
model = Notification
fields = "__all__"

View File

@ -77,6 +77,13 @@ class ProjectSerializer(BaseSerializer):
raise serializers.ValidationError(detail="Project Identifier is already taken") raise serializers.ValidationError(detail="Project Identifier is already taken")
class ProjectLiteSerializer(BaseSerializer):
class Meta:
model = Project
fields = ["id", "identifier", "name"]
read_only_fields = fields
class ProjectDetailSerializer(BaseSerializer): class ProjectDetailSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True) workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserLiteSerializer(read_only=True) default_assignee = UserLiteSerializer(read_only=True)
@ -94,7 +101,7 @@ class ProjectDetailSerializer(BaseSerializer):
class ProjectMemberSerializer(BaseSerializer): class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True) workspace = WorkSpaceSerializer(read_only=True)
project = ProjectSerializer(read_only=True) project = ProjectLiteSerializer(read_only=True)
member = UserLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True)
class Meta: class Meta:
@ -103,8 +110,8 @@ class ProjectMemberSerializer(BaseSerializer):
class ProjectMemberInviteSerializer(BaseSerializer): class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectSerializer(read_only=True) project = ProjectLiteSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
class Meta: class Meta:
model = ProjectMemberInvite model = ProjectMemberInvite
@ -118,7 +125,7 @@ class ProjectIdentifierSerializer(BaseSerializer):
class ProjectFavoriteSerializer(BaseSerializer): class ProjectFavoriteSerializer(BaseSerializer):
project_detail = ProjectSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta: class Meta:
model = ProjectFavorite model = ProjectFavorite
@ -129,8 +136,13 @@ class ProjectFavoriteSerializer(BaseSerializer):
] ]
class ProjectLiteSerializer(BaseSerializer):
class ProjectMemberLiteSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True)
is_subscribed = serializers.BooleanField(read_only=True)
class Meta: class Meta:
model = Project model = ProjectMember
fields = ["id", "identifier", "name"] fields = ["member", "id", "is_subscribed"]
read_only_fields = fields read_only_fields = fields

View File

@ -22,6 +22,7 @@ from plane.api.views import (
# User # User
UserEndpoint, UserEndpoint,
UpdateUserOnBoardedEndpoint, UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint,
UserActivityEndpoint, UserActivityEndpoint,
## End User ## End User
# Workspaces # Workspaces
@ -76,6 +77,8 @@ from plane.api.views import (
IssueLinkViewSet, IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint, BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint, IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
## End Issues ## End Issues
# States # States
StateViewSet, StateViewSet,
@ -148,6 +151,10 @@ from plane.api.views import (
ExportAnalyticsEndpoint, ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint, DefaultAnalyticsEndpoint,
## End Analytics ## End Analytics
# Notification
NotificationViewSet,
UnreadNotificationEndpoint,
## End Notification
) )
@ -197,7 +204,12 @@ urlpatterns = [
path( path(
"users/me/onboard/", "users/me/onboard/",
UpdateUserOnBoardedEndpoint.as_view(), UpdateUserOnBoardedEndpoint.as_view(),
name="change-password", name="user-onboard",
),
path(
"users/me/tour-completed/",
UpdateUserTourCompletedEndpoint.as_view(),
name="user-tour",
), ),
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"), path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
# user workspaces # user workspaces
@ -467,7 +479,6 @@ urlpatterns = [
"workspaces/<str:slug>/user-favorite-projects/", "workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view( ProjectFavoritesViewSet.as_view(
{ {
"get": "list",
"post": "create", "post": "create",
} }
), ),
@ -797,6 +808,34 @@ urlpatterns = [
name="project-issue-comment", name="project-issue-comment",
), ),
## End IssueComments ## End IssueComments
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
"post": "subscribe",
"delete": "unsubscribe",
}
),
name="project-issue-subscribers",
),
## End Issue Subscribers
## IssueProperty ## IssueProperty
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/", "workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
@ -821,6 +860,36 @@ urlpatterns = [
name="project-issue-roadmap", name="project-issue-roadmap",
), ),
## IssueProperty Ebd ## IssueProperty Ebd
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/unarchive/<uuid:pk>/",
IssueArchiveViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-issue-archive",
),
## End Issue Archives
## File Assets ## File Assets
path( path(
"workspaces/<str:slug>/file-assets/", "workspaces/<str:slug>/file-assets/",
@ -1273,4 +1342,51 @@ urlpatterns = [
name="default-analytics", name="default-analytics",
), ),
## End Analytics ## End Analytics
# Notification
path(
"workspaces/<str:slug>/users/notifications/",
NotificationViewSet.as_view(
{
"get": "list",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
NotificationViewSet.as_view(
{
"post": "mark_read",
"delete": "mark_unread",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
NotificationViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/unread/",
UnreadNotificationEndpoint.as_view(),
name="unread-notifications",
),
## End Notification
] ]

View File

@ -16,6 +16,7 @@ from .project import (
from .people import ( from .people import (
UserEndpoint, UserEndpoint,
UpdateUserOnBoardedEndpoint, UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint,
UserActivityEndpoint, UserActivityEndpoint,
) )
@ -65,6 +66,8 @@ from .issue import (
IssueLinkViewSet, IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint, BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint, IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
) )
from .auth_extended import ( from .auth_extended import (
@ -133,6 +136,7 @@ from .estimate import (
from .release import ReleaseNotesEndpoint from .release import ReleaseNotesEndpoint
from .inbox import InboxViewSet, InboxIssueViewSet from .inbox import InboxViewSet, InboxIssueViewSet
from .analytic import ( from .analytic import (
AnalyticsEndpoint, AnalyticsEndpoint,
AnalyticViewViewset, AnalyticViewViewset,
@ -140,3 +144,5 @@ from .analytic import (
ExportAnalyticsEndpoint, ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint, DefaultAnalyticsEndpoint,
) )
from .notification import NotificationViewSet, UnreadNotificationEndpoint

View File

@ -345,7 +345,7 @@ class MagicSignInEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
try: try:
user_token = request.data.get("token", "").strip().lower() user_token = request.data.get("token", "").strip()
key = request.data.get("key", False) key = request.data.get("key", False)
if not key or user_token == "": if not key or user_token == "":

View File

@ -706,9 +706,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
class CycleFavoriteViewSet(BaseViewSet): class CycleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = CycleFavoriteSerializer serializer_class = CycleFavoriteSerializer
model = CycleFavorite model = CycleFavorite

View File

@ -30,31 +30,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
count = 0
# If logger is enabled check for request limit
if settings.LOGGER_BASE_URL:
try:
headers = {
"Content-Type": "application/json",
}
response = requests.post(
settings.LOGGER_BASE_URL,
json={"user_id": str(request.user.id)},
headers=headers,
)
count = response.json().get("count", 0)
if not response.json().get("success", False):
return Response(
{
"error": "You have surpassed the monthly limit for AI assistance"
},
status=status.HTTP_429_TOO_MANY_REQUESTS,
)
except Exception as e:
capture_exception(e)
prompt = request.data.get("prompt", False) prompt = request.data.get("prompt", False)
task = request.data.get("task", False) task = request.data.get("task", False)
@ -67,7 +42,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
openai.api_key = settings.OPENAI_API_KEY openai.api_key = settings.OPENAI_API_KEY
response = openai.Completion.create( response = openai.Completion.create(
engine=settings.GPT_ENGINE, model=settings.GPT_ENGINE,
prompt=final_text, prompt=final_text,
temperature=0.7, temperature=0.7,
max_tokens=1024, max_tokens=1024,
@ -82,7 +57,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
{ {
"response": text, "response": text,
"response_html": text_html, "response_html": text_html,
"count": count,
"project_detail": ProjectLiteSerializer(project).data, "project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data, "workspace_detail": WorkspaceLiteSerializer(workspace).data,
}, },

View File

@ -15,6 +15,7 @@ from django.db.models import (
Value, Value,
CharField, CharField,
When, When,
Exists,
Max, Max,
) )
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
@ -43,11 +44,14 @@ from plane.api.serializers import (
IssueLinkSerializer, IssueLinkSerializer,
IssueLiteSerializer, IssueLiteSerializer,
IssueAttachmentSerializer, IssueAttachmentSerializer,
IssueSubscriberSerializer,
ProjectMemberLiteSerializer,
) )
from plane.api.permissions import ( from plane.api.permissions import (
ProjectEntityPermission, ProjectEntityPermission,
WorkSpaceAdminPermission, WorkSpaceAdminPermission,
ProjectMemberPermission, ProjectMemberPermission,
ProjectLitePermission,
) )
from plane.db.models import ( from plane.db.models import (
Project, Project,
@ -59,6 +63,8 @@ from plane.db.models import (
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
State, State,
IssueSubscriber,
ProjectMember,
) )
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
@ -162,8 +168,8 @@ class IssueViewSet(BaseViewSet):
issue_queryset = ( issue_queryset = (
self.get_queryset() self.get_queryset()
.filter(**filters) .filter(**filters)
.annotate(cycle_id=F("issue_cycle__id")) .annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__id")) .annotate(module_id=F("issue_module__module_id"))
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by() .order_by()
@ -256,7 +262,7 @@ class IssueViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
except Exception as e: except Exception as e:
capture_exception(e) print(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -905,3 +911,347 @@ class IssueAttachmentEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
class IssueArchiveViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = IssueFlatSerializer
model = Issue
def get_queryset(self):
return (
Issue.objects.annotate(
sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(archived_at__isnull=False)
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
try:
filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
self.get_queryset()
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
# State Ordering
elif order_by_param in [
"state__name",
"state__group",
"-state__name",
"-state__group",
]:
state_order = (
state_order
if order_by_param in ["state__name", "state__group"]
else state_order[::-1]
)
issue_queryset = issue_queryset.annotate(
state_order=Case(
*[
When(state__group=state_group, then=Value(i))
for i, state_group in enumerate(state_order)
],
default=Value(len(state_order)),
output_field=CharField(),
)
).order_by("state_order")
# assignee and label ordering
elif order_by_param in [
"labels__name",
"-labels__name",
"assignees__first_name",
"-assignees__first_name",
]:
issue_queryset = issue_queryset.annotate(
max_values=Max(
order_by_param[1::]
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issue_queryset = (
issue_queryset
if show_sub_issues == "true"
else issue_queryset.filter(parent__isnull=True)
)
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
return Response(
group_results(issues, group_by), status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def retrieve(self, request, slug, project_id, pk=None):
try:
issue = Issue.objects.get(
workspace__slug=slug,
project_id=project_id,
archived_at__isnull=False,
pk=pk,
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
except Issue.DoesNotExist:
return Response(
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def unarchive(self, request, slug, project_id, pk=None):
try:
issue = Issue.objects.get(
workspace__slug=slug,
project_id=project_id,
archived_at__isnull=False,
pk=pk,
)
issue.archived_at = None
issue.save()
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": None}),
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
except Issue.DoesNotExist:
return Response(
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong, please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueSubscriberViewSet(BaseViewSet):
serializer_class = IssueSubscriberSerializer
model = IssueSubscriber
permission_classes = [
ProjectEntityPermission,
]
def get_permissions(self):
if self.action in ["subscribe", "unsubscribe", "subscription_status"]:
self.permission_classes = [
ProjectLitePermission,
]
else:
self.permission_classes = [
ProjectEntityPermission,
]
return super(IssueSubscriberViewSet, self).get_permissions()
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
issue_id=self.kwargs.get("issue_id"),
)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(project__project_projectmember__member=self.request.user)
.order_by("-created_at")
.distinct()
)
def list(self, request, slug, project_id, issue_id):
try:
members = ProjectMember.objects.filter(
workspace__slug=slug, project_id=project_id
).annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
subscriber=OuterRef("member"),
)
)
).select_related("member")
serializer = ProjectMemberLiteSerializer(members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": e},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, issue_id, subscriber_id):
try:
issue_subscriber = IssueSubscriber.objects.get(
project=project_id,
subscriber=subscriber_id,
workspace__slug=slug,
issue=issue_id,
)
issue_subscriber.delete()
return Response(
status=status.HTTP_204_NO_CONTENT,
)
except IssueSubscriber.DoesNotExist:
return Response(
{"error": "User is not subscribed to this issue"},
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 subscribe(self, request, slug, project_id, issue_id):
try:
if IssueSubscriber.objects.filter(
issue_id=issue_id,
subscriber=request.user,
workspace__slug=slug,
project=project_id,
).exists():
return Response(
{"message": "User already subscribed to the issue."},
status=status.HTTP_400_BAD_REQUEST,
)
subscriber = IssueSubscriber.objects.create(
issue_id=issue_id,
subscriber_id=request.user.id,
project_id=project_id,
)
serilaizer = IssueSubscriberSerializer(subscriber)
return Response(serilaizer.data, status=status.HTTP_201_CREATED)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong, please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def unsubscribe(self, request, slug, project_id, issue_id):
try:
issue_subscriber = IssueSubscriber.objects.get(
project=project_id,
subscriber=request.user,
workspace__slug=slug,
issue=issue_id,
)
issue_subscriber.delete()
return Response(
status=status.HTTP_204_NO_CONTENT,
)
except IssueSubscriber.DoesNotExist:
return Response(
{"error": "User subscribed to this issue"},
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 subscription_status(self, request, slug, project_id, issue_id):
try:
issue_subscriber = IssueSubscriber.objects.filter(
issue=issue_id,
subscriber=request.user,
workspace__slug=slug,
project=project_id,
).exists()
return Response({"subscribed": issue_subscriber}, 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,
)

View File

@ -480,9 +480,6 @@ class ModuleLinkViewSet(BaseViewSet):
class ModuleFavoriteViewSet(BaseViewSet): class ModuleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = ModuleFavoriteSerializer serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite model = ModuleFavorite

View File

@ -0,0 +1,258 @@
# Django imports
from django.db.models import Q
from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue
from plane.api.serializers import NotificationSerializer
class NotificationViewSet(BaseViewSet):
model = Notification
serializer_class = NotificationSerializer
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
workspace__slug=self.kwargs.get("slug"),
receiver_id=self.request.user.id,
)
.select_related("workspace", "project," "triggered_by", "receiver")
)
def list(self, request, slug):
try:
snoozed = request.GET.get("snoozed", "false")
archived = request.GET.get("archived", "false")
read = request.GET.get("read", "true")
# Filter type
type = request.GET.get("type", "all")
notifications = Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id
).order_by("snoozed_till", "-created_at")
# Filter for snoozed notifications
if snoozed == "false":
notifications = notifications.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
)
if snoozed == "true":
notifications = notifications.filter(
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
)
if read == "false":
notifications = notifications.filter(read_at__isnull=True)
# Filter for archived or unarchive
if archived == "false":
notifications = notifications.filter(archived_at__isnull=True)
if archived == "true":
notifications = notifications.filter(archived_at__isnull=False)
# Subscribed issues
if type == "watching":
issue_ids = IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Assigned Issues
if type == "assigned":
issue_ids = IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Created issues
if type == "created":
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
serializer = NotificationSerializer(notifications, 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,
)
def partial_update(self, request, slug, pk):
try:
notification = Notification.objects.get(
workspace__slug=slug, pk=pk, receiver=request.user
)
# Only read_at and snoozed_till can be updated
notification_data = {
"snoozed_till": request.data.get("snoozed_till", None),
}
serializer = NotificationSerializer(
notification, data=notification_data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def mark_read(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.read_at = timezone.now()
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def mark_unread(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.read_at = None
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def archive(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.archived_at = timezone.now()
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def unarchive(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.archived_at = None
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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 UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug):
try:
# Watching Issues Count
watching_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True),
).count()
# My Issues Count
my_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True),
).count()
# Created Issues Count
created_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True),
).count()
return Response(
{
"watching_issues": watching_issues_count,
"my_issues": my_issues_count,
"created_issues": created_issues_count,
},
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,
)

View File

@ -37,7 +37,9 @@ class UserEndpoint(BaseViewSet):
workspace_invites = WorkspaceMemberInvite.objects.filter( workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email email=request.user.email
).count() ).count()
assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count() assigned_issues = Issue.issue_objects.filter(
assignees__in=[request.user]
).count()
serialized_data = UserSerializer(request.user).data serialized_data = UserSerializer(request.user).data
serialized_data["workspace"] = { serialized_data["workspace"] = {
@ -47,7 +49,9 @@ class UserEndpoint(BaseViewSet):
"fallback_workspace_slug": workspace.slug, "fallback_workspace_slug": workspace.slug,
"invites": workspace_invites, "invites": workspace_invites,
} }
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues serialized_data.setdefault("issues", {})[
"assigned_issues"
] = assigned_issues
return Response( return Response(
serialized_data, serialized_data,
@ -59,11 +63,15 @@ class UserEndpoint(BaseViewSet):
workspace_invites = WorkspaceMemberInvite.objects.filter( workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email email=request.user.email
).count() ).count()
assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count() assigned_issues = Issue.issue_objects.filter(
assignees__in=[request.user]
).count()
fallback_workspace = Workspace.objects.filter( fallback_workspace = (
workspace_member__member=request.user Workspace.objects.filter(workspace_member__member=request.user)
).order_by("created_at").first() .order_by("created_at")
.first()
)
serialized_data = UserSerializer(request.user).data serialized_data = UserSerializer(request.user).data
@ -78,7 +86,9 @@ class UserEndpoint(BaseViewSet):
else None, else None,
"invites": workspace_invites, "invites": workspace_invites,
} }
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues serialized_data.setdefault("issues", {})[
"assigned_issues"
] = assigned_issues
return Response( return Response(
serialized_data, serialized_data,
@ -109,6 +119,23 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
) )
class UpdateUserTourCompletedEndpoint(BaseAPIView):
def patch(self, request):
try:
user = User.objects.get(pk=request.user.id)
user.is_tour_completed = request.data.get("is_tour_completed", False)
user.save()
return Response(
{"message": "Updated successfully"}, 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 UserActivityEndpoint(BaseAPIView, BasePaginator): class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request): def get(self, request):
try: try:

View File

@ -96,6 +96,7 @@ class ProjectViewSet(BaseViewSet):
def list(self, request, slug): def list(self, request, slug):
try: try:
is_favorite = request.GET.get("is_favorite", "all")
subquery = ProjectFavorite.objects.filter( subquery = ProjectFavorite.objects.filter(
user=self.request.user, user=self.request.user,
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
@ -126,6 +127,12 @@ class ProjectViewSet(BaseViewSet):
.values("count") .values("count")
) )
) )
if is_favorite == "true":
projects = projects.filter(is_favorite=True)
if is_favorite == "false":
projects = projects.filter(is_favorite=False)
return Response(ProjectDetailSerializer(projects, many=True).data) return Response(ProjectDetailSerializer(projects, many=True).data)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
@ -153,32 +160,32 @@ class ProjectViewSet(BaseViewSet):
states = [ states = [
{ {
"name": "Backlog", "name": "Backlog",
"color": "#5e6ad2", "color": "#A3A3A3",
"sequence": 15000, "sequence": 15000,
"group": "backlog", "group": "backlog",
"default": True, "default": True,
}, },
{ {
"name": "Todo", "name": "Todo",
"color": "#eb5757", "color": "#3A3A3A",
"sequence": 25000, "sequence": 25000,
"group": "unstarted", "group": "unstarted",
}, },
{ {
"name": "In Progress", "name": "In Progress",
"color": "#26b5ce", "color": "#F59E0B",
"sequence": 35000, "sequence": 35000,
"group": "started", "group": "started",
}, },
{ {
"name": "Done", "name": "Done",
"color": "#f2c94c", "color": "#16A34A",
"sequence": 45000, "sequence": 45000,
"group": "completed", "group": "completed",
}, },
{ {
"name": "Cancelled", "name": "Cancelled",
"color": "#4cb782", "color": "#EF4444",
"sequence": 55000, "sequence": 55000,
"group": "cancelled", "group": "cancelled",
}, },
@ -259,7 +266,7 @@ class ProjectViewSet(BaseViewSet):
group="backlog", group="backlog",
description="Default state for managing all Inbox Issues", description="Default state for managing all Inbox Issues",
project_id=pk, project_id=pk,
color="#ff7700" color="#ff7700",
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -550,45 +557,47 @@ class AddMemberToProjectEndpoint(BaseAPIView):
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
try: try:
member_id = request.data.get("member_id", False) members = request.data.get("members", [])
role = request.data.get("role", False)
if not member_id or not role: # get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if not len(members):
return Response( return Response(
{"error": "Member ID and role is required"}, {"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# Check if the user is a member in the workspace project_members = ProjectMember.objects.bulk_create(
if not WorkspaceMember.objects.filter( [
workspace__slug=slug, member_id=member_id ProjectMember(
).exists(): member_id=member.get("member_id"),
# TODO: Update this error message - nk role=member.get("role", 10),
return Response( project_id=project_id,
{ workspace_id=project.workspace_id,
"error": "User is not a member of the workspace. Invite the user to the workspace to add him to project" )
}, for member in members
status=status.HTTP_400_BAD_REQUEST, ],
batch_size=10,
ignore_conflicts=True,
) )
# Check if the user is already member of project serializer = ProjectMemberSerializer(project_members, many=True)
if ProjectMember.objects.filter(
project=project_id, member_id=member_id
).exists():
return Response(
{"error": "User is already a member of the project"},
status=status.HTTP_400_BAD_REQUEST,
)
# Add the user to project
project_member = ProjectMember.objects.create(
project_id=project_id, member_id=member_id, role=role
)
serializer = ProjectMemberSerializer(project_member)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
except KeyError:
return Response(
{"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST
)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
return Response(
{"error": "User not member of the workspace"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
return Response( return Response(

View File

@ -3,6 +3,7 @@ import jwt
from datetime import date, datetime from datetime import date, datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from uuid import uuid4 from uuid import uuid4
# Django imports # Django imports
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Prefetch from django.db.models import Prefetch
@ -94,14 +95,34 @@ class WorkSpaceViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
return self.filter_queryset( return (
super().get_queryset().select_related("owner") self.filter_queryset(super().get_queryset().select_related("owner"))
).order_by("name").filter(workspace_member__member=self.request.user).annotate(total_members=member_count).annotate(total_issues=issue_count) .order_by("name")
.filter(workspace_member__member=self.request.user)
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
.select_related("owner")
)
def create(self, request): def create(self, request):
try: try:
serializer = WorkSpaceSerializer(data=request.data) serializer = WorkSpaceSerializer(data=request.data)
slug = request.data.get("slug", False)
name = request.data.get("name", False)
if not name or not slug:
return Response(
{"error": "Both name and slug are required"},
status=status.HTTP_400_BAD_REQUEST,
)
if len(name) > 80 or len(slug) > 48:
return Response(
{"error": "The maximum length for name is 80 and for slug is 48"},
status=status.HTTP_400_BAD_REQUEST,
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(owner=request.user) serializer.save(owner=request.user)
# Create Workspace member # Create Workspace member
@ -161,14 +182,20 @@ class UserWorkSpacesEndpoint(BaseAPIView):
) )
workspace = ( workspace = (
(
Workspace.objects.prefetch_related( Workspace.objects.prefetch_related(
Prefetch("workspace_member", queryset=WorkspaceMember.objects.all()) Prefetch(
"workspace_member", queryset=WorkspaceMember.objects.all()
)
) )
.filter( .filter(
workspace_member__member=request.user, workspace_member__member=request.user,
) )
.select_related("owner") .select_related("owner")
).annotate(total_members=member_count).annotate(total_issues=issue_count) )
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
)
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -217,9 +244,20 @@ class InviteWorkspaceEndpoint(BaseAPIView):
) )
# check for role level # check for role level
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user) requesting_user = WorkspaceMember.objects.get(
if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]): workspace__slug=slug, member=request.user
return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST) )
if len(
[
email
for email in emails
if int(email.get("role", 10)) > requesting_user.role
]
):
return Response(
{"error": "You cannot invite a user with higher role"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
@ -894,7 +932,9 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
) )
state_distribution = ( state_distribution = (
Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user]) Issue.issue_objects.filter(
workspace__slug=slug, assignees__in=[request.user]
)
.annotate(state_group=F("state__group")) .annotate(state_group=F("state__group"))
.values("state_group") .values("state_group")
.annotate(state_count=Count("state_group")) .annotate(state_count=Count("state_group"))

View File

@ -5,6 +5,7 @@ import requests
# Django imports # Django imports
from django.conf import settings from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
# Third Party imports # Third Party imports
from celery import shared_task from celery import shared_task
@ -20,6 +21,9 @@ from plane.db.models import (
State, State,
Cycle, Cycle,
Module, Module,
IssueSubscriber,
Notification,
IssueAssignee,
) )
from plane.api.serializers import IssueActivitySerializer from plane.api.serializers import IssueActivitySerializer
@ -554,6 +558,64 @@ def track_estimate_points(
) )
def track_archive_at(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if requested_data.get("archived_at") is None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} has restored the issue",
verb="updated",
actor=actor,
field="archived_at",
old_value="archive",
new_value="restore",
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"Plane has archived the issue",
verb="updated",
actor=actor,
field="archived_at",
old_value=None,
new_value="archive",
)
)
def track_closed_to(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if requested_data.get("closed_to") is not None:
updated_state = State.objects.get(
pk=requested_data.get("closed_to"), project=project
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=None,
new_value=updated_state.name,
field="state",
project=project,
workspace=project.workspace,
comment=f"Plane updated the state to {updated_state.name}",
old_identifier=None,
new_identifier=updated_state.id,
)
)
def update_issue_activity( def update_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities requested_data, current_instance, issue_id, project, actor, issue_activities
): ):
@ -570,6 +632,8 @@ def update_issue_activity(
"blocks_list": track_blocks, "blocks_list": track_blocks,
"blockers_list": track_blockings, "blockers_list": track_blockings,
"estimate_point": track_estimate_points, "estimate_point": track_estimate_points,
"archived_at": track_archive_at,
"closed_to": track_closed_to,
} }
requested_data = json.loads(requested_data) if requested_data is not None else None requested_data = json.loads(requested_data) if requested_data is not None else None
@ -950,7 +1014,13 @@ def delete_attachment_activity(
# Receive message from room group # Receive message from room group
@shared_task @shared_task
def issue_activity( def issue_activity(
type, requested_data, current_instance, issue_id, actor_id, project_id type,
requested_data,
current_instance,
issue_id,
actor_id,
project_id,
subscriber=True,
): ):
try: try:
issue_activities = [] issue_activities = []
@ -958,6 +1028,27 @@ def issue_activity(
actor = User.objects.get(pk=actor_id) actor = User.objects.get(pk=actor_id)
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
]:
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
if issue is not None:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber=actor
)
except Exception as e:
pass
ACTIVITY_MAPPER = { ACTIVITY_MAPPER = {
"issue.activity.created": create_issue_activity, "issue.activity.created": create_issue_activity,
"issue.activity.updated": update_issue_activity, "issue.activity.updated": update_issue_activity,
@ -992,6 +1083,7 @@ def issue_activity(
# Post the updates to segway for integrations and webhooks # Post the updates to segway for integrations and webhooks
if len(issue_activities_created): if len(issue_activities_created):
# Don't send activities if the actor is a bot # Don't send activities if the actor is a bot
try:
if settings.PROXY_BASE_URL: if settings.PROXY_BASE_URL:
for issue_activity in issue_activities_created: for issue_activity in issue_activities_created:
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
@ -1004,6 +1096,84 @@ def issue_activity(
json=issue_activity_json, json=issue_activity_json,
headers=headers, headers=headers,
) )
except Exception as e:
capture_exception(e)
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
issue_subscribers = list(
IssueSubscriber.objects.filter(project=project, issue_id=issue_id)
.exclude(subscriber_id=actor_id)
.values_list("subscriber", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(project=project, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
issue_subscribers = issue_subscribers + issue_assignees
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
# Add bot filtering
if (
issue is not None
and issue.created_by_id is not None
and not issue.created_by.is_bot
and str(issue.created_by_id) != str(actor_id)
):
issue_subscribers = issue_subscribers + [issue.created_by_id]
for subscriber in issue_subscribers:
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities",
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.comment,
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.id),
"verb": str(issue_activity.verb),
"field": str(issue_activity.field),
"actor": str(issue_activity.actor_id),
"new_value": str(issue_activity.new_value),
"old_value": str(issue_activity.old_value),
"issue_comment": str(
issue_activity.issue_comment.comment_stripped
if issue_activity.issue_comment is not None
else ""
),
},
},
)
)
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
return return
except Exception as e: except Exception as e:
# Print logs if in DEBUG mode # Print logs if in DEBUG mode

View File

@ -0,0 +1,157 @@
# Python imports
import json
from datetime import timedelta
# Django imports
from django.utils import timezone
from django.db.models import Q
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import Issue, Project, State
from plane.bgtasks.issue_activites_task import issue_activity
@shared_task
def archive_and_close_old_issues():
archive_old_issues()
close_old_issues()
def archive_old_issues():
try:
# Get all the projects whose archive_in is greater than 0
projects = Project.objects.filter(archive_in__gt=0)
for project in projects:
project_id = project.id
archive_in = project.archive_in
# Get all the issues whose updated_at in less that the archive_in month
issues = Issue.objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
updated_at__lte=(timezone.now() - timedelta(days=archive_in * 30)),
state__group__in=["completed", "cancelled"],
),
Q(issue_cycle__isnull=True)
| (
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
& Q(issue_cycle__isnull=False)
),
Q(issue_module__isnull=True)
| (
Q(issue_module__module__target_date__lt=timezone.now().date())
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
)
# Check if Issues
if issues:
issues_to_update = []
for issue in issues:
issue.archived_at = timezone.now()
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in issues_to_update
]
return
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
return
def close_old_issues():
try:
# Get all the projects whose close_in is greater than 0
projects = Project.objects.filter(close_in__gt=0).select_related(
"default_state"
)
for project in projects:
project_id = project.id
close_in = project.close_in
# Get all the issues whose updated_at in less that the close_in month
issues = Issue.objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
updated_at__lte=(timezone.now() - timedelta(days=close_in * 30)),
state__group__in=["backlog", "unstarted", "started"],
),
Q(issue_cycle__isnull=True)
| (
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
& Q(issue_cycle__isnull=False)
),
Q(issue_module__isnull=True)
| (
Q(issue_module__module__target_date__lt=timezone.now().date())
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
)
# Check if Issues
if issues:
if project.default_state is None:
close_state = State.objects.filter(group="cancelled").first()
else:
close_state = project.default_state
issues_to_update = []
for issue in issues:
issue.state = close_state
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in issues_to_update
]
return
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
return

View File

@ -1,6 +1,7 @@
import os import os
from celery import Celery from celery import Celery
from plane.settings.redis import redis_instance from plane.settings.redis import redis_instance
from celery.schedules import crontab
# Set the default Django settings module for the 'celery' program. # Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
@ -13,5 +14,15 @@ app = Celery("plane")
# pickle the object when using Windows. # pickle the object when using Windows.
app.config_from_object("django.conf:settings", namespace="CELERY") app.config_from_object("django.conf:settings", namespace="CELERY")
app.conf.beat_schedule = {
# Executes every day at 12 AM
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=0, minute=0),
},
}
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks() app.autodiscover_tasks()
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'

View File

@ -0,0 +1,42 @@
# Generated by Django 3.2.19 on 2023-07-04 16:55
from django.db import migrations, models
def update_company_organization_size(apps, schema_editor):
Model = apps.get_model("db", "Workspace")
updated_size = []
for obj in Model.objects.all():
obj.organization_size = str(obj.company_size)
updated_size.append(obj)
Model.objects.bulk_update(updated_size, ["organization_size"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
("db", "0034_auto_20230628_1046"),
]
operations = [
migrations.AddField(
model_name="workspace",
name="organization_size",
field=models.CharField(default="2-10", max_length=20),
),
migrations.RunPython(update_company_organization_size),
migrations.AlterField(
model_name="workspace",
name="name",
field=models.CharField(max_length=80, verbose_name="Workspace Name"),
),
migrations.AlterField(
model_name="workspace",
name="slug",
field=models.SlugField(max_length=48, unique=True),
),
migrations.RemoveField(
model_name="workspace",
name="company_size",
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-07-05 07:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0035_auto_20230704_2225'),
]
operations = [
migrations.AlterField(
model_name='workspace',
name='organization_size',
field=models.CharField(max_length=20),
),
]

View File

@ -0,0 +1,266 @@
# Generated by Django 4.2.3 on 2023-07-19 06:52
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.user
import uuid
def onboarding_default_steps(apps, schema_editor):
default_onboarding_schema = {
"workspace_join": True,
"profile_complete": True,
"workspace_create": True,
"workspace_invite": True,
}
Model = apps.get_model("db", "User")
updated_user = []
for obj in Model.objects.filter(is_onboarded=True):
obj.onboarding_step = default_onboarding_schema
obj.is_tour_completed = True
updated_user.append(obj)
Model.objects.bulk_update(updated_user, ["onboarding_step", "is_tour_completed"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
("db", "0036_alter_workspace_organization_size"),
]
operations = [
migrations.AddField(
model_name="issue",
name="archived_at",
field=models.DateField(null=True),
),
migrations.AddField(
model_name="project",
name="archive_in",
field=models.IntegerField(
default=0,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(12),
],
),
),
migrations.AddField(
model_name="project",
name="close_in",
field=models.IntegerField(
default=0,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(12),
],
),
),
migrations.AddField(
model_name="project",
name="default_state",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="default_state",
to="db.state",
),
),
migrations.AddField(
model_name="user",
name="is_tour_completed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="user",
name="onboarding_step",
field=models.JSONField(default=plane.db.models.user.get_default_onboarding),
),
migrations.RunPython(onboarding_default_steps),
migrations.CreateModel(
name="Notification",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("data", models.JSONField(null=True)),
("entity_identifier", models.UUIDField(null=True)),
("entity_name", models.CharField(max_length=255)),
("title", models.TextField()),
("message", models.JSONField(null=True)),
("message_html", models.TextField(blank=True, default="<p></p>")),
("message_stripped", models.TextField(blank=True, null=True)),
("sender", models.CharField(max_length=255)),
("read_at", models.DateTimeField(null=True)),
("snoozed_till", models.DateTimeField(null=True)),
("archived_at", models.DateTimeField(null=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="db.project",
),
),
(
"receiver",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="received_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"triggered_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="triggered_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="db.workspace",
),
),
],
options={
"verbose_name": "Notification",
"verbose_name_plural": "Notifications",
"db_table": "notifications",
"ordering": ("-created_at",),
},
),
migrations.CreateModel(
name="IssueSubscriber",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_subscribers",
to="db.issue",
),
),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"subscriber",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_subscribers",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Issue Subscriber",
"verbose_name_plural": "Issue Subscribers",
"db_table": "issue_subscribers",
"ordering": ("-created_at",),
"unique_together": {("issue", "subscriber")},
},
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.3 on 2023-07-20 09:35
from django.db import migrations, models
def restructure_theming(apps, schema_editor):
Model = apps.get_model("db", "User")
updated_user = []
for obj in Model.objects.exclude(theme={}).all():
current_theme = obj.theme
updated_theme = {
"primary": current_theme.get("accent", ""),
"background": current_theme.get("bgBase", ""),
"sidebarBackground": current_theme.get("sidebar", ""),
"text": current_theme.get("textBase", ""),
"sidebarText": current_theme.get("textBase", ""),
"palette": f"""{current_theme.get("bgBase","")},{current_theme.get("textBase", "")},{current_theme.get("accent", "")},{current_theme.get("sidebar","")},{current_theme.get("textBase", "")}""",
"darkPalette": current_theme.get("darkPalette", "")
}
obj.theme = updated_theme
updated_user.append(obj)
Model.objects.bulk_update(
updated_user, ["theme"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
("db", "0037_issue_archived_at_project_archive_in_and_more"),
]
operations = [
migrations.RunPython(restructure_theming)
]

View File

@ -33,6 +33,7 @@ from .issue import (
IssueLink, IssueLink,
IssueSequence, IssueSequence,
IssueAttachment, IssueAttachment,
IssueSubscriber,
) )
from .asset import FileAsset from .asset import FileAsset
@ -66,4 +67,7 @@ from .page import Page, PageBlock, PageFavorite, PageLabel
from .estimate import Estimate, EstimatePoint from .estimate import Estimate, EstimatePoint
from .inbox import Inbox, InboxIssue from .inbox import Inbox, InboxIssue
from .analytic import AnalyticView from .analytic import AnalyticView
from .notification import Notification

View File

@ -28,6 +28,7 @@ class IssueManager(models.Manager):
| models.Q(issue_inbox__status=2) | models.Q(issue_inbox__status=2)
| models.Q(issue_inbox__isnull=True) | models.Q(issue_inbox__isnull=True)
) )
.exclude(archived_at__isnull=False)
) )
@ -81,6 +82,7 @@ class Issue(ProjectBaseModel):
) )
sort_order = models.FloatField(default=65535) sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True) completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
objects = models.Manager() objects = models.Manager()
issue_objects = IssueManager() issue_objects = IssueManager()
@ -401,6 +403,27 @@ class IssueSequence(ProjectBaseModel):
ordering = ("-created_at",) ordering = ("-created_at",)
class IssueSubscriber(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_subscribers"
)
subscriber = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_subscribers",
)
class Meta:
unique_together = ["issue", "subscriber"]
verbose_name = "Issue Subscriber"
verbose_name_plural = "Issue Subscribers"
db_table = "issue_subscribers"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.subscriber.email}"
# TODO: Find a better method to save the model # TODO: Find a better method to save the model
@receiver(post_save, sender=Issue) @receiver(post_save, sender=Issue)
def create_issue_sequence(sender, instance, created, **kwargs): def create_issue_sequence(sender, instance, created, **kwargs):

View File

@ -0,0 +1,37 @@
# Django imports
from django.db import models
# Third party imports
from .base import BaseModel
class Notification(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", related_name="notifications", on_delete=models.CASCADE
)
project = models.ForeignKey(
"db.Project", related_name="notifications", on_delete=models.CASCADE, null=True
)
data = models.JSONField(null=True)
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(max_length=255)
title = models.TextField()
message = models.JSONField(null=True)
message_html = models.TextField(blank=True, default="<p></p>")
message_stripped = models.TextField(blank=True, null=True)
sender = models.CharField(max_length=255)
triggered_by = models.ForeignKey("db.User", related_name="triggered_notifications", on_delete=models.SET_NULL, null=True)
receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE)
read_at = models.DateTimeField(null=True)
snoozed_till = models.DateTimeField(null=True)
archived_at = models.DateTimeField(null=True)
class Meta:
verbose_name = "Notification"
verbose_name_plural = "Notifications"
db_table = "notifications"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the notifications"""
return f"{self.receiver.email} <{self.workspace.name}>"

View File

@ -4,6 +4,7 @@ from django.conf import settings
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.core.validators import MinValueValidator, MaxValueValidator
# Modeule imports # Modeule imports
from plane.db.mixins import AuditModel from plane.db.mixins import AuditModel
@ -74,6 +75,15 @@ class Project(BaseModel):
estimate = models.ForeignKey( estimate = models.ForeignKey(
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True "db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
) )
archive_in = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
)
close_in = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
)
default_state = models.ForeignKey(
"db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
)
def __str__(self): def __str__(self):
"""Return name of the project""" """Return name of the project"""

View File

@ -18,6 +18,13 @@ from sentry_sdk import capture_exception
from slack_sdk import WebClient from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError from slack_sdk.errors import SlackApiError
def get_default_onboarding():
return {
"profile_complete": False,
"workspace_create": False,
"workspace_invite": False,
"workspace_join": False,
}
class User(AbstractBaseUser, PermissionsMixin): class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField( id = models.UUIDField(
@ -73,6 +80,8 @@ class User(AbstractBaseUser, PermissionsMixin):
role = models.CharField(max_length=300, null=True, blank=True) role = models.CharField(max_length=300, null=True, blank=True)
is_bot = models.BooleanField(default=False) is_bot = models.BooleanField(default=False)
theme = models.JSONField(default=dict) theme = models.JSONField(default=dict)
is_tour_completed = models.BooleanField(default=False)
onboarding_step = models.JSONField(default=get_default_onboarding)
USERNAME_FIELD = "email" USERNAME_FIELD = "email"

View File

@ -15,15 +15,15 @@ ROLE_CHOICES = (
class Workspace(BaseModel): class Workspace(BaseModel):
name = models.CharField(max_length=255, verbose_name="Workspace Name") name = models.CharField(max_length=80, verbose_name="Workspace Name")
logo = models.URLField(verbose_name="Logo", blank=True, null=True) logo = models.URLField(verbose_name="Logo", blank=True, null=True)
owner = models.ForeignKey( owner = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="owner_workspace", related_name="owner_workspace",
) )
slug = models.SlugField(max_length=100, db_index=True, unique=True) slug = models.SlugField(max_length=48, db_index=True, unique=True)
company_size = models.PositiveIntegerField(default=10) organization_size = models.CharField(max_length=20)
def __str__(self): def __str__(self):
"""Return name of the Workspace""" """Return name of the Workspace"""

View File

@ -35,6 +35,7 @@ INSTALLED_APPS = [
"rest_framework_simplejwt.token_blacklist", "rest_framework_simplejwt.token_blacklist",
"corsheaders", "corsheaders",
"taggit", "taggit",
"django_celery_beat",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -213,3 +214,4 @@ SIMPLE_JWT = {
CELERY_TIMEZONE = TIME_ZONE CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task",)

View File

@ -10,16 +10,14 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa from .common import * # noqa
DEBUG = int(os.environ.get( DEBUG = int(os.environ.get("DEBUG", 1)) == 1
"DEBUG", 1
)) == 1
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("PGUSER", "plane"), "NAME": os.environ.get("PGUSER", "plane"),
"USER": "", "USER": "",
"PASSWORD": "", "PASSWORD": "",
@ -27,9 +25,7 @@ DATABASES = {
} }
} }
DOCKERIZED = int(os.environ.get( DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1
"DOCKERIZED", 0
)) == 1
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
@ -63,7 +59,29 @@ if os.environ.get("SENTRY_DSN", False):
send_default_pii=True, send_default_pii=True,
environment="local", environment="local",
traces_sample_rate=0.7, traces_sample_rate=0.7,
profiles_sample_rate=1.0,
) )
else:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "DEBUG",
},
"loggers": {
"*": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": True,
},
},
}
REDIS_HOST = "localhost" REDIS_HOST = "localhost"
REDIS_PORT = 6379 REDIS_PORT = 6379
@ -82,8 +100,9 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003") GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)

View File

@ -13,13 +13,11 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa from .common import * # noqa
# Database # Database
DEBUG = int(os.environ.get( DEBUG = int(os.environ.get("DEBUG", 0)) == 1
"DEBUG", 0
)) == 1
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql",
"NAME": "plane", "NAME": "plane",
"USER": os.environ.get("PGUSER", ""), "USER": os.environ.get("PGUSER", ""),
"PASSWORD": os.environ.get("PGPASSWORD", ""), "PASSWORD": os.environ.get("PGPASSWORD", ""),
@ -72,8 +70,12 @@ CORS_ALLOW_HEADERS = [
] ]
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
if bool(os.environ.get("SENTRY_DSN", False)): if bool(os.environ.get("SENTRY_DSN", False)):
sentry_sdk.init( sentry_sdk.init(
@ -84,11 +86,12 @@ if bool(os.environ.get("SENTRY_DSN", False)):
traces_sample_rate=1, traces_sample_rate=1,
send_default_pii=True, send_default_pii=True,
environment="production", environment="production",
profiles_sample_rate=1.0,
) )
if DOCKERIZED and USE_MINIO: if DOCKERIZED and USE_MINIO:
INSTALLED_APPS += ("storages",) INSTALLED_APPS += ("storages",)
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
# The AWS access key to use. # The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
# The AWS secret access key to use. # The AWS secret access key to use.
@ -96,7 +99,9 @@ if DOCKERIZED and USE_MINIO:
# The name of the bucket to store files in. # The name of the bucket to store files in.
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") 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. # 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 = os.environ.get(
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
)
# Default permissions # Default permissions
AWS_DEFAULT_ACL = "public-read" AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False AWS_QUERYSTRING_AUTH = False
@ -187,7 +192,10 @@ else:
# extra characters appended. # extra characters appended.
AWS_S3_FILE_OVERWRITE = False AWS_S3_FILE_OVERWRITE = False
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage" STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# AWS Settings End # AWS Settings End
# Enable Connection Pooling (if desired) # Enable Connection Pooling (if desired)
@ -202,9 +210,6 @@ ALLOWED_HOSTS = [
] ]
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
@ -241,8 +246,9 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003") GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)

View File

@ -11,13 +11,12 @@ from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa from .common import * # noqa
# Database # Database
DEBUG = int(os.environ.get( DEBUG = int(os.environ.get("DEBUG", 1)) == 1
"DEBUG", 1
)) == 1
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("PGUSER", "plane"), "NAME": os.environ.get("PGUSER", "plane"),
"USER": "", "USER": "",
"PASSWORD": "", "PASSWORD": "",
@ -48,13 +47,15 @@ ALLOWED_HOSTS = ["*"]
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD. # TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_ALL_ORIGINS = True
# Simplified static file serving. STORAGES = {
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" "staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Make true if running in a docker environment # Make true if running in a docker environment
DOCKERIZED = int(os.environ.get( DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1
"DOCKERIZED", 0
)) == 1
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) 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
@ -66,6 +67,7 @@ sentry_sdk.init(
traces_sample_rate=1, traces_sample_rate=1,
send_default_pii=True, send_default_pii=True,
environment="staging", environment="staging",
profiles_sample_rate=1.0,
) )
# The AWS region to connect to. # The AWS region to connect to.
@ -150,7 +152,9 @@ AWS_S3_SIGNATURE_VERSION = None
AWS_S3_FILE_OVERWRITE = False AWS_S3_FILE_OVERWRITE = False
# AWS Settings End # AWS Settings End
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# Enable Connection Pooling (if desired) # Enable Connection Pooling (if desired)
# DATABASES['default']['ENGINE'] = 'django_postgrespool' # DATABASES['default']['ENGINE'] = 'django_postgrespool'
@ -163,11 +167,6 @@ ALLOWED_HOSTS = [
"*", "*",
] ]
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
@ -199,15 +198,19 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003") GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
redis_url = os.environ.get("REDIS_URL") redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
CELERY_RESULT_BACKEND = broker_url CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url CELERY_BROKER_URL = broker_url

View File

@ -3,11 +3,10 @@
""" """
# from django.contrib import admin # from django.contrib import admin
from django.urls import path from django.urls import path, include, re_path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url, static
# from django.conf.urls.static import static # from django.conf.urls.static import static
@ -18,11 +17,10 @@ urlpatterns = [
path("", include("plane.web.urls")), path("", include("plane.web.urls")),
] ]
urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG: if settings.DEBUG:
import debug_toolbar import debug_toolbar
urlpatterns = [ urlpatterns = [
url(r"^__debug__/", include(debug_toolbar.urls)), re_path(r"^__debug__/", include(debug_toolbar.urls)),
] + urlpatterns ] + urlpatterns

View File

@ -166,16 +166,16 @@ def filter_target_date(params, filter, method):
for query in target_dates: for query in target_dates:
target_date_query = query.split(";") target_date_query = query.split(";")
if len(target_date_query) == 2 and "after" in target_date_query: if len(target_date_query) == 2 and "after" in target_date_query:
filter["target_date__gte"] = target_date_query[0] filter["target_date__gt"] = target_date_query[0]
else: else:
filter["target_date__lte"] = target_date_query[0] filter["target_date__lt"] = target_date_query[0]
else: else:
if params.get("target_date", None) and len(params.get("target_date")): if params.get("target_date", None) and len(params.get("target_date")):
for query in params.get("target_date"): for query in params.get("target_date"):
if query.get("timeline", "after") == "after": if query.get("timeline", "after") == "after":
filter["target_date__gte"] = query.get("datetime") filter["target_date__gt"] = query.get("datetime")
else: else:
filter["target_date__lte"] = query.get("datetime") filter["target_date__lt"] = query.get("datetime")
return filter return filter

View File

@ -1,31 +1,34 @@
# base requirements # base requirements
Django==3.2.19 Django==4.2.3
django-braces==1.15.0 django-braces==1.15.0
django-taggit==3.1.0 django-taggit==4.0.0
psycopg2==2.9.5 psycopg==3.1.9
django-oauth-toolkit==2.2.0 django-oauth-toolkit==2.3.0
mistune==2.0.4 mistune==3.0.1
djangorestframework==3.14.0 djangorestframework==3.14.0
redis==4.5.4 redis==4.6.0
django-nested-admin==4.0.2 django-nested-admin==4.0.2
django-cors-headers==3.13.0 django-cors-headers==4.1.0
whitenoise==6.3.0 whitenoise==6.5.0
django-allauth==0.52.0 django-allauth==0.54.0
faker==13.4.0 faker==18.11.2
django-filter==22.1 django-filter==23.2
jsonmodels==2.6.0 jsonmodels==2.6.0
djangorestframework-simplejwt==5.2.2 djangorestframework-simplejwt==5.2.2
sentry-sdk==1.14.0 sentry-sdk==1.27.0
django-s3-storage==0.13.11 django-s3-storage==0.14.0
django-crum==0.7.9 django-crum==0.7.9
django-guardian==2.4.0 django-guardian==2.4.0
dj_rest_auth==2.2.5 dj_rest_auth==2.2.5
google-auth==2.16.0 google-auth==2.21.0
google-api-python-client==2.75.0 google-api-python-client==2.92.0
django-redis==5.2.0 django-redis==5.3.0
uvicorn==0.20.0 uvicorn==0.22.0
channels==4.0.0 channels==4.0.0
openai==0.27.2 openai==0.27.8
slack-sdk==3.20.2 slack-sdk==3.21.3
celery==5.2.7 celery==5.3.1
django_celery_beat==2.5.0
psycopg-binary==3.1.9
psycopg-c==3.1.9

View File

@ -1,3 +1,3 @@
-r base.txt -r base.txt
django-debug-toolbar==3.8.1 django-debug-toolbar==4.1.0

View File

@ -1,12 +1,11 @@
-r base.txt -r base.txt
dj-database-url==1.2.0 dj-database-url==2.0.0
gunicorn==20.1.0 gunicorn==20.1.0
whitenoise==6.3.0 whitenoise==6.5.0
django-storages==1.13.2 django-storages==1.13.2
boto3==1.26.136 boto3==1.27.0
django-anymail==9.0 django-anymail==10.0
twilio==7.16.2 django-debug-toolbar==4.1.0
django-debug-toolbar==3.8.1 gevent==23.7.0
gevent==22.10.2
psycogreen==1.0.2 psycogreen==1.0.2

View File

@ -20,7 +20,7 @@ ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
COPY .gitignore .gitignore COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install RUN yarn install --network-timeout 500000
# Build the project # Build the project
COPY --from=builder /app/out/full/ . COPY --from=builder /app/out/full/ .

View File

@ -32,6 +32,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
setError, setError,
setValue, setValue,
getValues, getValues,
watch,
formState: { errors, isSubmitting, isValid, isDirty }, formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailCodeFormValues>({ } = useForm<EmailCodeFormValues>({
defaultValues: { defaultValues: {
@ -112,43 +113,35 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
return ( return (
<> <>
<form className="space-y-5 py-5 px-5">
{(codeSent || codeResent) && ( {(codeSent || codeResent) && (
<div className="rounded-md bg-green-500/20 p-4"> <p className="text-center mt-4">
<div className="flex"> We have sent the sign in code.
<div className="flex-shrink-0"> <br />
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" /> Please check your inbox at <span className="font-medium">{watch("email")}</span>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-500">
{codeResent
? "Please check your mail for new code."
: "Please check your mail for code."}
</p> </p>
</div>
</div>
</div>
)} )}
<div> <form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
<div className="space-y-1">
<Input <Input
id="email" id="email"
type="email" type="email"
name="email" name="email"
register={register} register={register}
validations={{ validations={{
required: "Email ID is required", required: "Email address is required",
validate: (value) => validate: (value) =>
/^(([^<>()[\]\\.,;:\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( /^(([^<>()[\]\\.,;:\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 value
) || "Email ID is not valid", ) || "Email address is not valid",
}} }}
error={errors.email} error={errors.email}
placeholder="Enter your Email ID" placeholder="Enter your email address..."
className="border-custom-border-300 h-[46px]"
/> />
</div> </div>
{codeSent && ( {codeSent && (
<div> <>
<Input <Input
id="token" id="token"
type="token" type="token"
@ -158,14 +151,15 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
required: "Code is required", required: "Code is required",
}} }}
error={errors.token} error={errors.token}
placeholder="Enter code" placeholder="Enter code..."
className="border-custom-border-300 h-[46px]"
/> />
<button <button
type="button" type="button"
className={`mt-5 flex w-full justify-end text-xs outline-none ${ className={`flex w-full justify-end text-xs outline-none ${
isResendDisabled isResendDisabled
? "cursor-default text-brand-secondary" ? "cursor-default text-custom-text-200"
: "cursor-pointer text-brand-accent" : "cursor-pointer text-custom-primary-100"
} `} } `}
onClick={() => { onClick={() => {
setIsCodeResending(true); setIsCodeResending(true);
@ -178,24 +172,21 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
disabled={isResendDisabled} disabled={isResendDisabled}
> >
{resendCodeTimer > 0 ? ( {resendCodeTimer > 0 ? (
<p className="text-right"> <span className="text-right">Request new code in {resendCodeTimer} seconds</span>
Didn{"'"}t receive code? Get new code in {resendCodeTimer} seconds.
</p>
) : isCodeResending ? ( ) : isCodeResending ? (
"Sending code..." "Sending new code..."
) : errorResendingCode ? ( ) : errorResendingCode ? (
"Please try again later" "Please try again later"
) : ( ) : (
"Resend code" <span className="font-medium">Resend code</span>
)} )}
</button> </button>
</div> </>
)} )}
<div>
{codeSent ? ( {codeSent ? (
<PrimaryButton <PrimaryButton
type="submit" type="submit"
className="w-full text-center" className="w-full text-center h-[46px]"
size="md" size="md"
onClick={handleSubmit(handleSignin)} onClick={handleSubmit(handleSignin)}
disabled={!isValid && isDirty} disabled={!isValid && isDirty}
@ -205,19 +196,19 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
</PrimaryButton> </PrimaryButton>
) : ( ) : (
<PrimaryButton <PrimaryButton
className="w-full text-center" className="w-full text-center h-[46px]"
size="md" size="md"
onClick={() => { onClick={() => {
handleSubmit(onSubmit)().then(() => { handleSubmit(onSubmit)().then(() => {
setResendCodeTimer(30); setResendCodeTimer(30);
}); });
}} }}
loading={isSubmitting || (!isValid && isDirty)} disabled={!isValid && isDirty}
loading={isSubmitting}
> >
{isSubmitting ? "Sending code..." : "Send code"} {isSubmitting ? "Sending code..." : "Send sign in code"}
</PrimaryButton> </PrimaryButton>
)} )}
</div>
</form> </form>
</> </>
); );

View File

@ -8,7 +8,7 @@ import { useForm } from "react-hook-form";
// components // components
import { EmailResetPasswordForm } from "components/account"; import { EmailResetPasswordForm } from "components/account";
// ui // ui
import { Input, SecondaryButton } from "components/ui"; import { Input, PrimaryButton } from "components/ui";
// types // types
type EmailPasswordFormValues = { type EmailPasswordFormValues = {
email: string; email: string;
@ -42,28 +42,39 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
return ( return (
<> <>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
{isResettingPassword
? "Reset your password"
: isSignUpPage
? "Sign up on Plane"
: "Sign in to Plane"}
</h1>
{isResettingPassword ? ( {isResettingPassword ? (
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} /> <EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
) : ( ) : (
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(onSubmit)}> <form
<div> className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
onSubmit={handleSubmit(onSubmit)}
>
<div className="space-y-1">
<Input <Input
id="email" id="email"
type="email" type="email"
name="email" name="email"
register={register} register={register}
validations={{ validations={{
required: "Email ID is required", required: "Email address is required",
validate: (value) => validate: (value) =>
/^(([^<>()[\]\\.,;:\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( /^(([^<>()[\]\\.,;:\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 value
) || "Email ID is not valid", ) || "Email address is not valid",
}} }}
error={errors.email} error={errors.email}
placeholder="Enter your email ID" placeholder="Enter your email address..."
className="border-custom-border-300 h-[46px]"
/> />
</div> </div>
<div className="mt-5"> <div className="space-y-1">
<Input <Input
id="password" id="password"
type="password" type="password"
@ -73,14 +84,14 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
required: "Password is required", required: "Password is required",
}} }}
error={errors.password} error={errors.password}
placeholder="Enter your password" placeholder="Enter your password..."
className="border-custom-border-300 h-[46px]"
/> />
</div> </div>
<div className="mt-2 flex items-center justify-between"> <div className="text-right text-xs">
<div className="ml-auto text-sm">
{isSignUpPage ? ( {isSignUpPage ? (
<Link href="/"> <Link href="/">
<a className="font-medium text-brand-accent hover:text-brand-accent"> <a className="text-custom-text-200 hover:text-custom-primary-100">
Already have an account? Sign in. Already have an account? Sign in.
</a> </a>
</Link> </Link>
@ -88,31 +99,30 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
<button <button
type="button" type="button"
onClick={() => setIsResettingPassword(true)} onClick={() => setIsResettingPassword(true)}
className="font-medium text-brand-accent hover:text-brand-accent" className="text-custom-text-200 hover:text-custom-primary-100"
> >
Forgot your password? Forgot your password?
</button> </button>
)} )}
</div> </div>
</div> <div>
<div className="mt-5"> <PrimaryButton
<SecondaryButton
type="submit" type="submit"
className="w-full text-center" className="w-full text-center h-[46px]"
disabled={!isValid && isDirty} disabled={!isValid && isDirty}
loading={isSubmitting} loading={isSubmitting}
> >
{isSignUpPage {isSignUpPage
? isSubmitting ? isSubmitting
? "Signing up..." ? "Signing up..."
: "Sign Up" : "Sign up"
: isSubmitting : isSubmitting
? "Signing in..." ? "Signing in..."
: "Sign In"} : "Sign in"}
</SecondaryButton> </PrimaryButton>
{!isSignUpPage && ( {!isSignUpPage && (
<Link href="/sign-up"> <Link href="/sign-up">
<a className="block font-medium text-brand-accent hover:text-brand-accent text-sm mt-1"> <a className="block text-custom-text-200 hover:text-custom-primary-100 text-xs mt-4">
Don{"'"}t have an account? Sign up. Don{"'"}t have an account? Sign up.
</a> </a>
</Link> </Link>

View File

@ -59,32 +59,36 @@ export const EmailResetPasswordForm: React.FC<Props> = ({ setIsResettingPassword
}; };
return ( return (
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(forgotPassword)}> <form
<div> className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
onSubmit={handleSubmit(forgotPassword)}
>
<div className="space-y-1">
<Input <Input
id="email" id="email"
type="email" type="email"
name="email" name="email"
register={register} register={register}
validations={{ validations={{
required: "Email ID is required", required: "Email address is required",
validate: (value) => validate: (value) =>
/^(([^<>()[\]\\.,;:\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( /^(([^<>()[\]\\.,;:\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 value
) || "Email ID is not valid", ) || "Email address is not valid",
}} }}
error={errors.email} error={errors.email}
placeholder="Enter registered Email ID" placeholder="Enter registered email address.."
className="border-custom-border-300 h-[46px]"
/> />
</div> </div>
<div className="mt-5 flex items-center gap-2"> <div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
<SecondaryButton <SecondaryButton
className="w-full text-center" className="w-full text-center h-[46px]"
onClick={() => setIsResettingPassword(false)} onClick={() => setIsResettingPassword(false)}
> >
Go Back Go Back
</SecondaryButton> </SecondaryButton>
<PrimaryButton type="submit" className="w-full text-center" loading={isSubmitting}> <PrimaryButton type="submit" className="w-full text-center h-[46px]" loading={isSubmitting}>
{isSubmitting ? "Sending link..." : "Send reset link"} {isSubmitting ? "Sending link..." : "Send reset link"}
</PrimaryButton> </PrimaryButton>
</div> </div>

View File

@ -1,9 +1,14 @@
import { useEffect, useState, FC } from "react"; import { useEffect, useState, FC } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// next-themes
import { useTheme } from "next-themes";
// images // images
import githubImage from "/public/logos/github-black.png"; import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
const { NEXT_PUBLIC_GITHUB_ID } = process.env; const { NEXT_PUBLIC_GITHUB_ID } = process.env;
@ -11,15 +16,15 @@ export interface GithubLoginButtonProps {
handleSignIn: React.Dispatch<string>; handleSignIn: React.Dispatch<string>;
} }
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => { export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => {
const { handleSignIn } = props; const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
// router const [gitCode, setGitCode] = useState<null | string>(null);
const { const {
query: { code }, query: { code },
} = useRouter(); } = useRouter();
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const { theme } = useTheme();
const [gitCode, setGitCode] = useState<null | string>(null);
useEffect(() => { useEffect(() => {
if (code && !gitCode) { if (code && !gitCode) {
@ -35,13 +40,18 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
}, []); }, []);
return ( return (
<div className="w-full flex justify-center items-center px-[3px]"> <div className="w-full flex justify-center items-center">
<Link <Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`} href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
> >
<button className="flex w-full items-center justify-center gap-3 rounded border border-brand-base p-2 text-sm font-medium text-brand-secondary duration-300 hover:bg-brand-surface-2"> <button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
<Image src={githubImage} height={20} width={20} color="#000" alt="GitHub Logo" /> <Image
<span>Sign In with Github</span> src={theme === "dark" ? githubWhiteImage : githubBlackImage}
height={20}
width={20}
alt="GitHub Logo"
/>
<span>Sign in with GitHub</span>
</button> </button>
</Link> </Link>
</div> </div>

View File

@ -1,5 +1,5 @@
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
// next
import Script from "next/script"; import Script from "next/script";
export interface IGoogleLoginButton { export interface IGoogleLoginButton {
@ -8,18 +8,18 @@ export interface IGoogleLoginButton {
styles?: CSSProperties; styles?: CSSProperties;
} }
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => { export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
const { handleSignIn } = props;
const googleSignInButton = useRef<HTMLDivElement>(null); const googleSignInButton = useRef<HTMLDivElement>(null);
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
const loadScript = useCallback(() => { const loadScript = useCallback(() => {
if (!googleSignInButton.current || gsiScriptLoaded) return; if (!googleSignInButton.current || gsiScriptLoaded) return;
window?.google?.accounts.id.initialize({ window?.google?.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
callback: handleSignIn, callback: handleSignIn,
}); });
window?.google?.accounts.id.renderButton( window?.google?.accounts.id.renderButton(
googleSignInButton.current, googleSignInButton.current,
{ {
@ -27,11 +27,13 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
theme: "outline", theme: "outline",
size: "large", size: "large",
logo_alignment: "center", logo_alignment: "center",
width: "410", width: "360",
text: "continue_with", text: "signin_with",
} as GsiButtonConfiguration // customization attributes } as GsiButtonConfiguration // customization attributes
); );
window?.google?.accounts.id.prompt(); // also display the One Tap dialog window?.google?.accounts.id.prompt(); // also display the One Tap dialog
setGsiScriptLoaded(true); setGsiScriptLoaded(true);
}, [handleSignIn, gsiScriptLoaded]); }, [handleSignIn, gsiScriptLoaded]);
@ -48,7 +50,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
<> <>
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} /> <Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
<div <div
className="overflow-hidden rounded w-full flex justify-center items-center" className="overflow-hidden rounded w-full flex justify-center items-center !text-sm !font-medium !text-custom-text-100"
id="googleSignInButton" id="googleSignInButton"
ref={googleSignInButton} ref={googleSignInButton}
/> />

View File

@ -97,7 +97,7 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" /> <div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-10 overflow-y-auto">
@ -111,10 +111,13 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> <Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div> <div>
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base"> <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Save Analytics Save Analytics
</Dialog.Title> </Dialog.Title>
<div className="mt-5"> <div className="mt-5">

View File

@ -61,7 +61,7 @@ export const CustomAnalytics: React.FC<Props> = ({
<AnalyticsSelectBar <AnalyticsSelectBar
control={control} control={control}
setValue={setValue} setValue={setValue}
projects={projects} projects={projects ?? []}
params={params} params={params}
fullScreen={fullScreen} fullScreen={fullScreen}
isProjectLevel={isProjectLevel} isProjectLevel={isProjectLevel}
@ -86,7 +86,7 @@ export const CustomAnalytics: React.FC<Props> = ({
</div> </div>
) : ( ) : (
<div className="grid h-full place-items-center p-5"> <div className="grid h-full place-items-center p-5">
<div className="space-y-4 text-brand-secondary"> <div className="space-y-4 text-custom-text-200">
<p className="text-sm">No matching issues found. Try changing the parameters.</p> <p className="text-sm">No matching issues found. Try changing the parameters.</p>
</div> </div>
</div> </div>
@ -104,7 +104,7 @@ export const CustomAnalytics: React.FC<Props> = ({
) )
) : ( ) : (
<div className="grid h-full place-items-center p-5"> <div className="grid h-full place-items-center p-5">
<div className="space-y-4 text-brand-secondary"> <div className="space-y-4 text-custom-text-200">
<p className="text-sm">There was some error in fetching the data.</p> <p className="text-sm">There was some error in fetching the data.</p>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<PrimaryButton <PrimaryButton

View File

@ -31,7 +31,7 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
} }
return ( return (
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs"> <div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
<span <span
className="h-3 w-3 rounded" className="h-3 w-3 rounded"
style={{ style={{
@ -39,7 +39,7 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
}} }}
/> />
<span <span
className={`font-medium text-brand-secondary ${ className={`font-medium text-custom-text-200 ${
params.segment params.segment
? params.segment === "priority" || params.segment === "state__group" ? params.segment === "priority" || params.segment === "state__group"
? "capitalize" ? "capitalize"

View File

@ -111,7 +111,6 @@ export const AnalyticsGraph: React.FC<Props> = ({
: undefined, : undefined,
}} }}
theme={{ theme={{
background: "rgb(var(--color-bg-base))",
axis: {}, axis: {},
}} }}
/> />

View File

@ -29,7 +29,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
> >
{!isProjectLevel && ( {!isProjectLevel && (
<div> <div>
<h6 className="text-xs text-brand-secondary">Project</h6> <h6 className="text-xs text-custom-text-200">Project</h6>
<Controller <Controller
name="project" name="project"
control={control} control={control}
@ -40,7 +40,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
</div> </div>
)} )}
<div> <div>
<h6 className="text-xs text-brand-secondary">Measure (y-axis)</h6> <h6 className="text-xs text-custom-text-200">Measure (y-axis)</h6>
<Controller <Controller
name="y_axis" name="y_axis"
control={control} control={control}
@ -50,7 +50,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
/> />
</div> </div>
<div> <div>
<h6 className="text-xs text-brand-secondary">Dimension (x-axis)</h6> <h6 className="text-xs text-custom-text-200">Dimension (x-axis)</h6>
<Controller <Controller
name="x_axis" name="x_axis"
control={control} control={control}
@ -67,7 +67,7 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
/> />
</div> </div>
<div> <div>
<h6 className="text-xs text-brand-secondary">Group</h6> <h6 className="text-xs text-custom-text-200">Group</h6>
<Controller <Controller
name="segment" name="segment"
control={control} control={control}

View File

@ -23,13 +23,14 @@ import {
import { ContrastIcon, LayerDiagonalIcon } from "components/icons"; import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
// helpers // helpers
import { renderShortDate } from "helpers/date-time.helper"; import { renderShortDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper";
// types // types
import { import {
IAnalyticsParams, IAnalyticsParams,
IAnalyticsResponse, IAnalyticsResponse,
ICurrentUserResponse, ICurrentUserResponse,
IExportAnalyticsFormData, IExportAnalyticsFormData,
IProject,
IWorkspace, IWorkspace,
} from "types"; } from "types";
// fetch-keys // fetch-keys
@ -178,23 +179,23 @@ export const AnalyticsSidebar: React.FC<Props> = ({
}; };
const selectedProjects = const selectedProjects =
params.project && params.project.length > 0 ? params.project : projects.map((p) => p.id); params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
return ( return (
<div <div
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${ className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
fullScreen fullScreen
? "border-l border-brand-base md:h-full md:border-l md:border-brand-base md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5" ? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
: "" : ""
}`} }`}
> >
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1 bg-brand-surface-2 rounded-md px-3 py-1 text-brand-secondary text-xs"> <div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
<LayerDiagonalIcon height={14} width={14} /> <LayerDiagonalIcon height={14} width={14} />
{analytics ? analytics.total : "..."} Issues {analytics ? analytics.total : "..."} Issues
</div> </div>
{isProjectLevel && ( {isProjectLevel && (
<div className="flex items-center gap-1 bg-brand-surface-2 rounded-md px-3 py-1 text-brand-secondary text-xs"> <div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
<CalendarDaysIcon className="h-3.5 w-3.5" /> <CalendarDaysIcon className="h-3.5 w-3.5" />
{renderShortDate( {renderShortDate(
(cycleId (cycleId
@ -206,7 +207,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</div> </div>
)} )}
</div> </div>
<div className="h-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
{fullScreen ? ( {fullScreen ? (
<> <>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && ( {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
@ -214,14 +215,15 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<h4 className="font-medium">Selected Projects</h4> <h4 className="font-medium">Selected Projects</h4>
<div className="space-y-6 mt-4 h-full overflow-y-auto"> <div className="space-y-6 mt-4 h-full overflow-y-auto">
{selectedProjects.map((projectId) => { {selectedProjects.map((projectId) => {
const project: IProject = projects.find((p) => p.id === projectId); const project = projects?.find((p) => p.id === projectId);
if (project)
return ( return (
<div key={project.id}> <div key={project.id} className="w-full">
<div className="text-sm flex items-center gap-1"> <div className="text-sm flex items-center gap-1">
{project.emoji ? ( {project.emoji ? (
<span className="grid h-6 w-6 flex-shrink-0 place-items-center"> <span className="grid h-6 w-6 flex-shrink-0 place-items-center">
{String.fromCodePoint(parseInt(project.emoji))} {renderEmoji(project.emoji)}
</span> </span>
) : project.icon_prop ? ( ) : project.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0"> <div className="h-6 w-6 grid place-items-center flex-shrink-0">
@ -237,34 +239,34 @@ export const AnalyticsSidebar: React.FC<Props> = ({
{project?.name.charAt(0)} {project?.name.charAt(0)}
</span> </span>
)} )}
<h5 className="break-words"> <h5 className="flex items-center gap-1">
{project.name} <p className="break-words">{truncateText(project.name, 20)}</p>
<span className="text-brand-secondary text-xs ml-1"> <span className="text-custom-text-200 text-xs ml-1">
({project.identifier}) ({project.identifier})
</span> </span>
</h5> </h5>
</div> </div>
<div className="mt-4 space-y-3 pl-2"> <div className="mt-4 space-y-3 pl-2 w-full">
<div className="flex items-center justify-between gap-2 text-xs"> <div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserGroupIcon className="h-4 w-4 text-brand-secondary" /> <UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<h6>Total members</h6> <h6>Total members</h6>
</div> </div>
<span className="text-brand-secondary">{project.total_members}</span> <span className="text-custom-text-200">{project.total_members}</span>
</div> </div>
<div className="flex items-center justify-between gap-2 text-xs"> <div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ContrastIcon height={16} width={16} /> <ContrastIcon height={16} width={16} />
<h6>Total cycles</h6> <h6>Total cycles</h6>
</div> </div>
<span className="text-brand-secondary">{project.total_cycles}</span> <span className="text-custom-text-200">{project.total_cycles}</span>
</div> </div>
<div className="flex items-center justify-between gap-2 text-xs"> <div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserGroupIcon className="h-4 w-4 text-brand-secondary" /> <UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<h6>Total modules</h6> <h6>Total modules</h6>
</div> </div>
<span className="text-brand-secondary">{project.total_modules}</span> <span className="text-custom-text-200">{project.total_modules}</span>
</div> </div>
</div> </div>
</div> </div>
@ -279,13 +281,13 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4> <h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4>
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Lead</h6> <h6 className="text-custom-text-200">Lead</h6>
<span> <span>
{cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name} {cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Start Date</h6> <h6 className="text-custom-text-200">Start Date</h6>
<span> <span>
{cycleDetails.start_date && cycleDetails.start_date !== "" {cycleDetails.start_date && cycleDetails.start_date !== ""
? renderShortDate(cycleDetails.start_date) ? renderShortDate(cycleDetails.start_date)
@ -293,7 +295,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Target Date</h6> <h6 className="text-custom-text-200">Target Date</h6>
<span> <span>
{cycleDetails.end_date && cycleDetails.end_date !== "" {cycleDetails.end_date && cycleDetails.end_date !== ""
? renderShortDate(cycleDetails.end_date) ? renderShortDate(cycleDetails.end_date)
@ -307,14 +309,14 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4> <h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4>
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Lead</h6> <h6 className="text-custom-text-200">Lead</h6>
<span> <span>
{moduleDetails.lead_detail?.first_name}{" "} {moduleDetails.lead_detail?.first_name}{" "}
{moduleDetails.lead_detail?.last_name} {moduleDetails.lead_detail?.last_name}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Start Date</h6> <h6 className="text-custom-text-200">Start Date</h6>
<span> <span>
{moduleDetails.start_date && moduleDetails.start_date !== "" {moduleDetails.start_date && moduleDetails.start_date !== ""
? renderShortDate(moduleDetails.start_date) ? renderShortDate(moduleDetails.start_date)
@ -322,7 +324,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Target Date</h6> <h6 className="text-custom-text-200">Target Date</h6>
<span> <span>
{moduleDetails.target_date && moduleDetails.target_date !== "" {moduleDetails.target_date && moduleDetails.target_date !== ""
? renderShortDate(moduleDetails.target_date) ? renderShortDate(moduleDetails.target_date)
@ -336,7 +338,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{projectDetails?.emoji ? ( {projectDetails?.emoji ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center"> <div className="grid h-6 w-6 flex-shrink-0 place-items-center">
{String.fromCodePoint(parseInt(projectDetails.emoji))} {renderEmoji(projectDetails.emoji)}
</div> </div>
) : projectDetails?.icon_prop ? ( ) : projectDetails?.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0"> <div className="h-6 w-6 grid place-items-center flex-shrink-0">
@ -356,7 +358,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</div> </div>
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Network</h6> <h6 className="text-custom-text-200">Network</h6>
<span> <span>
{ {
NETWORK_CHOICES[ NETWORK_CHOICES[

View File

@ -37,9 +37,9 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
<div className="flow-root"> <div className="flow-root">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="inline-block min-w-full align-middle"> <div className="inline-block min-w-full align-middle">
<table className="min-w-full divide-y divide-brand-base whitespace-nowrap border-y border-brand-base"> <table className="min-w-full divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
<thead className="bg-brand-surface-2"> <thead className="bg-custom-background-80">
<tr className="divide-x divide-brand-base text-sm text-brand-base"> <tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">
<th scope="col" className="py-3 px-2.5 text-left font-medium"> <th scope="col" className="py-3 px-2.5 text-left font-medium">
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label} {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
</th> </th>
@ -80,11 +80,11 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
)} )}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-brand-base"> <tbody className="divide-y divide-custom-border-200">
{barGraphData.data.map((item, index) => ( {barGraphData.data.map((item, index) => (
<tr <tr
key={`table-row-${index}`} key={`table-row-${index}`}
className="divide-x divide-brand-base text-xs text-brand-secondary" className="divide-x divide-custom-border-200 text-xs text-custom-text-200"
> >
<td <td
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${ className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${

View File

@ -150,16 +150,16 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
return ( return (
<div <div
className={`absolute top-0 z-30 h-full bg-brand-surface-1 ${ className={`absolute top-0 z-30 h-full bg-custom-background-90 ${
fullScreen ? "p-2 w-full" : "w-1/2" fullScreen ? "p-2 w-full" : "w-1/2"
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`} } ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
> >
<div <div
className={`flex h-full flex-col overflow-hidden border-brand-base bg-brand-base text-left ${ className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
fullScreen ? "rounded-lg border" : "border-l" fullScreen ? "rounded-lg border" : "border-l"
}`} }`}
> >
<div className="flex items-center justify-between gap-4 bg-brand-base px-5 py-4 text-sm"> <div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
<h3 className="break-words"> <h3 className="break-words">
Analytics for{" "} Analytics for{" "}
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name} {cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
@ -167,7 +167,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
className="grid place-items-center p-1 text-brand-secondary hover:text-brand-base" className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
onClick={() => setFullScreen((prevData) => !prevData)} onClick={() => setFullScreen((prevData) => !prevData)}
> >
{fullScreen ? ( {fullScreen ? (
@ -178,7 +178,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
</button> </button>
<button <button
type="button" type="button"
className="grid place-items-center p-1 text-brand-secondary hover:text-brand-base" className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
onClick={handleClose} onClick={handleClose}
> >
<XMarkIcon className="h-4 w-4" /> <XMarkIcon className="h-4 w-4" />
@ -186,13 +186,13 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
</div> </div>
</div> </div>
<Tab.Group as={Fragment}> <Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-brand-base p-5 pt-0"> <Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
{tabsList.map((tab) => ( {tabsList.map((tab) => (
<Tab <Tab
key={tab} key={tab}
className={({ selected }) => className={({ selected }) =>
`rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-surface-2 ${ `rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
selected ? "bg-brand-surface-2" : "" selected ? "bg-custom-background-80" : ""
}` }`
} }
onClick={() => trackAnalyticsEvent(tab)} onClick={() => trackAnalyticsEvent(tab)}

View File

@ -10,10 +10,10 @@ type Props = {
}; };
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => ( export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="space-y-3 rounded-[10px] border border-brand-base p-3"> <div className="space-y-3 rounded-[10px] border border-custom-border-200 p-3">
<h5 className="text-xs text-red-500">DEMAND</h5> <h5 className="text-xs text-red-500">DEMAND</h5>
<div> <div>
<h4 className="text-brand-bas text-base font-medium">Total open tasks</h4> <h4 className="text-custom-text-100 text-base font-medium">Total open tasks</h4>
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3> <h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
@ -31,13 +31,13 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
}} }}
/> />
<h6 className="capitalize">{group.state_group}</h6> <h6 className="capitalize">{group.state_group}</h6>
<span className="ml-1 rounded-3xl bg-brand-surface-2 px-2 py-0.5 text-[0.65rem] text-brand-secondary"> <span className="ml-1 rounded-3xl bg-custom-background-80 px-2 py-0.5 text-[0.65rem] text-custom-text-200">
{group.state_count} {group.state_count}
</span> </span>
</div> </div>
<p className="text-brand-secondary">{percentage}%</p> <p className="text-custom-text-200">{percentage}%</p>
</div> </div>
<div className="bar relative h-1 w-full rounded bg-brand-surface-2"> <div className="bar relative h-1 w-full rounded bg-custom-background-80">
<div <div
className="absolute top-0 left-0 h-1 rounded duration-300" className="absolute top-0 left-0 h-1 rounded duration-300"
style={{ style={{
@ -50,8 +50,8 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
); );
})} })}
</div> </div>
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs"> <div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
<p className="flex items-center gap-1 text-brand-secondary"> <p className="flex items-center gap-1 text-custom-text-200">
<PlayIcon className="h-4 w-4 -rotate-90" aria-hidden="true" /> <PlayIcon className="h-4 w-4 -rotate-90" aria-hidden="true" />
<span>Estimate Demand:</span> <span>Estimate Demand:</span>
</p> </p>

View File

@ -10,7 +10,7 @@ type Props = {
}; };
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => ( export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
<div className="p-3 border border-brand-base rounded-[10px]"> <div className="p-3 border border-custom-border-200 rounded-[10px]">
<h6 className="text-base font-medium">{title}</h6> <h6 className="text-base font-medium">{title}</h6>
{users.length > 0 ? ( {users.length > 0 ? (
<div className="mt-3 space-y-3"> <div className="mt-3 space-y-3">
@ -33,7 +33,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
{user.firstName !== "" ? user.firstName[0] : "?"} {user.firstName !== "" ? user.firstName[0] : "?"}
</div> </div>
)} )}
<span className="break-words text-brand-secondary"> <span className="break-words text-custom-text-200">
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"} {user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
</span> </span>
</div> </div>
@ -42,7 +42,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
))} ))}
</div> </div>
) : ( ) : (
<div className="text-brand-secondary text-center text-sm py-8">No matching data found.</div> <div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
)} )}
</div> </div>
); );

View File

@ -88,7 +88,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
) )
) : ( ) : (
<div className="grid h-full place-items-center p-5"> <div className="grid h-full place-items-center p-5">
<div className="space-y-4 text-brand-secondary"> <div className="space-y-4 text-custom-text-200">
<p className="text-sm">There was some error in fetching the data.</p> <p className="text-sm">There was some error in fetching the data.</p>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<PrimaryButton onClick={() => mutateDefaultAnalytics()}>Refresh</PrimaryButton> <PrimaryButton onClick={() => mutateDefaultAnalytics()}>Refresh</PrimaryButton>

View File

@ -8,9 +8,9 @@ type Props = {
}; };
export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => ( export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="rounded-[10px] border border-brand-base"> <div className="rounded-[10px] border border-custom-border-200">
<h5 className="p-3 text-xs text-green-500">SCOPE</h5> <h5 className="p-3 text-xs text-green-500">SCOPE</h5>
<div className="divide-y divide-brand-base"> <div className="divide-y divide-custom-border-200">
<div> <div>
<h6 className="px-3 text-base font-medium">Pending issues</h6> <h6 className="px-3 text-base font-medium">Pending issues</h6>
{defaultAnalytics.pending_issue_user.length > 0 ? ( {defaultAnalytics.pending_issue_user.length > 0 ? (
@ -27,8 +27,8 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
); );
return ( return (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs"> <div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
<span className="font-medium text-brand-secondary"> <span className="font-medium text-custom-text-200">
{assignee {assignee
? assignee.assignees__first_name + " " + assignee.assignees__last_name ? assignee.assignees__first_name + " " + assignee.assignees__last_name
: "No assignee"} : "No assignee"}
@ -69,12 +69,11 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
}} }}
margin={{ top: 20 }} margin={{ top: 20 }}
theme={{ theme={{
background: "rgb(var(--color-bg-base))",
axis: {}, axis: {},
}} }}
/> />
) : ( ) : (
<div className="text-brand-secondary text-center text-sm py-8"> <div className="text-custom-text-200 text-center text-sm py-8">
No matching data found. No matching data found.
</div> </div>
)} )}

View File

@ -9,20 +9,15 @@ type Props = {
defaultAnalytics: IDefaultAnalyticsResponse; defaultAnalytics: IDefaultAnalyticsResponse;
}; };
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => { export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => (
const currentMonth = new Date().getMonth(); <div className="py-3 border border-custom-border-200 rounded-[10px]">
const startMonth = Math.floor(currentMonth / 3) * 3 + 1;
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2];
return (
<div className="py-3 border border-brand-base rounded-[10px]">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1> <h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
{defaultAnalytics.issue_completed_month_wise.length > 0 ? ( {defaultAnalytics.issue_completed_month_wise.length > 0 ? (
<LineGraph <LineGraph
data={[ data={[
{ {
id: "issues_closed", id: "issues_closed",
color: "rgb(var(--color-accent))", color: "rgb(var(--color-primary-100))",
data: MONTHS_LIST.map((month) => ({ data: MONTHS_LIST.map((month) => ({
x: month.label.substring(0, 3), x: month.label.substring(0, 3),
y: y:
@ -32,31 +27,28 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
})), })),
}, },
]} ]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => { customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
if (quarterMonthsList.includes(data.month)) return data.count; (data) => data.count
)}
return 0;
})}
height="300px" height="300px"
colors={(datum) => datum.color} colors={(datum) => datum.color}
curve="monotoneX" curve="monotoneX"
margin={{ top: 20 }} margin={{ top: 20 }}
enableSlices="x" enableSlices="x"
sliceTooltip={(datum) => ( sliceTooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs"> <div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
{datum.slice.points[0].data.yFormatted} {datum.slice.points[0].data.yFormatted}
<span className="text-brand-secondary"> issues closed in </span> <span className="text-custom-text-200"> issues closed in </span>
{datum.slice.points[0].data.xFormatted} {datum.slice.points[0].data.xFormatted}
</div> </div>
)} )}
theme={{ theme={{
background: "rgb(var(--color-bg-base))", background: "rgb(var(--color-background-100))",
}} }}
enableArea enableArea
/> />
) : ( ) : (
<div className="text-brand-secondary text-center text-sm py-8">No matching data found.</div> <div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
)} )}
</div> </div>
); );
};

View File

@ -15,7 +15,7 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
query: project.name + project.identifier, query: project.name + project.identifier,
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-brand-secondary text-[0.65rem]">{project.identifier}</span> <span className="text-custom-text-200 text-[0.65rem]">{project.identifier}</span>
{project.name} {project.name}
</div> </div>
), ),

View File

@ -23,7 +23,7 @@ export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => {
label={ label={
<span> <span>
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? ( {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? (
<span className="text-brand-secondary">No value</span> <span className="text-custom-text-200">No value</span>
)} )}
</span> </span>
} }

View File

@ -22,7 +22,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
return ( return (
<DefaultLayout> <DefaultLayout>
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-brand-surface-1 text-center"> <div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
<div className="h-44 w-72"> <div className="h-44 w-72">
<Image <Image
src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg} src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg}
@ -31,16 +31,16 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
alt="ProjectSettingImg" alt="ProjectSettingImg"
/> />
</div> </div>
<h1 className="text-xl font-medium text-brand-base"> <h1 className="text-xl font-medium text-custom-text-100">
Oops! You are not authorized to view this page Oops! You are not authorized to view this page
</h1> </h1>
<div className="w-full max-w-md text-base text-brand-secondary"> <div className="w-full max-w-md text-base text-custom-text-200">
{user ? ( {user ? (
<p> <p>
You have signed in as {user.email}. <br /> You have signed in as {user.email}. <br />
<Link href={`/?next=${currentPath}`}> <Link href={`/?next=${currentPath}`}>
<a className="font-medium text-brand-base">Sign in</a> <a className="font-medium text-custom-text-100">Sign in</a>
</Link>{" "} </Link>{" "}
with different account that has access to this page. with different account that has access to this page.
</p> </p>
@ -48,7 +48,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
<p> <p>
You need to{" "} You need to{" "}
<Link href={`/?next=${currentPath}`}> <Link href={`/?next=${currentPath}`}>
<a className="font-medium text-brand-base">Sign in</a> <a className="font-medium text-custom-text-100">Sign in</a>
</Link>{" "} </Link>{" "}
with an account that has access to this page. with an account that has access to this page.
</p> </p>

View File

@ -41,13 +41,15 @@ export const JoinProject: React.FC = () => {
}; };
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-brand-surface-1 text-center"> <div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
<div className="h-44 w-72"> <div className="h-44 w-72">
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" /> <Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
</div> </div>
<h1 className="text-xl font-medium text-brand-base">You are not a member of this project</h1> <h1 className="text-xl font-medium text-custom-text-100">
You are not a member of this project
</h1>
<div className="w-full max-w-md text-base text-brand-secondary"> <div className="w-full max-w-md text-base text-custom-text-200">
<p className="mx-auto w-full text-sm md:w-3/4"> <p className="mx-auto w-full text-sm md:w-3/4">
You are not a member of this project, but you can join this project by clicking the button You are not a member of this project, but you can join this project by clicking the button
below. below.

View File

@ -11,7 +11,7 @@ export const NotAWorkspaceMember = () => (
<div className="space-y-8 text-center"> <div className="space-y-8 text-center">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-lg font-semibold">Not Authorized!</h3> <h3 className="text-lg font-semibold">Not Authorized!</h3>
<p className="mx-auto w-1/2 text-sm text-brand-secondary"> <p className="mx-auto w-1/2 text-sm text-custom-text-200">
You{"'"}re not a member of this workspace. Please contact the workspace admin to get an You{"'"}re not a member of this workspace. Please contact the workspace admin to get an
invitation or check your pending invitations. invitation or check your pending invitations.
</p> </p>

View File

@ -0,0 +1,90 @@
import React, { useState } from "react";
// component
import { CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
// types
import { IProject } from "types";
type Props = {
projectDetails: IProject | undefined;
handleChange: (formData: Partial<IProject>) => Promise<void>;
};
export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
const [monthModal, setmonthModal] = useState(false);
const initialValues: Partial<IProject> = { archive_in: 1 };
return (
<>
<SelectMonthModal
type="auto-archive"
initialValues={initialValues}
isOpen={monthModal}
handleClose={() => setmonthModal(false)}
handleChange={handleChange}
/>
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-100 bg-custom-background-90">
<div className="flex items-center justify-between gap-x-8 gap-y-2">
<div className="flex flex-col gap-2.5">
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
<p className="text-sm text-custom-text-200">
Plane will automatically archive issues that have been completed or cancelled for the
configured time period.
</p>
</div>
<ToggleSwitch
value={projectDetails?.archive_in !== 0}
onChange={() =>
projectDetails?.archive_in === 0
? handleChange({ archive_in: 1 })
: handleChange({ archive_in: 0 })
}
size="sm"
/>
</div>
{projectDetails?.archive_in !== 0 && (
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">
Auto-archive issues that are closed for
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.archive_in}
label={`${projectDetails?.archive_in} ${
projectDetails?.archive_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ archive_in: val });
}}
input
verticalPosition="top"
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
</CustomSelect.Option>
))}
<button
type="button"
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
Customize Time Range
</button>
</>
</CustomSelect>
</div>
</div>
)}
</div>
</>
);
};

View File

@ -0,0 +1,177 @@
import React, { useState } from "react";
import useSWR from "swr";
import { useRouter } from "next/router";
// component
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
// services
import stateService from "services/state.service";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
import { STATES_LIST } from "constants/fetch-keys";
// types
import { IProject } from "types";
// helper
import { getStatesList } from "helpers/state.helper";
type Props = {
projectDetails: IProject | undefined;
handleChange: (formData: Partial<IProject>) => Promise<void>;
};
export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
const [monthModal, setmonthModal] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
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 ?? {});
const options = states
?.filter((state) => state.group === "cancelled")
.map((state) => ({
value: state.id,
query: state.name,
content: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)}
{state.name}
</div>
),
}));
const multipleOptions = options.length > 1;
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
const selectedOption = states?.find(
(s) => s.id === projectDetails?.default_state ?? defaultState
);
const currentDefaultState = states.find((s) => s.id === defaultState);
const initialValues: Partial<IProject> = {
close_in: 1,
default_state: defaultState,
};
return (
<>
<SelectMonthModal
type="auto-close"
initialValues={initialValues}
isOpen={monthModal}
handleClose={() => setmonthModal(false)}
handleChange={handleChange}
/>
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-100 bg-custom-background-90">
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
<div className="flex flex-col gap-2.5">
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
<p className="text-sm text-custom-text-200">
Plane will automatically close the issues that have not been updated for the
configured time period.
</p>
</div>
<ToggleSwitch
value={projectDetails?.close_in !== 0}
onChange={() =>
projectDetails?.close_in === 0
? handleChange({ close_in: 1, default_state: defaultState })
: handleChange({ close_in: 0, default_state: null })
}
size="sm"
/>
</div>
{projectDetails?.close_in !== 0 && (
<div className="flex flex-col gap-4 w-full">
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">
Auto-close issues that are inactive for
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.close_in}
label={`${projectDetails?.close_in} ${
projectDetails?.close_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ close_in: val });
}}
input
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
</CustomSelect.Option>
))}
<button
type="button"
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
Customize Time Range
</button>
</>
</CustomSelect>
</div>
</div>
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">Auto-close Status</div>
<div className="w-1/2 ">
<CustomSearchSelect
value={
projectDetails?.default_state ? projectDetails?.default_state : defaultState
}
label={
<div className="flex items-center gap-2">
{selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
) : currentDefaultState ? (
getStateGroupIcon(
currentDefaultState.group,
"16",
"16",
currentDefaultState.color
)
) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)}
{selectedOption?.name
? selectedOption.name
: currentDefaultState?.name ?? (
<span className="text-custom-text-200">State</span>
)}
</div>
}
onChange={(val: string) => {
handleChange({ default_state: val });
}}
options={options}
disabled={!multipleOptions}
width="w-full"
input
/>
</div>
</div>
</div>
)}
</div>
</>
);
};

View File

@ -0,0 +1,3 @@
export * from "./auto-close-automation";
export * from "./auto-archive-automation";
export * from "./select-month-modal";

View File

@ -0,0 +1,147 @@
import React from "react";
import { useRouter } from "next/router";
// react-hook-form
import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// types
import type { IProject } from "types";
// types
type Props = {
isOpen: boolean;
type: "auto-close" | "auto-archive";
initialValues: Partial<IProject>;
handleClose: () => void;
handleChange: (formData: Partial<IProject>) => Promise<void>;
};
export const SelectMonthModal: React.FC<Props> = ({
type,
initialValues,
isOpen,
handleClose,
handleChange,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
} = useForm<IProject>({
defaultValues: initialValues,
});
const onClose = () => {
handleClose();
reset(initialValues);
};
const onSubmit = (formData: Partial<IProject>) => {
if (!workspaceSlug && !projectId) return;
handleChange(formData);
onClose();
};
const inputSection = (name: string) => (
<div className="relative flex flex-col gap-1 justify-center w-full">
<Input
type="number"
id={name}
name={name}
placeholder="Enter Months"
autoComplete="off"
register={register}
width="full"
validations={{
required: "Select a month between 1 and 12.",
min: 1,
max: 12,
}}
className="border-custom-border-200"
/>
<span className="absolute text-sm text-custom-text-200 top-2.5 right-8">Months</span>
</div>
);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-30" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-90 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Customize Time Range
</Dialog.Title>
<div className="mt-8 flex items-center gap-2">
<div className="flex w-full flex-col gap-1 justify-center">
{type === "auto-close" ? (
<>
{inputSection("close_in")}
{errors.close_in && (
<span className="text-sm px-1 text-red-500">
Select a month between 1 and 12.
</span>
)}
</>
) : (
<>
{inputSection("archive_in")}
{errors.archive_in && (
<span className="text-sm px-1 text-red-500">
Select a month between 1 and 12.
</span>
)}
</>
)}
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</PrimaryButton>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -17,12 +17,12 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
<div className="flex items-center"> <div className="flex items-center">
<button <button
type="button" type="button"
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1" className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-custom-sidebar-border-200 text-center text-sm hover:bg-custom-sidebar-background-90"
onClick={() => router.back()} onClick={() => router.back()}
> >
<Icon <Icon
iconName="keyboard_backspace" iconName="keyboard_backspace"
className="text-base leading-4 text-brand-secondary group-hover:text-brand-base" className="text-base leading-4 text-custom-sidebar-text-200 group-hover:text-custom-sidebar-text-100"
/> />
</button> </button>
{children} {children}
@ -41,7 +41,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
<> <>
{link ? ( {link ? (
<Link href={link}> <Link href={link}>
<a className="border-r-2 border-brand-base px-3 text-sm"> <a className="border-r-2 border-custom-sidebar-border-200 px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}> <p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon ?? null} {icon ?? null}
{title} {title}

View File

@ -34,8 +34,8 @@ export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
{theme.label} {theme.label}
</div> </div>
</Command.Item> </Command.Item>

View File

@ -380,7 +380,6 @@ export const CommandPalette: React.FC = () => {
user={user} user={user}
/> />
)} )}
<CreateUpdateIssueModal <CreateUpdateIssueModal
isOpen={isIssueModalOpen} isOpen={isIssueModalOpen}
handleClose={() => setIsIssueModalOpen(false)} handleClose={() => setIsIssueModalOpen(false)}
@ -408,7 +407,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" /> <div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20"> <div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
@ -421,7 +420,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-custom-border-200 divide-opacity-10 rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
<Command <Command
filter={(value, search) => { filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1; if (value.toLowerCase().includes(search.toLowerCase())) return 1;
@ -444,7 +443,7 @@ export const CommandPalette: React.FC = () => {
> >
{issueId && issueDetails && ( {issueId && issueDetails && (
<div className="flex p-3"> <div className="flex p-3">
<p className="overflow-hidden truncate rounded-md bg-brand-surface-1 p-1 px-2 text-xs font-medium text-brand-secondary"> <p className="overflow-hidden truncate rounded-md bg-custom-background-90 p-1 px-2 text-xs font-medium text-custom-text-200">
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "} {issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails?.name} {issueDetails?.name}
</p> </p>
@ -452,11 +451,11 @@ export const CommandPalette: React.FC = () => {
)} )}
<div className="relative"> <div className="relative">
<MagnifyingGlassIcon <MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-secondary" className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-200"
aria-hidden="true" aria-hidden="true"
/> />
<Command.Input <Command.Input
className="w-full border-0 border-b border-brand-base bg-transparent p-4 pl-11 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm" className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
placeholder={placeholder} placeholder={placeholder}
value={searchTerm} value={searchTerm}
onValueChange={(e) => { onValueChange={(e) => {
@ -470,7 +469,7 @@ export const CommandPalette: React.FC = () => {
resultsCount === 0 && resultsCount === 0 &&
searchTerm !== "" && searchTerm !== "" &&
debouncedSearchTerm !== "" && ( debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-brand-secondary"> <div className="my-4 text-center text-custom-text-200">
No results found. No results found.
</div> </div>
)} )}
@ -533,9 +532,9 @@ export const CommandPalette: React.FC = () => {
value={value} value={value}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 overflow-hidden text-brand-secondary"> <div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
<Icon <Icon
className="h-4 w-4 text-brand-secondary" className="h-4 w-4 text-custom-text-200"
color="#6b7280" color="#6b7280"
/> />
<p className="block flex-1 truncate">{item.name}</p> <p className="block flex-1 truncate">{item.name}</p>
@ -562,8 +561,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" /> <Squares2X2Icon className="h-4 w-4 text-custom-text-200" />
Change state... Change state...
</div> </div>
</Command.Item> </Command.Item>
@ -575,8 +574,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<ChartBarIcon className="h-4 w-4 text-brand-secondary" /> <ChartBarIcon className="h-4 w-4 text-custom-text-200" />
Change priority... Change priority...
</div> </div>
</Command.Item> </Command.Item>
@ -588,8 +587,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<UsersIcon className="h-4 w-4 text-brand-secondary" /> <UsersIcon className="h-4 w-4 text-custom-text-200" />
Assign to... Assign to...
</div> </div>
</Command.Item> </Command.Item>
@ -600,15 +599,15 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
{issueDetails?.assignees.includes(user.id) ? ( {issueDetails?.assignees.includes(user.id) ? (
<> <>
<UserMinusIcon className="h-4 w-4 text-brand-secondary" /> <UserMinusIcon className="h-4 w-4 text-custom-text-200" />
Un-assign from me Un-assign from me
</> </>
) : ( ) : (
<> <>
<UserPlusIcon className="h-4 w-4 text-brand-secondary" /> <UserPlusIcon className="h-4 w-4 text-custom-text-200" />
Assign to me Assign to me
</> </>
)} )}
@ -616,8 +615,8 @@ export const CommandPalette: React.FC = () => {
</Command.Item> </Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none"> <Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<TrashIcon className="h-4 w-4 text-brand-secondary" /> <TrashIcon className="h-4 w-4 text-custom-text-200" />
Delete issue Delete issue
</div> </div>
</Command.Item> </Command.Item>
@ -628,8 +627,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<LinkIcon className="h-4 w-4 text-brand-secondary" /> <LinkIcon className="h-4 w-4 text-custom-text-200" />
Copy issue URL to clipboard Copy issue URL to clipboard
</div> </div>
</Command.Item> </Command.Item>
@ -638,9 +637,9 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="Issue"> <Command.Group heading="Issue">
<Command.Item <Command.Item
onSelect={createNewIssue} onSelect={createNewIssue}
className="focus:bg-brand-surface-2" className="focus:bg-custom-background-80"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" /> <LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
Create new issue Create new issue
</div> </div>
@ -654,7 +653,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewProject} onSelect={createNewProject}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" /> <AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
Create new project Create new project
</div> </div>
@ -670,7 +669,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewCycle} onSelect={createNewCycle}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<ContrastIcon className="h-4 w-4" color="#6b7280" /> <ContrastIcon className="h-4 w-4" color="#6b7280" />
Create new cycle Create new cycle
</div> </div>
@ -683,7 +682,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewModule} onSelect={createNewModule}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" /> <PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
Create new module Create new module
</div> </div>
@ -693,7 +692,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="View"> <Command.Group heading="View">
<Command.Item onSelect={createNewView} className="focus:outline-none"> <Command.Item onSelect={createNewView} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<ViewListIcon className="h-4 w-4" color="#6b7280" /> <ViewListIcon className="h-4 w-4" color="#6b7280" />
Create new view Create new view
</div> </div>
@ -703,7 +702,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="Page"> <Command.Group heading="Page">
<Command.Item onSelect={createNewPage} className="focus:outline-none"> <Command.Item onSelect={createNewPage} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<DocumentTextIcon className="h-4 w-4" color="#6b7280" /> <DocumentTextIcon className="h-4 w-4" color="#6b7280" />
Create new page Create new page
</div> </div>
@ -721,7 +720,7 @@ export const CommandPalette: React.FC = () => {
} }
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<InboxIcon className="h-4 w-4" color="#6b7280" /> <InboxIcon className="h-4 w-4" color="#6b7280" />
Open inbox Open inbox
</div> </div>
@ -740,7 +739,7 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4" color="#6b7280" /> <SettingIcon className="h-4 w-4" color="#6b7280" />
Search settings... Search settings...
</div> </div>
@ -751,8 +750,8 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewWorkspace} onSelect={createNewWorkspace}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<FolderPlusIcon className="h-4 w-4 text-brand-secondary" /> <FolderPlusIcon className="h-4 w-4 text-custom-text-200" />
Create new workspace Create new workspace
</div> </div>
</Command.Item> </Command.Item>
@ -764,8 +763,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Change interface theme... Change interface theme...
</div> </div>
</Command.Item> </Command.Item>
@ -781,8 +780,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<RocketLaunchIcon className="h-4 w-4 text-brand-secondary" /> <RocketLaunchIcon className="h-4 w-4 text-custom-text-200" />
Open keyboard shortcuts Open keyboard shortcuts
</div> </div>
</Command.Item> </Command.Item>
@ -793,8 +792,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<DocumentIcon className="h-4 w-4 text-brand-secondary" /> <DocumentIcon className="h-4 w-4 text-custom-text-200" />
Open Plane documentation Open Plane documentation
</div> </div>
</Command.Item> </Command.Item>
@ -805,7 +804,7 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<DiscordIcon className="h-4 w-4" color="#6b7280" /> <DiscordIcon className="h-4 w-4" color="#6b7280" />
Join our Discord Join our Discord
</div> </div>
@ -820,7 +819,7 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<GithubIcon className="h-4 w-4" color="#6b7280" /> <GithubIcon className="h-4 w-4" color="#6b7280" />
Report a bug Report a bug
</div> </div>
@ -832,8 +831,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" /> <ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-custom-text-200" />
Chat with us Chat with us
</div> </div>
</Command.Item> </Command.Item>
@ -847,8 +846,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => redirect(`/${workspaceSlug}/settings`)} onSelect={() => redirect(`/${workspaceSlug}/settings`)}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
General General
</div> </div>
</Command.Item> </Command.Item>
@ -856,8 +855,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)} onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Members Members
</div> </div>
</Command.Item> </Command.Item>
@ -865,8 +864,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)} onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Billing and Plans Billing and Plans
</div> </div>
</Command.Item> </Command.Item>
@ -874,8 +873,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)} onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Integrations Integrations
</div> </div>
</Command.Item> </Command.Item>
@ -883,8 +882,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)} onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-custom-text-200" />
Import/Export Import/Export
</div> </div>
</Command.Item> </Command.Item>

View File

@ -85,29 +85,29 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-80 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-brand-surface-2 p-5"> <div className="bg-custom-background-80 p-5">
<div className="sm:flex sm:items-start"> <div className="sm:flex sm:items-start">
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left"> <div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="flex justify-between text-lg font-medium leading-6 text-brand-base" className="flex justify-between text-lg font-medium leading-6 text-custom-text-100"
> >
<span>Keyboard Shortcuts</span> <span>Keyboard Shortcuts</span>
<span> <span>
<button type="button" onClick={() => setIsOpen(false)}> <button type="button" onClick={() => setIsOpen(false)}>
<XMarkIcon <XMarkIcon
className="h-6 w-6 text-gray-400 hover:text-brand-secondary" className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100"
aria-hidden="true" aria-hidden="true"
/> />
</button> </button>
</span> </span>
</Dialog.Title> </Dialog.Title>
<div> <div>
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-brand-base bg-brand-surface-1 px-3 py-2"> <div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-custom-border-200 bg-custom-background-90 px-3 py-2">
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-brand-secondary" /> <MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-200" />
<Input <Input
className="w-full border-none bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none" className="w-full border-none bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
id="search" id="search"
name="search" name="search"
type="text" type="text"
@ -123,22 +123,22 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div key={shortcut.keys} className="flex w-full flex-col"> <div key={shortcut.keys} className="flex w-full flex-col">
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-brand-secondary"> <p className="text-sm text-custom-text-200">
{shortcut.description} {shortcut.description}
</p> </p>
<div className="flex items-center gap-x-2.5"> <div className="flex items-center gap-x-2.5">
{shortcut.keys.split(",").map((key, index) => ( {shortcut.keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1"> <span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? ( {key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5"> <span className="flex h-full items-center rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" /> <CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
</span> </span>
) : key === "Ctrl" ? ( ) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary"> <kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5 text-sm font-medium text-custom-text-200">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" /> <CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
</kbd> </kbd>
) : ( ) : (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary"> <kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 px-2 py-1 text-sm font-medium text-custom-text-200">
{key} {key}
</kbd> </kbd>
)} )}
@ -151,7 +151,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
)) ))
) : ( ) : (
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
<p className="text-sm text-brand-secondary"> <p className="text-sm text-custom-text-200">
No shortcuts found for{" "} No shortcuts found for{" "}
<span className="font-semibold italic"> <span className="font-semibold italic">
{`"`} {`"`}
@ -168,20 +168,20 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
{shortcuts.map(({ keys, description }, index) => ( {shortcuts.map(({ keys, description }, index) => (
<div key={index} className="flex items-center justify-between"> <div key={index} className="flex items-center justify-between">
<p className="text-sm text-brand-secondary">{description}</p> <p className="text-sm text-custom-text-200">{description}</p>
<div className="flex items-center gap-x-2.5"> <div className="flex items-center gap-x-2.5">
{keys.split(",").map((key, index) => ( {keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1"> <span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? ( {key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-brand-secondary"> <span className="flex h-full items-center rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5 text-custom-text-200">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" /> <CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
</span> </span>
) : key === "Ctrl" ? ( ) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary"> <kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 p-1.5 text-sm font-medium text-custom-text-200">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" /> <CommandIcon className="h-4 w-4 fill-current text-custom-text-200" />
</kbd> </kbd>
) : ( ) : (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary"> <kbd className="rounded-sm border border-custom-border-200 bg-custom-background-90 px-2 py-1 text-sm font-medium text-custom-text-200">
{key} {key}
</kbd> </kbd>
)} )}

View File

@ -74,7 +74,7 @@ export const AllBoards: React.FC<Props> = ({
); );
})} })}
{!showEmptyGroups && ( {!showEmptyGroups && (
<div className="h-full w-96 flex-shrink-0 space-y-3 p-1"> <div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
<h2 className="text-lg font-semibold">Hidden groups</h2> <h2 className="text-lg font-semibold">Hidden groups</h2>
<div className="space-y-3"> <div className="space-y-3">
{Object.keys(groupedByIssues).map((singleGroup, index) => { {Object.keys(groupedByIssues).map((singleGroup, index) => {
@ -85,7 +85,7 @@ export const AllBoards: React.FC<Props> = ({
return ( return (
<div <div
key={index} key={index}
className="flex items-center justify-between gap-2 rounded bg-brand-surface-1 p-2 shadow" className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{currentState && {currentState &&
@ -96,7 +96,7 @@ export const AllBoards: React.FC<Props> = ({
: addSpaceIfCamelCase(singleGroup)} : addSpaceIfCamelCase(singleGroup)}
</h4> </h4>
</div> </div>
<span className="text-xs text-brand-secondary">0</span> <span className="text-xs text-custom-text-200">0</span>
</div> </div>
); );
})} })}

View File

@ -57,18 +57,6 @@ export const BoardHeader: React.FC<Props> = ({
: null : null
); );
let bgColor = "#000000";
if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000";
if (selectedGroup === "priority")
groupTitle === "high"
? (bgColor = "#dc2626")
: groupTitle === "medium"
? (bgColor = "#f97316")
: groupTitle === "low"
? (bgColor = "#22c55e")
: (bgColor = "#ff0000");
const getGroupTitle = () => { const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle); let title = addSpaceIfCamelCase(groupTitle);
@ -96,7 +84,8 @@ export const BoardHeader: React.FC<Props> = ({
switch (selectedGroup) { switch (selectedGroup) {
case "state": case "state":
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor); icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break; break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "text-lg"); icon = getPriorityIcon(groupTitle, "text-lg");
@ -124,18 +113,18 @@ export const BoardHeader: React.FC<Props> = ({
return ( return (
<div <div
className={`flex items-center justify-between px-1 ${ className={`flex items-center justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : "" !isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
}`} }`}
> >
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div <div
className={`flex cursor-pointer items-center gap-x-3 ${ className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : "" !isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`} }`}
> >
<span className="flex items-center">{getGroupIcon()}</span> <span className="flex items-center">{getGroupIcon()}</span>
<h2 <h2
className="text-lg font-semibold capitalize" className="text-lg font-semibold capitalize truncate"
style={{ style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb", writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}} }}
@ -145,7 +134,7 @@ export const BoardHeader: React.FC<Props> = ({
<span <span
className={`${ className={`${
isCollapsed ? "ml-0.5" : "" isCollapsed ? "ml-0.5" : ""
} min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs`} } min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`}
> >
{groupedByIssues?.[groupTitle].length ?? 0} {groupedByIssues?.[groupTitle].length ?? 0}
</span> </span>
@ -155,7 +144,7 @@ export const BoardHeader: React.FC<Props> = ({
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button <button
type="button" type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2" className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
onClick={() => { onClick={() => {
setIsCollapsed((prevData) => !prevData); setIsCollapsed((prevData) => !prevData);
}} }}
@ -169,7 +158,7 @@ export const BoardHeader: React.FC<Props> = ({
{!isCompleted && selectedGroup !== "created_by" && ( {!isCompleted && selectedGroup !== "created_by" && (
<button <button
type="button" type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2" className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />

View File

@ -60,6 +60,10 @@ export const SingleBoard: React.FC<Props> = ({
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
const issuesLength = groupedByIssues?.[groupTitle].length;
const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
return ( return (
@ -77,7 +81,9 @@ export const SingleBoard: React.FC<Props> = ({
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className={`relative h-full ${ className={`relative h-full ${
orderBy !== "sort_order" && snapshot.isDraggingOver ? "bg-brand-base/20" : "" orderBy !== "sort_order" && snapshot.isDraggingOver
? "bg-custom-background-100/20"
: ""
} ${!isCollapsed ? "hidden" : "flex flex-col"}`} } ${!isCollapsed ? "hidden" : "flex flex-col"}`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
@ -87,12 +93,12 @@ export const SingleBoard: React.FC<Props> = ({
<div <div
className={`absolute ${ className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden" snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-brand-surface-1 opacity-50`} } pointer-events-none top-0 left-0 z-[99] h-full w-full bg-custom-background-90 opacity-50`}
/> />
<div <div
className={`absolute ${ className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden" snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-brand-base p-2 text-xs`} } pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-custom-background-100 p-2 text-xs`}
> >
This board is ordered by{" "} This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase( {replaceUnderscoreIfSnakeCase(
@ -101,7 +107,11 @@ export const SingleBoard: React.FC<Props> = ({
</div> </div>
</> </>
)} )}
<div className="pt-3 overflow-hidden overflow-y-scroll"> <div
className={`pt-3 ${
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
} `}
>
{groupedByIssues?.[groupTitle].map((issue, index) => ( {groupedByIssues?.[groupTitle].map((issue, index) => (
<Draggable <Draggable
key={issue.id} key={issue.id}
@ -150,7 +160,7 @@ export const SingleBoard: React.FC<Props> = ({
{type === "issue" ? ( {type === "issue" ? (
<button <button
type="button" type="button"
className="flex items-center gap-2 font-medium text-brand-accent outline-none p-1" className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
@ -162,7 +172,7 @@ export const SingleBoard: React.FC<Props> = ({
customButton={ customButton={
<button <button
type="button" type="button"
className="flex items-center gap-2 font-medium text-brand-accent outline-none" className="flex items-center gap-2 font-medium text-custom-primary outline-none"
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue

View File

@ -43,7 +43,7 @@ import {
import { LayerDiagonalIcon } from "components/icons"; import { LayerDiagonalIcon } from "components/icons";
// helpers // helpers
import { handleIssuesMutation } from "constants/issue"; import { handleIssuesMutation } from "constants/issue";
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
import { import {
ICurrentUserResponse, ICurrentUserResponse,
@ -265,8 +265,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
</a> </a>
</ContextMenu> </ContextMenu>
<div <div
className={`mb-3 rounded bg-brand-base shadow ${ className={`mb-3 rounded bg-custom-background-90 shadow ${
snapshot.isDragging ? "border-2 border-brand-accent shadow-lg" : "" snapshot.isDragging ? "border-2 border-custom-primary shadow-lg" : ""
}`} }`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
@ -290,7 +290,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
<CustomMenu <CustomMenu
customButton={ customButton={
<button <button
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded p-1 text-left text-xs duration-300 hover:bg-brand-surface-2" className="flex w-full cursor-pointer items-center justify-between gap-1 rounded p-1 text-left text-xs duration-300 hover:bg-custom-background-80"
onClick={() => setIsMenuActive(!isMenuActive)} onClick={() => setIsMenuActive(!isMenuActive)}
> >
<EllipsisHorizontalIcon className="h-4 w-4" /> <EllipsisHorizontalIcon className="h-4 w-4" />
@ -330,13 +330,11 @@ export const SingleBoardIssue: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a> <a>
{properties.key && ( {properties.key && (
<div className="mb-2.5 text-xs font-medium text-brand-secondary"> <div className="mb-2.5 text-xs font-medium text-custom-text-200">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
)} )}
<h5 className="text-sm group-hover:text-brand-accent break-words line-clamp-3"> <h5 className="text-sm break-words line-clamp-3">{issue.name}</h5>
{issue.name}
</h5>
</a> </a>
</Link> </Link>
<div className="relative mt-2.5 flex flex-wrap items-center gap-2 text-xs"> <div className="relative mt-2.5 flex flex-wrap items-center gap-2 text-xs">
@ -358,7 +356,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned selfPositioned
/> />
)} )}
{properties.due_date && ( {properties.due_date && issue.target_date && (
<ViewDueDateSelect <ViewDueDateSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -366,7 +364,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.labels && ( {properties.labels && issue.labels.length > 0 && (
<ViewLabelSelect <ViewLabelSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -384,7 +382,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned selfPositioned
/> />
)} )}
{properties.estimate && ( {properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect <ViewEstimateSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -393,30 +391,30 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned selfPositioned
/> />
)} )}
{properties.sub_issue_count && ( {properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}> <Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" /> <LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count} {issue.sub_issues_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.link && ( {properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" /> <LinkIcon className="h-3.5 w-3.5" />
{issue.link_count} {issue.link_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.attachment_count && ( {properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" /> <PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count} {issue.attachment_count}
</div> </div>

View File

@ -62,7 +62,7 @@ export const CalendarHeader: React.FC<Props> = ({
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button> <Popover.Button>
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-brand-base"> <div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
<span>{formatDate(currentDate, "Month")}</span>{" "} <span>{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "yyyy")}</span> <span>{formatDate(currentDate, "yyyy")}</span>
</div> </div>
@ -77,30 +77,30 @@ export const CalendarHeader: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg"> <Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm"> <div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
{YEARS_LIST.map((year) => ( {YEARS_LIST.map((year) => (
<button <button
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))} onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
className={` ${ className={` ${
isSameYear(year.value, currentDate) isSameYear(year.value, currentDate)
? "text-sm font-medium text-brand-base" ? "text-sm font-medium text-custom-text-100"
: "text-xs text-brand-secondary " : "text-xs text-custom-text-200 "
} hover:text-sm hover:font-medium hover:text-brand-base`} } hover:text-sm hover:font-medium hover:text-custom-text-100`}
> >
{year.label} {year.label}
</button> </button>
))} ))}
</div> </div>
<div className="grid grid-cols-4 border-t border-brand-base px-2"> <div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
{MONTHS_LIST.map((month) => ( {MONTHS_LIST.map((month) => (
<button <button
onClick={() => onClick={() =>
updateDate(updateDateWithMonth(`${month.value}`, currentDate)) updateDate(updateDateWithMonth(`${month.value}`, currentDate))
} }
className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${ className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
isSameMonth(`${month.value}`, currentDate) isSameMonth(`${month.value}`, currentDate)
? "font-medium text-brand-base" ? "font-medium text-custom-text-100"
: "" : ""
}`} }`}
> >
@ -152,7 +152,7 @@ export const CalendarHeader: React.FC<Props> = ({
<div className="flex w-full items-center justify-end gap-2"> <div className="flex w-full items-center justify-end gap-2">
<button <button
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none" className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
onClick={() => { onClick={() => {
if (isMonthlyView) { if (isMonthlyView) {
updateDate(new Date()); updateDate(new Date());
@ -170,7 +170,7 @@ export const CalendarHeader: React.FC<Props> = ({
<CustomMenu <CustomMenu
customButton={ customButton={
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none "> <div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none ">
{isMonthlyView ? "Monthly" : "Weekly"} {isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" /> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div> </div>
@ -181,7 +181,7 @@ export const CalendarHeader: React.FC<Props> = ({
setIsMonthlyView(true); setIsMonthlyView(true);
changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate)); changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate));
}} }}
className="w-52 text-sm text-brand-secondary" className="w-52 text-sm text-custom-text-200"
> >
<div className="flex w-full max-w-[260px] items-center justify-between gap-2"> <div className="flex w-full max-w-[260px] items-center justify-between gap-2">
<span className="flex items-center gap-2">Monthly View</span> <span className="flex items-center gap-2">Monthly View</span>
@ -198,7 +198,7 @@ export const CalendarHeader: React.FC<Props> = ({
getCurrentWeekEndDate(currentDate) getCurrentWeekEndDate(currentDate)
); );
}} }}
className="w-52 text-sm text-brand-secondary" className="w-52 text-sm text-custom-text-200"
> >
<div className="flex w-full items-center justify-between gap-2"> <div className="flex w-full items-center justify-between gap-2">
<span className="flex items-center gap-2">Weekly View</span> <span className="flex items-center gap-2">Weekly View</span>
@ -207,7 +207,7 @@ export const CalendarHeader: React.FC<Props> = ({
/> />
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary"> <div className="mt-1 flex w-52 items-center justify-between border-t border-custom-border-200 py-2 px-1 text-sm text-custom-text-200">
<h4>Show weekends</h4> <h4>Show weekends</h4>
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} /> <ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
</div> </div>

View File

@ -170,9 +170,9 @@ export const CalendarView: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
return calendarIssues ? ( return calendarIssues ? (
<div className="h-full"> <div className="h-full overflow-y-auto">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<div className="h-full rounded-lg p-8 text-brand-secondary"> <div className="h-full rounded-lg p-8 text-custom-text-200">
<CalendarHeader <CalendarHeader
isMonthlyView={isMonthlyView} isMonthlyView={isMonthlyView}
setIsMonthlyView={setIsMonthlyView} setIsMonthlyView={setIsMonthlyView}
@ -191,7 +191,7 @@ export const CalendarView: React.FC<Props> = ({
{weeks.map((date, index) => ( {weeks.map((date, index) => (
<div <div
key={index} key={index}
className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-brand-secondary ${ className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200 ${
!isMonthlyView !isMonthlyView
? showWeekEnds ? showWeekEnds
? (index + 1) % 7 === 0 ? (index + 1) % 7 === 0

View File

@ -49,7 +49,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
key={index} key={index}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${ className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-200 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${
isMonthlyView ? "" : "pt-9" isMonthlyView ? "" : "pt-9"
} ${ } ${
showWeekEnds showWeekEnds
@ -83,7 +83,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
{totalIssues > 4 && ( {totalIssues > 4 && (
<button <button
type="button" type="button"
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs" className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 px-1.5 py-1 text-xs"
onClick={() => setShowAllIssues((prevData) => !prevData)} onClick={() => setShowAllIssues((prevData) => !prevData)}
> >
{showAllIssues ? "Hide" : totalIssues - 4 + " more"} {showAllIssues ? "Hide" : totalIssues - 4 + " more"}
@ -91,13 +91,13 @@ export const SingleCalendarDate: React.FC<Props> = ({
)} )}
<div <div
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`} className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-custom-background-80 p-1 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100`}
> >
<button <button
className="flex items-center justify-center gap-1 text-center" className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)} onClick={() => addIssueToDate(date.date)}
> >
<PlusSmallIcon className="h-4 w-4 text-brand-secondary" /> <PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
Add issue Add issue
</button> </button>
</div> </div>

View File

@ -163,8 +163,8 @@ export const SingleCalendarIssue: React.FC<Props> = ({
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
className={`w-full relative cursor-pointer rounded border border-brand-base px-1.5 py-1.5 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${ className={`w-full relative cursor-pointer rounded border border-custom-border-200 px-1.5 py-1.5 text-xs duration-300 hover:cursor-move hover:bg-custom-background-80 ${
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : "" snapshot.isDragging ? "bg-custom-background-80 shadow-lg" : ""
}`} }`}
> >
<div className="group/card flex w-full flex-col items-start justify-center gap-1.5 text-xs sm:w-auto "> <div className="group/card flex w-full flex-col items-start justify-center gap-1.5 text-xs sm:w-auto ">
@ -199,18 +199,18 @@ export const SingleCalendarIssue: React.FC<Props> = ({
tooltipHeading="Issue ID" tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
> >
<span className="flex-shrink-0 text-xs text-brand-secondary"> <span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span> </span>
</Tooltip> </Tooltip>
)} )}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-xs text-brand-base">{truncateText(issue.name, 25)}</span> <span className="text-xs text-custom-text-100">{truncateText(issue.name, 25)}</span>
</Tooltip> </Tooltip>
</a> </a>
</Link> </Link>
{displayProperties && ( {displayProperties && (
<div className="relative mt-1.5 flex flex-wrap items-center gap-2 text-xs"> <div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
{properties.priority && ( {properties.priority && (
<ViewPrioritySelect <ViewPrioritySelect
issue={issue} issue={issue}
@ -225,12 +225,13 @@ export const SingleCalendarIssue: React.FC<Props> = ({
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
position="left" position="left"
className="max-w-full"
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
user={user} user={user}
/> />
)} )}
{properties.due_date && ( {properties.due_date && issue.target_date && (
<ViewDueDateSelect <ViewDueDateSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -238,7 +239,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.labels && ( {properties.labels && issue.labels.length > 0 && (
<ViewLabelSelect <ViewLabelSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -256,7 +257,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.estimate && ( {properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect <ViewEstimateSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -265,30 +266,30 @@ export const SingleCalendarIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.sub_issue_count && ( {properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}> <Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" /> <LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count} {issue.sub_issues_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.link && ( {properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" /> <LinkIcon className="h-3.5 w-3.5" />
{issue.link_count} {issue.link_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.attachment_count && ( {properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" /> <PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count} {issue.attachment_count}
</div> </div>

View File

@ -1,198 +0,0 @@
import React, { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { useForm } from "react-hook-form";
// hooks
import useUser from "hooks/use-user";
// ui
import { PrimaryButton } from "components/ui";
import { ColorPickerInput } from "components/core";
// services
import userService from "services/user.service";
// helper
import { applyTheme } from "helpers/theme.helper";
// types
import { ICustomTheme } from "types";
type Props = {
preLoadedData?: Partial<ICustomTheme> | null;
};
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
const [darkPalette, setDarkPalette] = useState(false);
const defaultValues = {
accent: preLoadedData?.accent ?? "#FE5050",
bgBase: preLoadedData?.bgBase ?? "#FFF7F7",
bgSurface1: preLoadedData?.bgSurface1 ?? "#FFE0E0",
bgSurface2: preLoadedData?.bgSurface2 ?? "#FFF7F7",
border: preLoadedData?.border ?? "#FFC9C9",
darkPalette: preLoadedData?.darkPalette ?? false,
palette: preLoadedData?.palette ?? "",
sidebar: preLoadedData?.sidebar ?? "#FFFFFF",
textBase: preLoadedData?.textBase ?? "#430000",
textSecondary: preLoadedData?.textSecondary ?? "#323232",
};
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
watch,
setValue,
reset,
} = useForm<any>({
defaultValues,
});
const { setTheme } = useTheme();
const { mutateUser } = useUser();
const handleFormSubmit = async (formData: any) => {
await userService
.updateUser({
theme: {
accent: formData.accent,
bgBase: formData.bgBase,
bgSurface1: formData.bgSurface1,
bgSurface2: formData.bgSurface2,
border: formData.border,
darkPalette: darkPalette,
palette: `${formData.bgBase},${formData.bgSurface1},${formData.bgSurface2},${formData.border},${formData.sidebar},${formData.accent},${formData.textBase},${formData.textSecondary}`,
sidebar: formData.sidebar,
textBase: formData.textBase,
textSecondary: formData.textSecondary,
},
})
.then((res) => {
mutateUser((prevData) => {
if (!prevData) return prevData;
return { ...prevData, user: res };
}, false);
applyTheme(formData.palette, darkPalette);
setTheme("custom");
})
.catch((err) => console.log(err));
};
const handleUpdateTheme = async (formData: any) => {
await handleFormSubmit({ ...formData, darkPalette });
reset({
...defaultValues,
});
};
useEffect(() => {
reset({
...defaultValues,
...preLoadedData,
});
}, [preLoadedData, reset]);
return (
<form onSubmit={handleSubmit(handleUpdateTheme)}>
<div className="space-y-5">
<h3 className="text-lg font-semibold text-brand-base">Customize your theme</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Background</h3>
<ColorPickerInput
name="bgBase"
error={errors.bgBase}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Background surface 1</h3>
<ColorPickerInput
name="bgSurface1"
error={errors.bgSurface1}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Background surface 2</h3>
<ColorPickerInput
name="bgSurface2"
error={errors.bgSurface2}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Border</h3>
<ColorPickerInput
name="border"
error={errors.border}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Sidebar</h3>
<ColorPickerInput
name="sidebar"
error={errors.sidebar}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Accent</h3>
<ColorPickerInput
name="accent"
error={errors.accent}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Text primary</h3>
<ColorPickerInput
name="textBase"
error={errors.textBase}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-base text-brand-secondary">Text secondary</h3>
<ColorPickerInput
name="textSecondary"
error={errors.textSecondary}
watch={watch}
setValue={setValue}
register={register}
/>
</div>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Creating Theme..." : "Set Theme"}
</PrimaryButton>
</div>
</form>
);
};

View File

@ -5,21 +5,13 @@ import Link from "next/link";
// icons // icons
import { import {
ArrowTopRightOnSquareIcon, ArrowTopRightOnSquareIcon,
CalendarDaysIcon,
ChartBarIcon,
ChatBubbleBottomCenterTextIcon,
ChatBubbleLeftEllipsisIcon, ChatBubbleLeftEllipsisIcon,
LinkIcon,
PaperClipIcon,
PlayIcon,
RectangleGroupIcon,
Squares2X2Icon, Squares2X2Icon,
TrashIcon,
UserIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons"; import { BlockedIcon, BlockerIcon } from "components/icons";
import { Icon } from "components/ui";
// helpers // helpers
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper"; import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
import RemirrorRichTextEditor from "components/rich-text-editor"; import RemirrorRichTextEditor from "components/rich-text-editor";
@ -32,11 +24,11 @@ const activityDetails: {
} = { } = {
assignee: { assignee: {
message: "removed the assignee", message: "removed the assignee",
icon: <UserGroupIcon className="h-3 w-3" color="#6b7280" aria-hidden="true" />, icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
}, },
assignees: { assignees: {
message: "added a new assignee", message: "added a new assignee",
icon: <UserGroupIcon className="h-3 w-3" color="#6b7280" aria-hidden="true" />, icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
}, },
blocks: { blocks: {
message: "marked this issue being blocked by", message: "marked this issue being blocked by",
@ -48,62 +40,62 @@ const activityDetails: {
}, },
cycles: { cycles: {
message: "set the cycle to", message: "set the cycle to",
icon: <CyclesIcon height="12" width="12" color="#6b7280" />, icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
}, },
labels: { labels: {
icon: <TagIcon height="12" width="12" color="#6b7280" />, icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
}, },
modules: { modules: {
message: "set the module to", message: "set the module to",
icon: <RectangleGroupIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
}, },
state: { state: {
message: "set the state to", message: "set the state to",
icon: <Squares2X2Icon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: <Squares2X2Icon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
}, },
priority: { priority: {
message: "set the priority to", message: "set the priority to",
icon: <ChartBarIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
}, },
name: { name: {
message: "set the name to", message: "set the name to",
icon: ( icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
),
}, },
description: { description: {
message: "updated the description.", message: "updated the description.",
icon: ( icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
),
}, },
estimate_point: { estimate_point: {
message: "set the estimate point to", message: "set the estimate point to",
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-brand-secondary" aria-hidden="true" />, icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
}, },
target_date: { target_date: {
message: "set the due date to", message: "set the due date to",
icon: <CalendarDaysIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
}, },
parent: { parent: {
message: "set the parent to", message: "set the parent to",
icon: <UserIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
}, },
issue: { issue: {
message: "deleted the issue.", message: "deleted the issue.",
icon: <TrashIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: <Icon iconName="delete" className="!text-sm" aria-hidden="true" />,
}, },
estimate: { estimate: {
message: "updated the estimate", message: "updated the estimate",
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />, icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
}, },
link: { link: {
message: "updated the link", message: "updated the link",
icon: <LinkIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
}, },
attachment: { attachment: {
message: "updated the attachment", message: "updated the attachment",
icon: <PaperClipIcon className="h-3 w-3 text-gray-500 " aria-hidden="true" />, icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
},
archived_at: {
message: "archived",
icon: <Icon iconName="archive" className="!text-sm text-custom-text-200" aria-hidden="true" />,
}, },
}; };
@ -144,6 +136,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
action = `${activity.verb} the`; action = `${activity.verb} the`;
} else if (activity.field === "link") { } else if (activity.field === "link") {
action = `${activity.verb} the`; action = `${activity.verb} the`;
} else if (activity.field === "archived_at") {
action =
activity.new_value && activity.new_value === "restore"
? "restored the issue"
: "archived the issue";
} }
// for values that are after the action clause // for values that are after the action clause
let value: any = activity.new_value ? activity.new_value : activity.old_value; let value: any = activity.new_value ? activity.new_value : activity.old_value;
@ -157,7 +154,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
) { ) {
const { workspace_detail, project, issue } = activity; const { workspace_detail, project, issue } = activity;
value = ( value = (
<span className="text-brand-secondary"> <span className="text-custom-text-200">
created{" "} created{" "}
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}> <Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
<a className="inline-flex items-center hover:underline"> <a className="inline-flex items-center hover:underline">
@ -187,7 +184,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
activity.new_value && activity.new_value !== "" activity.new_value && activity.new_value !== ""
? activity.new_value ? activity.new_value
: activity.old_value; : activity.old_value;
value = renderShortNumericDateFormat(date as string); value = renderShortDateWithYearFormat(date as string);
} else if (activity.field === "description") { } else if (activity.field === "description") {
value = "description"; value = "description";
} else if (activity.field === "attachment") { } else if (activity.field === "attachment") {
@ -205,7 +202,13 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div key={activity.id} className="mt-2"> <div key={activity.id} className="mt-2">
<div className="relative flex items-start space-x-3"> <div className="relative flex items-start space-x-3">
<div className="relative px-1"> <div className="relative px-1">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( {activity.field ? (
activity.new_value === "restore" ? (
<Icon iconName="history" className="text-sm text-custom-text-200" />
) : (
activityDetails[activity.field as keyof typeof activityDetails]?.icon
)
) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
<img <img
src={activity.actor_detail.avatar} src={activity.actor_detail.avatar}
alt={activity.actor_detail.first_name} alt={activity.actor_detail.first_name}
@ -221,9 +224,9 @@ export const Feeds: React.FC<any> = ({ activities }) => (
</div> </div>
)} )}
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px"> <span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
<ChatBubbleLeftEllipsisIcon <ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-brand-secondary" className="h-3.5 w-3.5 text-custom-text-200"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@ -234,7 +237,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
{activity.actor_detail.first_name} {activity.actor_detail.first_name}
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name} {activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
</div> </div>
<p className="mt-0.5 text-xs text-brand-secondary"> <p className="mt-0.5 text-xs text-custom-text-200">
Commented {timeAgo(activity.created_at)} Commented {timeAgo(activity.created_at)}
</p> </p>
</div> </div>
@ -247,7 +250,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
} }
editable={false} editable={false}
noBorder noBorder
customClassName="text-xs border border-brand-base bg-brand-base" customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/> />
</div> </div>
</div> </div>
@ -262,7 +265,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div className="relative pb-1"> <div className="relative pb-1">
{activities.length > 1 && activityIdx !== activities.length - 1 ? ( {activities.length > 1 && activityIdx !== activities.length - 1 ? (
<span <span
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-brand-surface-2" className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
aria-hidden="true" aria-hidden="true"
/> />
) : null} ) : null}
@ -271,7 +274,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div> <div>
<div className="relative px-1.5"> <div className="relative px-1.5">
<div className="mt-1.5"> <div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-2 ring-white"> <div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
{activity.field ? ( {activity.field ? (
activityDetails[activity.field as keyof typeof activityDetails]?.icon activityDetails[activity.field as keyof typeof activityDetails]?.icon
) : activity.actor_detail.avatar && ) : activity.actor_detail.avatar &&
@ -295,15 +298,24 @@ export const Feeds: React.FC<any> = ({ activities }) => (
</div> </div>
</div> </div>
<div className="min-w-0 flex-1 py-3"> <div className="min-w-0 flex-1 py-3">
<div className="text-xs text-brand-secondary"> <div className="text-xs text-custom-text-200">
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span>
) : (
<span className="text-gray font-medium"> <span className="text-gray font-medium">
{activity.actor_detail.first_name} {activity.actor_detail.first_name}
{activity.actor_detail.is_bot {activity.actor_detail.is_bot
? " Bot" ? " Bot"
: " " + activity.actor_detail.last_name} : " " + activity.actor_detail.last_name}
</span> </span>
)}
<span> {action} </span> <span> {action} </span>
<span className="text-xs font-medium text-brand-base"> {value} </span> {activity.field !== "archived_at" && (
<span className="text-xs font-medium text-custom-text-100">
{" "}
{value}{" "}
</span>
)}
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span> <span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
</div> </div>
</div> </div>

View File

@ -0,0 +1,186 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// react-datepicker
import DatePicker from "react-datepicker";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useIssuesView from "hooks/use-issues-view";
// components
import { DueDateFilterSelect } from "./due-date-filter-select";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
// helpers
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
type Props = {
isOpen: boolean;
handleClose: () => void;
};
type TFormValues = {
filterType: "before" | "after" | "range";
date1: Date;
date2: Date;
};
const defaultValues: TFormValues = {
filterType: "range",
date1: new Date(),
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
};
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => {
const { filters, setFilters } = useIssuesView();
const router = useRouter();
const { viewId } = router.query;
const { handleSubmit, watch, control } = useForm<TFormValues>({
defaultValues,
});
const handleFormSubmit = (formData: TFormValues) => {
const { filterType, date1, date2 } = formData;
if (filterType === "range") {
setFilters(
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
!Boolean(viewId)
);
} else {
const filteredArray = filters?.target_date?.filter((item) => {
if (item?.includes(filterType)) return false;
return true;
});
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne)
setFilters(
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
!Boolean(viewId)
);
else
setFilters(
{
target_date: [`${renderDateFormat(date1)};${filterType}`],
},
!Boolean(viewId)
);
}
handleClose();
};
const isInvalid =
watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
const nextDay = new Date(watch("date1"));
nextDay.setDate(nextDay.getDate() + 1);
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 flex w-full justify-center overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form className="space-y-4" onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex w-full justify-between">
<Controller
control={control}
name="filterType"
render={({ field: { value, onChange } }) => (
<DueDateFilterSelect value={value} onChange={onChange} />
)}
/>
<XMarkIcon
className="border-base h-4 w-4 cursor-pointer"
onClick={handleClose}
/>
</div>
<div className="flex w-full justify-between gap-4">
<Controller
control={control}
name="date1"
render={({ field: { value, onChange } }) => (
<DatePicker
selected={value}
onChange={(val) => onChange(val)}
dateFormat="dd-MM-yyyy"
calendarClassName="h-full"
inline
/>
)}
/>
{watch("filterType") === "range" && (
<Controller
control={control}
name="date2"
render={({ field: { value, onChange } }) => (
<DatePicker
selected={value}
onChange={onChange}
dateFormat="dd-MM-yyyy"
calendarClassName="h-full"
minDate={nextDay}
inline
/>
)}
/>
)}
</div>
{watch("filterType") === "range" && (
<h6 className="text-xs flex items-center gap-1">
<span className="text-custom-text-200">After:</span>
<span>{renderShortDateWithYearFormat(watch("date1"))}</span>
<span className="text-custom-text-200 ml-1">Before:</span>
{!isInvalid && <span>{renderShortDateWithYearFormat(watch("date2"))}</span>}
</h6>
)}
<div className="flex justify-end gap-4">
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
Cancel
</SecondaryButton>
<PrimaryButton
type="submit"
className="flex items-center gap-2"
disabled={isInvalid}
>
Apply
</PrimaryButton>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -0,0 +1,58 @@
import React from "react";
// ui
import { CustomSelect } from "components/ui";
// icons
import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "components/icons";
// fetch-keys
type Props = {
value: string;
onChange: (value: string) => void;
};
type DueDate = {
name: string;
value: string;
icon: any;
};
const dueDateRange: DueDate[] = [
{
name: "Due date before",
value: "before",
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
},
{
name: "Due date after",
value: "after",
icon: <CalendarAfterIcon className="h-4 w-4 " />,
},
{
name: "Due date range",
value: "range",
icon: <CalendarMonthIcon className="h-4 w-4 " />,
},
];
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
<CustomSelect
value={value}
label={
<div className="flex items-center gap-2 text-xs">
{dueDateRange.find((item) => item.value === value)?.icon}
<span>{dueDateRange.find((item) => item.value === value)?.name}</span>
</div>
}
onChange={onChange}
>
{dueDateRange.map((option, index) => (
<CustomSelect.Option key={index} value={option.value}>
<>
<span>{option.icon}</span>
{option.name}
</>
</CustomSelect.Option>
))}
</CustomSelect>
);

View File

@ -17,6 +17,7 @@ import stateService from "services/state.service";
// types // types
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
import { IIssueFilterOptions } from "types"; import { IIssueFilterOptions } from "types";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
export const FilterList: React.FC<any> = ({ filters, setFilters }) => { export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
const router = useRouter(); const router = useRouter();
@ -57,10 +58,10 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return ( return (
<div <div
key={key} key={key}
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1" className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
> >
<span className="capitalize text-brand-secondary"> <span className="capitalize text-custom-text-200">
{replaceUnderscoreIfSnakeCase(key)}: {key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
</span> </span>
{filters[key as keyof IIssueFilterOptions] === null || {filters[key as keyof IIssueFilterOptions] === null ||
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? ( (filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
@ -131,7 +132,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
? "bg-yellow-500/20 text-yellow-500" ? "bg-yellow-500/20 text-yellow-500"
: priority === "low" : priority === "low"
? "bg-green-500/20 text-green-500" ? "bg-green-500/20 text-green-500"
: "bg-brand-surface-1 text-brand-secondary" : "bg-custom-background-90 text-custom-text-200"
}`} }`}
> >
<span>{getPriorityIcon(priority)}</span> <span>{getPriorityIcon(priority)}</span>
@ -170,7 +171,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return ( return (
<div <div
key={memberId} key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize" className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
> >
<Avatar user={member} /> <Avatar user={member} />
<span>{member?.first_name}</span> <span>{member?.first_name}</span>
@ -211,7 +212,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return ( return (
<div <div
key={`${memberId}-${key}`} key={`${memberId}-${key}`}
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize" className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
> >
<Avatar user={member} /> <Avatar user={member} />
<span>{member?.first_name}</span> <span>{member?.first_name}</span>
@ -299,6 +300,51 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
<XMarkIcon className="h-3 w-3" /> <XMarkIcon className="h-3 w-3" />
</button> </button>
</div> </div>
) : key === "target_date" ? (
<div className="flex flex-wrap items-center gap-1">
{filters.target_date?.map((date: string) => {
if (filters.target_date.length <= 0) return null;
const splitDate = date.split(";");
return (
<div
key={date}
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
>
<div className="h-1.5 w-1.5 rounded-full" />
<span className="capitalize">
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters(
{
target_date: filters.target_date?.filter(
(d: any) => d !== date
),
},
!Boolean(viewId)
)
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})}
<button
type="button"
onClick={() =>
setFilters({
target_date: null,
})
}
>
<XMarkIcon className="h-3 w-3" />
</button>
</div>
) : ( ) : (
(filters[key as keyof IIssueFilterOptions] as any)?.join(", ") (filters[key as keyof IIssueFilterOptions] as any)?.join(", ")
)} )}
@ -332,9 +378,10 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
assignees: null, assignees: null,
labels: null, labels: null,
created_by: null, created_by: null,
target_date: null,
}) })
} }
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs" className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
> >
<span>Clear all filters</span> <span>Clear all filters</span>
<XMarkIcon className="h-3 w-3" /> <XMarkIcon className="h-3 w-3" />

View File

@ -0,0 +1,4 @@
export * from "./due-date-filter-modal";
export * from "./due-date-filter-select";
export * from "./filters-list";
export * from "./issues-view-filter";

View File

@ -2,34 +2,60 @@ import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// hooks // hooks
import useIssuesProperties from "hooks/use-issue-properties"; import useIssuesProperties from "hooks/use-issue-properties";
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
// headless ui import useEstimateOption from "hooks/use-estimate-option";
import { Popover, Transition } from "@headlessui/react";
// components // components
import { SelectFilters } from "components/views"; import { SelectFilters } from "components/views";
// ui // ui
import { CustomMenu, Icon, ToggleSwitch } from "components/ui"; import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { import {
ChevronDownIcon, CalendarMonthOutlined,
ListBulletIcon, FormatListBulletedOutlined,
Squares2X2Icon, GridViewOutlined,
CalendarDaysIcon, TableChartOutlined,
ChartBarIcon, WaterfallChartOutlined,
} from "@heroicons/react/24/outline"; } from "@mui/icons-material";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types // types
import { Properties } from "types"; import { Properties, TIssueViewOptions } from "types";
// constants // constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
import useEstimateOption from "hooks/use-estimate-option";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
},
{
type: "kanban",
Icon: GridViewOutlined,
},
{
type: "calendar",
Icon: CalendarMonthOutlined,
},
{
type: "spreadsheet",
Icon: TableChartOutlined,
},
{
type: "gantt_chart",
Icon: WaterfallChartOutlined,
},
];
export const IssuesFilterView: React.FC = () => { export const IssuesFilterView: React.FC = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const { const {
issueView, issueView,
@ -55,76 +81,65 @@ export const IssuesFilterView: React.FC = () => {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!isArchivedIssues && (
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
{issueViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom"
>
<button <button
type="button" type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${ className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
issueView === "list" ? "bg-brand-surface-2" : "" issueView === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`} }`}
onClick={() => setIssueView("list")} onClick={() => setIssueView(option.type)}
> >
<ListBulletIcon className="h-4 w-4 text-brand-secondary" /> <option.Icon
</button> sx={{
<button fontSize: 16,
type="button" }}
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${ className={option.type === "gantt_chart" ? "rotate-90" : ""}
issueView === "kanban" ? "bg-brand-surface-2" : "" />
}`}
onClick={() => setIssueView("kanban")}
>
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
issueView === "calendar" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setIssueView("calendar")}
>
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
issueView === "spreadsheet" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setIssueView("spreadsheet")}
>
<Icon iconName="table_chart" className="text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-brand-surface-2 ${
issueView === "gantt_chart" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setIssueView("gantt_chart")}
>
<span className="material-symbols-rounded text-brand-secondary text-[18px] rotate-90">
waterfall_chart
</span>
</button> </button>
</Tooltip>
))}
</div> </div>
)}
<SelectFilters <SelectFilters
filters={filters} filters={filters}
onSelect={(option) => { onSelect={(option) => {
const key = option.key as keyof typeof filters; const key = option.key as keyof typeof filters;
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters.target_date ?? [],
option.value
);
setFilters({
target_date: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value); const valueExists = filters[key]?.includes(option.value);
if (valueExists) { if (valueExists)
setFilters( setFilters(
{ {
...(filters ?? {}),
[option.key]: ((filters[key] ?? []) as any[])?.filter( [option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value (val) => val !== option.value
), ),
}, },
!Boolean(viewId) !Boolean(viewId)
); );
} else { else
setFilters( setFilters(
{ {
...(filters ?? {}),
[option.key]: [...((filters[key] ?? []) as any[]), option.value], [option.key]: [...((filters[key] ?? []) as any[]), option.value],
}, },
!Boolean(viewId) !Boolean(viewId)
@ -138,8 +153,10 @@ export const IssuesFilterView: React.FC = () => {
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
className={`group flex items-center gap-2 rounded-md border border-brand-base bg-transparent px-3 py-1.5 text-xs hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none ${ className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary" open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
}`} }`}
> >
View View
@ -155,19 +172,18 @@ export const IssuesFilterView: React.FC = () => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg"> <Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-brand-base"> <div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs"> <div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && issueView !== "spreadsheet" && ( {issueView !== "calendar" && issueView !== "spreadsheet" && (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Group by</h4> <h4 className="text-custom-text-200">Group by</h4>
<CustomMenu <CustomMenu
label={ label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty) GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select" ?.name ?? "Select"
} }
width="lg"
> >
{GROUP_BY_OPTIONS.map((option) => {GROUP_BY_OPTIONS.map((option) =>
issueView === "kanban" && option.key === null ? null : ( issueView === "kanban" && option.key === null ? null : (
@ -182,13 +198,12 @@ export const IssuesFilterView: React.FC = () => {
</CustomMenu> </CustomMenu>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Order by</h4> <h4 className="text-custom-text-200">Order by</h4>
<CustomMenu <CustomMenu
label={ label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ?? ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select" "Select"
} }
width="lg"
> >
{ORDER_BY_OPTIONS.map((option) => {ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : ( groupByProperty === "priority" && option.key === "priority" ? null : (
@ -207,13 +222,12 @@ export const IssuesFilterView: React.FC = () => {
</> </>
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Issue type</h4> <h4 className="text-custom-text-200">Issue type</h4>
<CustomMenu <CustomMenu
label={ label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type) FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
?.name ?? "Select" ?.name ?? "Select"
} }
width="lg"
> >
{FILTER_ISSUE_OPTIONS.map((option) => ( {FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem <CustomMenu.MenuItem
@ -233,7 +247,7 @@ export const IssuesFilterView: React.FC = () => {
{issueView !== "calendar" && issueView !== "spreadsheet" && ( {issueView !== "calendar" && issueView !== "spreadsheet" && (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Show empty states</h4> <h4 className="text-custom-text-200">Show empty states</h4>
<ToggleSwitch <ToggleSwitch
value={showEmptyGroups} value={showEmptyGroups}
onChange={() => setShowEmptyGroups(!showEmptyGroups)} onChange={() => setShowEmptyGroups(!showEmptyGroups)}
@ -245,7 +259,7 @@ export const IssuesFilterView: React.FC = () => {
</button> </button>
<button <button
type="button" type="button"
className="font-medium text-brand-accent" className="font-medium text-custom-primary"
onClick={() => setNewFilterDefaultView()} onClick={() => setNewFilterDefaultView()}
> >
Set as default Set as default
@ -256,15 +270,22 @@ export const IssuesFilterView: React.FC = () => {
</div> </div>
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
<h4 className="text-sm text-brand-secondary">Display Properties</h4> <h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => { {Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null; if (key === "estimate" && !isEstimateActive) return null;
if ( if (
(issueView === "spreadsheet" && key === "sub_issue_count") || issueView === "spreadsheet" &&
key === "attachment_count" || (key === "attachment_count" ||
key === "link" key === "link" ||
key === "sub_issue_count")
)
return null;
if (
issueView !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
) )
return null; return null;
@ -274,8 +295,8 @@ export const IssuesFilterView: React.FC = () => {
type="button" type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${ className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties] properties[key as keyof Properties]
? "border-brand-accent bg-brand-accent text-white" ? "border-custom-primary bg-custom-primary text-white"
: "border-brand-base" : "border-custom-border-200"
}`} }`}
onClick={() => setProperties(key as keyof Properties)} onClick={() => setProperties(key as keyof Properties)}
> >

View File

@ -62,7 +62,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
return ( return (
<Popover className="relative z-[2]" ref={ref}> <Popover className="relative z-[2]" ref={ref}>
<Popover.Button <Popover.Button
className="rounded-md border border-brand-base bg-brand-surface-2 px-2 py-1 text-xs text-brand-secondary" className="rounded-md border border-custom-border-200 bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
> >
{label} {label}
@ -76,16 +76,16 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-brand-base bg-brand-surface-2 shadow-lg"> <Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
<div className="h-96 w-80 overflow-auto rounded border border-brand-base bg-brand-surface-2 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]"> <div className="h-96 w-80 overflow-auto rounded border border-custom-border-200 bg-custom-background-80 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
<Tab.Group> <Tab.Group>
<Tab.List as="span" className="inline-block rounded bg-brand-surface-2 p-1"> <Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
{tabOptions.map((tab) => ( {tabOptions.map((tab) => (
<Tab <Tab
key={tab.key} key={tab.key}
className={({ selected }) => className={({ selected }) =>
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${ `rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
selected ? "bg-brand-accent text-white" : "text-brand-base" selected ? "bg-custom-primary text-white" : "text-custom-text-100"
}` }`
} }
> >

View File

@ -1,19 +1,12 @@
export * from "./board-view"; export * from "./board-view";
export * from "./calendar-view"; export * from "./calendar-view";
export * from "./filters";
export * from "./gantt-chart-view"; export * from "./gantt-chart-view";
export * from "./list-view"; export * from "./list-view";
export * from "./modals";
export * from "./spreadsheet-view"; export * from "./spreadsheet-view";
export * from "./theme";
export * from "./sidebar"; export * from "./sidebar";
export * from "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal";
export * from "./filters-list";
export * from "./gpt-assistant-modal";
export * from "./image-upload-modal";
export * from "./issues-view-filter";
export * from "./issues-view"; export * from "./issues-view";
export * from "./link-modal";
export * from "./image-picker-popover"; export * from "./image-picker-popover";
export * from "./feeds"; export * from "./feeds";
export * from "./theme-switch";
export * from "./custom-theme-selector";
export * from "./color-picker-input";

View File

@ -29,19 +29,14 @@ import {
} from "components/core"; } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views"; import { CreateUpdateViewModal } from "components/views";
import { CycleIssuesGanttChartView, TransferIssues, TransferIssuesModal } from "components/cycles"; import { TransferIssues, TransferIssuesModal } from "components/cycles";
import { IssueGanttChartView } from "components/issues/gantt-chart";
// ui // ui
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui"; import { EmptyState, PrimaryButton, Spinner, Icon } from "components/ui";
// icons // icons
import { import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
ListBulletIcon,
PlusIcon,
RectangleStackIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
// images // images
import emptyIssue from "public/empty-state/empty-issue.svg"; import emptyIssue from "public/empty-state/issue.svg";
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
@ -56,7 +51,6 @@ import {
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST, STATES_LIST,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
import { ModuleIssuesGanttChartView } from "components/modules";
type Props = { type Props = {
type?: "issue" | "cycle" | "module"; type?: "issue" | "cycle" | "module";
@ -107,7 +101,7 @@ export const IssuesView: React.FC<Props> = ({
groupByProperty: selectedGroup, groupByProperty: selectedGroup,
orderBy, orderBy,
filters, filters,
isNotEmpty, isEmpty,
setFilters, setFilters,
params, params,
} = useIssuesView(); } = useIssuesView();
@ -495,7 +489,7 @@ export const IssuesView: React.FC<Props> = ({
{viewId ? "Update" : "Save"} view {viewId ? "Update" : "Save"} view
</PrimaryButton> </PrimaryButton>
</div> </div>
{<div className="mt-3 border-t border-brand-base" />} {<div className="mt-3 border-t border-custom-border-200" />}
</> </>
)} )}
@ -505,7 +499,7 @@ export const IssuesView: React.FC<Props> = ({
<div <div
className={`${ className={`${
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0" trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-brand-base px-3 py-5 text-xs font-medium italic text-red-500 ${ } fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : "" snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
} transition duration-300`} } transition duration-300`}
ref={provided.innerRef} ref={provided.innerRef}
@ -517,7 +511,7 @@ export const IssuesView: React.FC<Props> = ({
)} )}
</StrictModeDroppable> </StrictModeDroppable>
{groupedByIssues ? ( {groupedByIssues ? (
isNotEmpty ? ( !isEmpty || issueView === "kanban" || issueView === "calendar" ? (
<> <>
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />} {isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
{issueView === "list" ? ( {issueView === "list" ? (
@ -584,46 +578,36 @@ export const IssuesView: React.FC<Props> = ({
issueView === "gantt_chart" && <GanttChartView /> issueView === "gantt_chart" && <GanttChartView />
)} )}
</> </>
) : type === "issue" ? ( ) : router.pathname.includes("archived-issues") ? (
<EmptyState <EmptyState
type="issue" title="Archived Issues will be shown here"
title="Create New Issue" description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done." image={emptyIssueArchive}
imgURL={emptyIssue} buttonText="Go to Automation Settings"
onClick={() => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
}}
/> />
) : ( ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <EmptyState
<EmptySpace title={
title="You don't have any issue yet." cycleId
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done." ? "Cycle issues will appear here"
Icon={RectangleStackIcon} : moduleId
> ? "Module issues will appear here"
<EmptySpaceItem : "Project issues will appear here"
title="Create a new issue"
description={
<span>
Use <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>{" "}
shortcut to create a new issue
</span>
} }
Icon={PlusIcon} description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
action={() => { image={emptyIssue}
buttonText="New Issue"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", { const e = new KeyboardEvent("keydown", {
key: "c", key: "c",
}); });
document.dispatchEvent(e); document.dispatchEvent(e);
}} }}
/> />
{openIssuesListModal && (
<EmptySpaceItem
title="Add an existing issue"
description="Open list"
Icon={ListBulletIcon}
action={openIssuesListModal}
/>
)}
</EmptySpace>
</div>
) )
) : ( ) : (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">

View File

@ -38,7 +38,7 @@ export const AllLists: React.FC<Props> = ({
return ( return (
<> <>
{groupedByIssues && ( {groupedByIssues && (
<div> <div className="h-full overflow-y-auto">
{Object.keys(groupedByIssues).map((singleGroup) => { {Object.keys(groupedByIssues).map((singleGroup) => {
const currentState = const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;

View File

@ -84,6 +84,7 @@ export const SingleListIssue: React.FC<Props> = ({
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -181,7 +182,11 @@ export const SingleListIssue: React.FC<Props> = ({
}); });
}; };
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; const singleIssuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
: `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted || isArchivedIssues;
return ( return (
<> <>
@ -207,25 +212,21 @@ export const SingleListIssue: React.FC<Props> = ({
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}> <ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link Copy issue link
</ContextMenu.Item> </ContextMenu.Item>
<a <a href={singleIssuePath} target="_blank" rel="noreferrer noopener">
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
target="_blank"
rel="noreferrer noopener"
>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}> <ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab Open issue in new tab
</ContextMenu.Item> </ContextMenu.Item>
</a> </a>
</ContextMenu> </ContextMenu>
<div <div
className="flex flex-wrap items-center justify-between px-4 py-2.5 gap-2 border-b border-brand-base bg-brand-base last:border-b-0" className="flex flex-wrap items-center justify-between px-4 py-2.5 gap-2 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu(true); setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY }); setContextMenuPosition({ x: e.pageX, y: e.pageY });
}} }}
> >
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> <Link href={singleIssuePath}>
<div className="flex-grow cursor-pointer"> <div className="flex-grow cursor-pointer">
<a className="group relative flex items-center gap-2"> <a className="group relative flex items-center gap-2">
{properties.key && ( {properties.key && (
@ -233,13 +234,13 @@ export const SingleListIssue: React.FC<Props> = ({
tooltipHeading="Issue ID" tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
> >
<span className="flex-shrink-0 text-xs text-brand-secondary"> <span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span> </span>
</Tooltip> </Tooltip>
)} )}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-brand-base"> <span className="text-[0.825rem] text-custom-text-100">
{truncateText(issue.name, 50)} {truncateText(issue.name, 50)}
</span> </span>
</Tooltip> </Tooltip>
@ -247,7 +248,11 @@ export const SingleListIssue: React.FC<Props> = ({
</div> </div>
</Link> </Link>
<div className="flex w-full flex-shrink flex-wrap items-center gap-2 text-xs sm:w-auto"> <div
className={`flex w-full flex-shrink flex-wrap items-center gap-2 text-xs sm:w-auto ${
isArchivedIssues ? "opacity-60" : ""
}`}
>
{properties.priority && ( {properties.priority && (
<ViewPrioritySelect <ViewPrioritySelect
issue={issue} issue={issue}
@ -266,7 +271,7 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.due_date && ( {properties.due_date && issue.target_date && (
<ViewDueDateSelect <ViewDueDateSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -274,7 +279,7 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.labels && ( {properties.labels && issue.labels.length > 0 && (
<ViewLabelSelect <ViewLabelSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -292,7 +297,7 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.estimate && ( {properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect <ViewEstimateSelect
issue={issue} issue={issue}
partialUpdateIssue={partialUpdateIssue} partialUpdateIssue={partialUpdateIssue}
@ -301,30 +306,30 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.sub_issue_count && ( {properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}> <Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" /> <LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count} {issue.sub_issues_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.link && ( {properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" /> <LinkIcon className="h-3.5 w-3.5" />
{issue.link_count} {issue.link_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.attachment_count && ( {properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" /> <PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count} {issue.attachment_count}
</div> </div>

View File

@ -33,7 +33,6 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
type?: "issue" | "cycle" | "module"; type?: "issue" | "cycle" | "module";
currentState?: IState | null; currentState?: IState | null;
bgColor?: string;
groupTitle: string; groupTitle: string;
groupedByIssues: { groupedByIssues: {
[key: string]: IIssue[]; [key: string]: IIssue[];
@ -53,7 +52,6 @@ type Props = {
export const SingleList: React.FC<Props> = ({ export const SingleList: React.FC<Props> = ({
type, type,
currentState, currentState,
bgColor,
groupTitle, groupTitle,
groupedByIssues, groupedByIssues,
selectedGroup, selectedGroup,
@ -69,6 +67,7 @@ export const SingleList: React.FC<Props> = ({
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
@ -113,7 +112,8 @@ export const SingleList: React.FC<Props> = ({
switch (selectedGroup) { switch (selectedGroup) {
case "state": case "state":
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor); icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break; break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "text-lg"); icon = getPriorityIcon(groupTitle, "text-lg");
@ -142,28 +142,30 @@ export const SingleList: React.FC<Props> = ({
<Disclosure as="div" defaultOpen> <Disclosure as="div" defaultOpen>
{({ open }) => ( {({ open }) => (
<div> <div>
<div className="flex items-center justify-between px-4 py-2.5"> <div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
<Disclosure.Button> <Disclosure.Button>
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
{selectedGroup !== null && ( {selectedGroup !== null && (
<div className="flex items-center">{getGroupIcon()}</div> <div className="flex items-center">{getGroupIcon()}</div>
)} )}
{selectedGroup !== null ? ( {selectedGroup !== null ? (
<h2 className="text-sm font-semibold capitalize leading-6 text-brand-base"> <h2 className="text-sm font-semibold capitalize leading-6 text-custom-text-100">
{getGroupTitle()} {getGroupTitle()}
</h2> </h2>
) : ( ) : (
<h2 className="font-medium leading-5">All Issues</h2> <h2 className="font-medium leading-5">All Issues</h2>
)} )}
<span className="text-brand-2 min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs"> <span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
{groupedByIssues[groupTitle as keyof IIssue].length} {groupedByIssues[groupTitle as keyof IIssue].length}
</span> </span>
</div> </div>
</Disclosure.Button> </Disclosure.Button>
{type === "issue" ? ( {isArchivedIssues ? (
""
) : type === "issue" ? (
<button <button
type="button" type="button"
className="p-1 text-brand-secondary hover:bg-brand-surface-2" className="p-1 text-custom-text-200 hover:bg-custom-background-80"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
@ -222,7 +224,7 @@ export const SingleList: React.FC<Props> = ({
/> />
)) ))
) : ( ) : (
<p className="bg-brand-base px-4 py-2.5 text-sm text-brand-secondary"> <p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">
No issues. No issues.
</p> </p>
) )

View File

@ -173,7 +173,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
<form> <form>
<Combobox <Combobox
onChange={(val: string) => { onChange={(val: string) => {
@ -188,12 +188,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
> >
<div className="relative m-1"> <div className="relative m-1">
<MagnifyingGlassIcon <MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40" className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
aria-hidden="true" aria-hidden="true"
/> />
<input <input
type="text" type="text"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm" className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..." placeholder="Search..."
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
/> />
@ -201,16 +201,16 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
<Combobox.Options <Combobox.Options
static static
className="max-h-80 scroll-py-2 divide-y divide-brand-base overflow-y-auto" className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
> >
{filteredIssues.length > 0 ? ( {filteredIssues.length > 0 ? (
<li className="p-2"> <li className="p-2">
{query === "" && ( {query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base"> <h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100">
Select issues to delete Select issues to delete
</h2> </h2>
)} )}
<ul className="text-sm text-brand-secondary"> <ul className="text-sm text-custom-text-200">
{filteredIssues.map((issue) => ( {filteredIssues.map((issue) => (
<Combobox.Option <Combobox.Option
key={issue.id} key={issue.id}
@ -218,7 +218,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
value={issue.id} value={issue.id}
className={({ active, selected }) => className={({ active, selected }) =>
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${ `flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
active ? "bg-brand-surface-2 text-brand-base" : "" active ? "bg-custom-background-80 text-custom-text-100" : ""
}` }`
} }
> >
@ -246,9 +246,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
) : ( ) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center"> <div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" /> <LayerDiagonalIcon height="56" width="56" />
<h3 className="text-brand-secondary"> <h3 className="text-custom-text-200">
No issues found. Create a new issue with{" "} No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>. <pre className="inline rounded bg-custom-background-80 px-2 py-1">C</pre>.
</h3> </h3>
</div> </div>
)} )}

View File

@ -141,7 +141,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" /> <div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20"> <div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
@ -154,7 +154,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
<Combobox <Combobox
as="div" as="div"
onChange={(val: ISearchIssueResponse) => { onChange={(val: ISearchIssueResponse) => {
@ -165,24 +165,24 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
> >
<div className="relative m-1"> <div className="relative m-1">
<MagnifyingGlassIcon <MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40" className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
aria-hidden="true" aria-hidden="true"
/> />
<Combobox.Input <Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm" className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
placeholder="Type to search..." placeholder="Type to search..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
</div> </div>
<div className="text-brand-secondary text-[0.825rem] p-2"> <div className="text-custom-text-200 text-[0.825rem] p-2">
{selectedIssues.length > 0 ? ( {selectedIssues.length > 0 ? (
<div className="flex items-center gap-2 flex-wrap mt-1"> <div className="flex items-center gap-2 flex-wrap mt-1">
{selectedIssues.map((issue) => ( {selectedIssues.map((issue) => (
<div <div
key={issue.id} key={issue.id}
className="flex items-center gap-1 text-xs border border-brand-base bg-brand-surface-2 pl-2 py-1 rounded-md text-brand-base whitespace-nowrap" className="flex items-center gap-1 text-xs border border-custom-border-200 bg-custom-background-80 pl-2 py-1 rounded-md text-custom-text-100 whitespace-nowrap"
> >
{issue.project__identifier}-{issue.sequence_id} {issue.project__identifier}-{issue.sequence_id}
<button <button
@ -194,13 +194,13 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
) )
} }
> >
<XMarkIcon className="h-3 w-3 text-brand-secondary group-hover:text-brand-base" /> <XMarkIcon className="h-3 w-3 text-custom-text-200 group-hover:text-custom-text-100" />
</button> </button>
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<div className="w-min text-xs border border-brand-base bg-brand-surface-2 p-2 rounded-md whitespace-nowrap"> <div className="w-min text-xs border border-custom-border-200 bg-custom-background-80 p-2 rounded-md whitespace-nowrap">
No issues selected No issues selected
</div> </div>
)} )}
@ -208,9 +208,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto mt-2"> <Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto mt-2">
{debouncedSearchTerm !== "" && ( {debouncedSearchTerm !== "" && (
<h5 className="text-[0.825rem] text-brand-secondary mx-2"> <h5 className="text-[0.825rem] text-custom-text-200 mx-2">
Search results for{" "} Search results for{" "}
<span className="text-brand-base"> <span className="text-custom-text-100">
{'"'} {'"'}
{debouncedSearchTerm} {debouncedSearchTerm}
{'"'} {'"'}
@ -225,9 +225,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
debouncedSearchTerm !== "" && ( debouncedSearchTerm !== "" && (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center"> <div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="52" width="52" /> <LayerDiagonalIcon height="52" width="52" />
<h3 className="text-brand-secondary"> <h3 className="text-custom-text-200">
No issues found. Create a new issue with{" "} No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-brand-surface-2 px-2 py-1 text-sm"> <pre className="inline rounded bg-custom-background-80 px-2 py-1 text-sm">
C C
</pre> </pre>
. .
@ -243,7 +243,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
) : ( ) : (
<ul className={`text-sm text-brand-base ${issues.length > 0 ? "p-2" : ""}`}> <ul
className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}
>
{issues.map((issue) => { {issues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id); const selected = selectedIssues.some((i) => i.id === issue.id);
@ -254,9 +256,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
htmlFor={`issue-${issue.id}`} htmlFor={`issue-${issue.id}`}
value={issue} value={issue}
className={({ active }) => className={({ active }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${ `flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-brand-surface-2 text-brand-base" : "" active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-brand-base" : ""}` } ${selected ? "text-custom-text-100" : ""}`
} }
> >
<input type="checkbox" checked={selected} readOnly /> <input type="checkbox" checked={selected} readOnly />

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