Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11faf3f810 | ||
|
|
30ea1adf61 | ||
|
|
546aa40aa3 | ||
|
|
78669363b1 | ||
|
|
bca749986a | ||
|
|
229938114d | ||
|
|
c5e418ab47 | ||
|
|
ecdd1f1d03 | ||
|
|
e687cd6f6a | ||
|
|
9b6721790f | ||
|
|
05df65577a | ||
|
|
52d21b9dda | ||
|
|
51f10d5f36 | ||
|
|
9275e6f373 | ||
|
|
4aef8c2242 | ||
|
|
780573dadd | ||
|
|
5e625ab132 | ||
|
|
34123681cf | ||
|
|
c72ff782ac | ||
|
|
6eb72507a5 | ||
|
|
26b18b431b | ||
|
|
1bae9289f5 | ||
|
|
5c5bcb33e3 | ||
|
|
bed5f76082 | ||
|
|
124c2f772e | ||
|
|
b38898753f | ||
|
|
86a120f11e | ||
|
|
da603dc3f8 | ||
|
|
2f3970f641 | ||
|
|
d759438ebd | ||
|
|
0102f1d693 | ||
|
|
53e443d816 | ||
|
|
98b9957753 | ||
|
|
509af4662d | ||
|
|
d1f2a819f5 | ||
|
|
a42bff675b | ||
|
|
c71a2137e6 | ||
|
|
5e1f0a2604 | ||
|
|
ccab382d7d | ||
|
|
a7aa78a349 | ||
|
|
5ae963c451 | ||
|
|
07c097c9ad | ||
|
|
fc92d7d1a0 | ||
|
|
9ba8f5c21f | ||
|
|
059b8c793a | ||
|
|
93da220c4a | ||
|
|
0feab162ff | ||
|
|
55a1291b1d | ||
|
|
b12a00cf4a | ||
|
|
52ee8c5615 | ||
|
|
68108c9fe9 | ||
|
|
88fbe85087 | ||
|
|
9b423cea4b | ||
|
|
9d891ecce1 | ||
|
|
16a7bd3bda | ||
|
|
6e9f3971a5 | ||
|
|
dddfeb17b7 | ||
|
|
538d67dbd9 | ||
|
|
090870b03e | ||
|
|
0a56a30ab2 | ||
|
|
8df1648329 | ||
|
|
b69c4b6b30 | ||
|
|
e0181342c0 | ||
|
|
f9f8b5c3d9 | ||
|
|
5fadf53580 | ||
|
|
da6ecd439c | ||
|
|
7914bcf486 | ||
|
|
c9a5893c3f | ||
|
|
7361657660 | ||
|
|
60e96bcb72 | ||
|
|
3f3fb373cc | ||
|
|
a829e6fc40 | ||
|
|
864e592bc5 | ||
|
|
411a661abd | ||
|
|
61ad6b9e0e | ||
|
|
2ff49c93bd | ||
|
|
120d06983d | ||
|
|
c9cbca5ec8 | ||
|
|
275942a246 | ||
|
|
a1b09fcbc6 | ||
|
|
26f0e9da00 | ||
|
|
fd3f6ab7a4 | ||
|
|
d05d814efe | ||
|
|
6394f517a2 | ||
|
|
379d258375 | ||
|
|
6a27827c16 | ||
|
|
206ca98307 | ||
|
|
473c32d3d4 | ||
|
|
124d1c30aa | ||
|
|
fd7274ba1c | ||
|
|
46d93525be | ||
|
|
dbbce439ff | ||
|
|
4c0857233e |
@@ -19,14 +19,14 @@
|
||||
<p>
|
||||
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://app.plane.so/#gh-dark-mode-only" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -86,7 +86,7 @@ docker compose up -d
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -95,7 +95,7 @@ docker compose up -d
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -104,7 +104,7 @@ docker compose up -d
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -113,7 +113,7 @@ docker compose up -d
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -122,7 +122,7 @@ docker compose up -d
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -132,7 +132,7 @@ docker compose up -d
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<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"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
@@ -49,7 +49,7 @@ USER root
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
COPY ./bin ./bin/
|
||||
|
||||
RUN chmod +x ./bin/takeoff ./bin/worker
|
||||
RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
USER captain
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
python manage.py wait_for_db
|
||||
celery -A plane beat -l info
|
||||
@@ -6,4 +6,4 @@ python manage.py migrate
|
||||
# Create a Default User
|
||||
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 -
|
||||
|
||||
@@ -462,9 +462,9 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
# Issue Serializer with state details
|
||||
class IssueStateSerializer(BaseSerializer):
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
project_detail = ProjectSerializer(read_only=True, source="project")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
bridge_id = serializers.UUIDField(read_only=True)
|
||||
@@ -477,7 +477,7 @@ class IssueStateSerializer(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")
|
||||
parent_detail = IssueFlatSerializer(read_only=True, source="parent")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
|
||||
@@ -106,7 +106,7 @@ class ModuleFlatSerializer(BaseSerializer):
|
||||
|
||||
class ModuleIssueSerializer(BaseSerializer):
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
@@ -151,7 +151,7 @@ class ModuleLinkSerializer(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")
|
||||
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# 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
|
||||
|
||||
@@ -110,8 +110,8 @@ class ProjectMemberSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
project = ProjectSerializer(read_only=True)
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectMemberInvite
|
||||
@@ -125,7 +125,7 @@ class ProjectIdentifierSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class ProjectFavoriteSerializer(BaseSerializer):
|
||||
project_detail = ProjectSerializer(source="project", read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectFavorite
|
||||
@@ -136,11 +136,6 @@ class ProjectFavoriteSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ProjectLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ["id", "identifier", "name"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ProjectMemberLiteSerializer(BaseSerializer):
|
||||
|
||||
@@ -22,6 +22,7 @@ from plane.api.views import (
|
||||
# User
|
||||
UserEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
## End User
|
||||
# Workspaces
|
||||
@@ -152,6 +153,7 @@ from plane.api.views import (
|
||||
## End Analytics
|
||||
# Notification
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
## End Notification
|
||||
)
|
||||
|
||||
@@ -202,7 +204,12 @@ urlpatterns = [
|
||||
path(
|
||||
"users/me/onboard/",
|
||||
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"),
|
||||
# user workspaces
|
||||
@@ -472,7 +479,6 @@ urlpatterns = [
|
||||
"workspaces/<str:slug>/user-favorite-projects/",
|
||||
ProjectFavoritesViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
@@ -1377,5 +1383,10 @@ urlpatterns = [
|
||||
),
|
||||
name="notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/unread/",
|
||||
UnreadNotificationEndpoint.as_view(),
|
||||
name="unread-notifications",
|
||||
),
|
||||
## End Notification
|
||||
]
|
||||
|
||||
@@ -16,6 +16,7 @@ from .project import (
|
||||
from .people import (
|
||||
UserEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
)
|
||||
|
||||
@@ -144,4 +145,4 @@ from .analytic import (
|
||||
DefaultAnalyticsEndpoint,
|
||||
)
|
||||
|
||||
from .notification import NotificationViewSet
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint
|
||||
@@ -345,7 +345,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user_token = request.data.get("token", "").strip().lower()
|
||||
user_token = request.data.get("token", "").strip()
|
||||
key = request.data.get("key", False)
|
||||
|
||||
if not key or user_token == "":
|
||||
|
||||
@@ -706,9 +706,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class CycleFavoriteViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
serializer_class = CycleFavoriteSerializer
|
||||
model = CycleFavorite
|
||||
|
||||
@@ -30,31 +30,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
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)
|
||||
task = request.data.get("task", False)
|
||||
|
||||
@@ -82,7 +57,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
{
|
||||
"response": text,
|
||||
"response_html": text_html,
|
||||
"count": count,
|
||||
"project_detail": ProjectLiteSerializer(project).data,
|
||||
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
|
||||
},
|
||||
|
||||
@@ -45,7 +45,6 @@ from plane.api.serializers import (
|
||||
IssueLiteSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueSubscriberSerializer,
|
||||
ProjectMemberSerializer,
|
||||
ProjectMemberLiteSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
@@ -169,8 +168,8 @@ class IssueViewSet(BaseViewSet):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__id"))
|
||||
.annotate(module_id=F("issue_module__id"))
|
||||
.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()
|
||||
@@ -955,8 +954,8 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__id"))
|
||||
.annotate(module_id=F("issue_module__id"))
|
||||
.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()
|
||||
@@ -1087,7 +1086,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue.save()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_in": None}),
|
||||
requested_data=json.dumps({"archived_at": None}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
|
||||
@@ -480,9 +480,6 @@ class ModuleLinkViewSet(BaseViewSet):
|
||||
|
||||
|
||||
class ModuleFavoriteViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
serializer_class = ModuleFavoriteSerializer
|
||||
model = ModuleFavorite
|
||||
|
||||
@@ -8,7 +8,7 @@ from rest_framework.response import Response
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue
|
||||
from plane.api.serializers import NotificationSerializer
|
||||
|
||||
@@ -25,21 +25,21 @@ class NotificationViewSet(BaseViewSet):
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
receiver_id=self.request.user.id,
|
||||
)
|
||||
.select_related("workspace")
|
||||
.select_related("workspace", "project," "triggered_by", "receiver")
|
||||
)
|
||||
|
||||
def list(self, request, slug):
|
||||
try:
|
||||
order_by = request.GET.get("order_by", "-created_at")
|
||||
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(order_by)
|
||||
).order_by("snoozed_till", "-created_at")
|
||||
|
||||
# Filter for snoozed notifications
|
||||
if snoozed == "false":
|
||||
@@ -49,20 +49,23 @@ class NotificationViewSet(BaseViewSet):
|
||||
|
||||
if snoozed == "true":
|
||||
notifications = notifications.filter(
|
||||
snoozed_till__lt=timezone.now(),
|
||||
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 == "true":
|
||||
if archived == "false":
|
||||
notifications = notifications.filter(archived_at__isnull=True)
|
||||
|
||||
if archived == "false":
|
||||
if archived == "true":
|
||||
notifications = notifications.filter(archived_at__isnull=False)
|
||||
|
||||
# Subscribed issues
|
||||
if type == "watching":
|
||||
issue_ids = IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subsriber_id=request.user.id
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
@@ -117,7 +120,7 @@ class NotificationViewSet(BaseViewSet):
|
||||
{"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(
|
||||
@@ -160,7 +163,6 @@ class NotificationViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def archive(self, request, slug, pk):
|
||||
try:
|
||||
notification = Notification.objects.get(
|
||||
@@ -203,3 +205,54 @@ class NotificationViewSet(BaseViewSet):
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -37,7 +37,9 @@ class UserEndpoint(BaseViewSet):
|
||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).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["workspace"] = {
|
||||
@@ -47,7 +49,9 @@ class UserEndpoint(BaseViewSet):
|
||||
"fallback_workspace_slug": workspace.slug,
|
||||
"invites": workspace_invites,
|
||||
}
|
||||
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
|
||||
serialized_data.setdefault("issues", {})[
|
||||
"assigned_issues"
|
||||
] = assigned_issues
|
||||
|
||||
return Response(
|
||||
serialized_data,
|
||||
@@ -59,11 +63,15 @@ class UserEndpoint(BaseViewSet):
|
||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=request.user.email
|
||||
).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(
|
||||
workspace_member__member=request.user
|
||||
).order_by("created_at").first()
|
||||
fallback_workspace = (
|
||||
Workspace.objects.filter(workspace_member__member=request.user)
|
||||
.order_by("created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
serialized_data = UserSerializer(request.user).data
|
||||
|
||||
@@ -78,7 +86,9 @@ class UserEndpoint(BaseViewSet):
|
||||
else None,
|
||||
"invites": workspace_invites,
|
||||
}
|
||||
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
|
||||
serialized_data.setdefault("issues", {})[
|
||||
"assigned_issues"
|
||||
] = assigned_issues
|
||||
|
||||
return Response(
|
||||
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):
|
||||
def get(self, request):
|
||||
try:
|
||||
|
||||
@@ -96,6 +96,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
def list(self, request, slug):
|
||||
try:
|
||||
is_favorite = request.GET.get("is_favorite", "all")
|
||||
subquery = ProjectFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
@@ -126,6 +127,12 @@ class ProjectViewSet(BaseViewSet):
|
||||
.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)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
@@ -153,32 +160,32 @@ class ProjectViewSet(BaseViewSet):
|
||||
states = [
|
||||
{
|
||||
"name": "Backlog",
|
||||
"color": "#5e6ad2",
|
||||
"color": "#A3A3A3",
|
||||
"sequence": 15000,
|
||||
"group": "backlog",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"name": "Todo",
|
||||
"color": "#eb5757",
|
||||
"color": "#3A3A3A",
|
||||
"sequence": 25000,
|
||||
"group": "unstarted",
|
||||
},
|
||||
{
|
||||
"name": "In Progress",
|
||||
"color": "#26b5ce",
|
||||
"color": "#F59E0B",
|
||||
"sequence": 35000,
|
||||
"group": "started",
|
||||
},
|
||||
{
|
||||
"name": "Done",
|
||||
"color": "#f2c94c",
|
||||
"color": "#16A34A",
|
||||
"sequence": 45000,
|
||||
"group": "completed",
|
||||
},
|
||||
{
|
||||
"name": "Cancelled",
|
||||
"color": "#4cb782",
|
||||
"color": "#EF4444",
|
||||
"sequence": 55000,
|
||||
"group": "cancelled",
|
||||
},
|
||||
|
||||
@@ -101,6 +101,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
.filter(workspace_member__member=self.request.user)
|
||||
.annotate(total_members=member_count)
|
||||
.annotate(total_issues=issue_count)
|
||||
.select_related("owner")
|
||||
)
|
||||
|
||||
def create(self, request):
|
||||
|
||||
@@ -558,22 +558,62 @@ def track_estimate_points(
|
||||
)
|
||||
|
||||
|
||||
def track_archive_in(
|
||||
def track_archive_at(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
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="archvied_at",
|
||||
old_value="archive",
|
||||
new_value="restore",
|
||||
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(
|
||||
@@ -592,7 +632,8 @@ def update_issue_activity(
|
||||
"blocks_list": track_blocks,
|
||||
"blockers_list": track_blockings,
|
||||
"estimate_point": track_estimate_points,
|
||||
"archived_in": track_archive_in,
|
||||
"archived_at": track_archive_at,
|
||||
"closed_to": track_closed_to,
|
||||
}
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
@@ -970,11 +1011,16 @@ def delete_attachment_activity(
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
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:
|
||||
issue_activities = []
|
||||
@@ -982,16 +1028,26 @@ def issue_activity(
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if issue is not None:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save()
|
||||
|
||||
# add the user to issue subscriber
|
||||
try:
|
||||
_ = IssueSubscriber.objects.create(issue_id=issue_id, subscriber=actor)
|
||||
except Exception as e:
|
||||
pass
|
||||
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 = {
|
||||
"issue.activity.created": create_issue_activity,
|
||||
@@ -1043,55 +1099,80 @@ def issue_activity(
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
|
||||
# Create Notifications
|
||||
bulk_notifications = []
|
||||
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_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_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_subscribers = issue_subscribers + issue_assignees
|
||||
|
||||
if issue.created_by_id:
|
||||
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
|
||||
|
||||
issue = Issue.objects.get(project=project, pk=issue_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,
|
||||
# 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 ""
|
||||
),
|
||||
},
|
||||
},
|
||||
"issue_activity": str(issue_activity.id),
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Bulk create notifications
|
||||
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
|
||||
# Bulk create notifications
|
||||
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Python improts
|
||||
# Python imports
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
# Django imports
|
||||
@@ -11,7 +12,8 @@ from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue, Project, IssueActivity, State
|
||||
from plane.db.models import Issue, Project, State
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -19,6 +21,7 @@ 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
|
||||
@@ -46,6 +49,11 @@ def archive_old_issues():
|
||||
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
|
||||
@@ -56,24 +64,21 @@ def archive_old_issues():
|
||||
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)
|
||||
IssueActivity.objects.bulk_create(
|
||||
[
|
||||
IssueActivity(
|
||||
issue_id=issue.id,
|
||||
actor=project.created_by,
|
||||
verb="updated",
|
||||
field="archived_at",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="Plane archived the issue",
|
||||
new_value="archive",
|
||||
old_value=""
|
||||
)
|
||||
for issue in issues_to_update
|
||||
],
|
||||
batch_size=100,
|
||||
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:
|
||||
@@ -81,10 +86,13 @@ def archive_old_issues():
|
||||
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")
|
||||
projects = Project.objects.filter(close_in__gt=0).select_related(
|
||||
"default_state"
|
||||
)
|
||||
|
||||
for project in projects:
|
||||
project_id = project.id
|
||||
@@ -108,15 +116,19 @@ def close_old_issues():
|
||||
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 = project.default_state
|
||||
else:
|
||||
close_state = State.objects.filter(group="cancelled").first()
|
||||
|
||||
else:
|
||||
close_state = project.default_state
|
||||
|
||||
issues_to_update = []
|
||||
for issue in issues:
|
||||
@@ -125,24 +137,21 @@ def close_old_issues():
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
IssueActivity.objects.bulk_create(
|
||||
[
|
||||
IssueActivity(
|
||||
issue_id=issue.id,
|
||||
actor=project.created_by,
|
||||
verb="updated",
|
||||
field="state",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="Plane cancelled the issue",
|
||||
)
|
||||
for issue in issues_to_update
|
||||
],
|
||||
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
|
||||
return
|
||||
|
||||
@@ -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")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
]
|
||||
@@ -28,7 +28,6 @@ class IssueManager(models.Manager):
|
||||
| models.Q(issue_inbox__status=2)
|
||||
| models.Q(issue_inbox__isnull=True)
|
||||
)
|
||||
.filter(archived_at__isnull=True)
|
||||
.exclude(archived_at__isnull=False)
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ from sentry_sdk import capture_exception
|
||||
from slack_sdk import WebClient
|
||||
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):
|
||||
id = models.UUIDField(
|
||||
@@ -73,6 +80,8 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
role = models.CharField(max_length=300, null=True, blank=True)
|
||||
is_bot = models.BooleanField(default=False)
|
||||
theme = models.JSONField(default=dict)
|
||||
is_tour_completed = models.BooleanField(default=False)
|
||||
onboarding_step = models.JSONField(default=get_default_onboarding)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ.get("PGUSER", "plane"),
|
||||
"USER": "",
|
||||
"PASSWORD": "",
|
||||
|
||||
@@ -17,7 +17,7 @@ DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "plane",
|
||||
"USER": os.environ.get("PGUSER", ""),
|
||||
"PASSWORD": os.environ.get("PGPASSWORD", ""),
|
||||
|
||||
@@ -16,7 +16,7 @@ from .common import * # noqa
|
||||
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ.get("PGUSER", "plane"),
|
||||
"USER": "",
|
||||
"PASSWORD": "",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Django==4.2.3
|
||||
django-braces==1.15.0
|
||||
django-taggit==4.0.0
|
||||
psycopg2==2.9.6
|
||||
psycopg==3.1.9
|
||||
django-oauth-toolkit==2.3.0
|
||||
mistune==3.0.1
|
||||
djangorestframework==3.14.0
|
||||
@@ -30,3 +30,5 @@ openai==0.27.8
|
||||
slack-sdk==3.21.3
|
||||
celery==5.3.1
|
||||
django_celery_beat==2.5.0
|
||||
psycopg-binary==3.1.9
|
||||
psycopg-c==3.1.9
|
||||
@@ -7,5 +7,5 @@ django-storages==1.13.2
|
||||
boto3==1.27.0
|
||||
django-anymail==10.0
|
||||
django-debug-toolbar==4.1.0
|
||||
gevent==22.10.2
|
||||
gevent==23.7.0
|
||||
psycogreen==1.0.2
|
||||
@@ -32,6 +32,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
setError,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailCodeFormValues>({
|
||||
defaultValues: {
|
||||
@@ -112,43 +113,35 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className="space-y-5 py-5 px-5">
|
||||
{(codeSent || codeResent) && (
|
||||
<div className="rounded-md bg-green-500/20 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{(codeSent || codeResent) && (
|
||||
<p className="text-center mt-4">
|
||||
We have sent the sign in code.
|
||||
<br />
|
||||
Please check your inbox at <span className="font-medium">{watch("email")}</span>
|
||||
</p>
|
||||
)}
|
||||
<form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Email ID is required",
|
||||
required: "Email address is required",
|
||||
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(
|
||||
value
|
||||
) || "Email ID is not valid",
|
||||
) || "Email address is not valid",
|
||||
}}
|
||||
error={errors.email}
|
||||
placeholder="Enter your Email ID"
|
||||
placeholder="Enter your email address..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{codeSent && (
|
||||
<div>
|
||||
<>
|
||||
<Input
|
||||
id="token"
|
||||
type="token"
|
||||
@@ -158,14 +151,15 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
required: "Code is required",
|
||||
}}
|
||||
error={errors.token}
|
||||
placeholder="Enter code"
|
||||
placeholder="Enter code..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
<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
|
||||
? "cursor-default text-custom-text-200"
|
||||
: "cursor-pointer text-custom-primary"
|
||||
: "cursor-pointer text-custom-primary-100"
|
||||
} `}
|
||||
onClick={() => {
|
||||
setIsCodeResending(true);
|
||||
@@ -178,46 +172,43 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
disabled={isResendDisabled}
|
||||
>
|
||||
{resendCodeTimer > 0 ? (
|
||||
<p className="text-right">
|
||||
Didn{"'"}t receive code? Get new code in {resendCodeTimer} seconds.
|
||||
</p>
|
||||
<span className="text-right">Request new code in {resendCodeTimer} seconds</span>
|
||||
) : isCodeResending ? (
|
||||
"Sending code..."
|
||||
"Sending new code..."
|
||||
) : errorResendingCode ? (
|
||||
"Please try again later"
|
||||
) : (
|
||||
"Resend code"
|
||||
<span className="font-medium">Resend code</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{codeSent ? (
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center h-[46px]"
|
||||
size="md"
|
||||
onClick={handleSubmit(handleSignin)}
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing in..." : "Sign in"}
|
||||
</PrimaryButton>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
className="w-full text-center h-[46px]"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
handleSubmit(onSubmit)().then(() => {
|
||||
setResendCodeTimer(30);
|
||||
});
|
||||
}}
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Sending code..." : "Send sign in code"}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
<div>
|
||||
{codeSent ? (
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center"
|
||||
size="md"
|
||||
onClick={handleSubmit(handleSignin)}
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing in..." : "Sign in"}
|
||||
</PrimaryButton>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
className="w-full text-center"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
handleSubmit(onSubmit)().then(() => {
|
||||
setResendCodeTimer(30);
|
||||
});
|
||||
}}
|
||||
loading={isSubmitting || (!isValid && isDirty)}
|
||||
>
|
||||
{isSubmitting ? "Sending code..." : "Send code"}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useForm } from "react-hook-form";
|
||||
// components
|
||||
import { EmailResetPasswordForm } from "components/account";
|
||||
// ui
|
||||
import { Input, SecondaryButton } from "components/ui";
|
||||
import { Input, PrimaryButton } from "components/ui";
|
||||
// types
|
||||
type EmailPasswordFormValues = {
|
||||
email: string;
|
||||
@@ -42,28 +42,39 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
|
||||
|
||||
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 ? (
|
||||
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
|
||||
) : (
|
||||
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<form
|
||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Email ID is required",
|
||||
required: "Email address is required",
|
||||
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(
|
||||
value
|
||||
) || "Email ID is not valid",
|
||||
) || "Email address is not valid",
|
||||
}}
|
||||
error={errors.email}
|
||||
placeholder="Enter your email ID"
|
||||
placeholder="Enter your email address..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
@@ -73,46 +84,45 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
|
||||
required: "Password is required",
|
||||
}}
|
||||
error={errors.password}
|
||||
placeholder="Enter your password"
|
||||
placeholder="Enter your password..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="ml-auto text-sm">
|
||||
{isSignUpPage ? (
|
||||
<Link href="/">
|
||||
<a className="font-medium text-custom-primary hover:text-custom-primary">
|
||||
Already have an account? Sign in.
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsResettingPassword(true)}
|
||||
className="font-medium text-custom-primary hover:text-custom-primary"
|
||||
>
|
||||
Forgot your password?
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right text-xs">
|
||||
{isSignUpPage ? (
|
||||
<Link href="/">
|
||||
<a className="text-custom-text-200 hover:text-custom-primary-100">
|
||||
Already have an account? Sign in.
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsResettingPassword(true)}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
Forgot your password?
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<SecondaryButton
|
||||
<div>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center"
|
||||
className="w-full text-center h-[46px]"
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSignUpPage
|
||||
? isSubmitting
|
||||
? "Signing up..."
|
||||
: "Sign Up"
|
||||
: "Sign up"
|
||||
: isSubmitting
|
||||
? "Signing in..."
|
||||
: "Sign In"}
|
||||
</SecondaryButton>
|
||||
: "Sign in"}
|
||||
</PrimaryButton>
|
||||
{!isSignUpPage && (
|
||||
<Link href="/sign-up">
|
||||
<a className="block font-medium text-custom-primary hover:text-custom-primary 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.
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -59,32 +59,36 @@ export const EmailResetPasswordForm: React.FC<Props> = ({ setIsResettingPassword
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="mt-5 py-5 px-5" onSubmit={handleSubmit(forgotPassword)}>
|
||||
<div>
|
||||
<form
|
||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
||||
onSubmit={handleSubmit(forgotPassword)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Email ID is required",
|
||||
required: "Email address is required",
|
||||
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(
|
||||
value
|
||||
) || "Email ID is not valid",
|
||||
) || "Email address is not valid",
|
||||
}}
|
||||
error={errors.email}
|
||||
placeholder="Enter registered Email ID"
|
||||
placeholder="Enter registered email address.."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
</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
|
||||
className="w-full text-center"
|
||||
className="w-full text-center h-[46px]"
|
||||
onClick={() => setIsResettingPassword(false)}
|
||||
>
|
||||
Go Back
|
||||
</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"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useEffect, useState, FC } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// 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;
|
||||
|
||||
@@ -11,15 +16,15 @@ export interface GithubLoginButtonProps {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
}
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
const { handleSignIn } = props;
|
||||
// router
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => {
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||
const [gitCode, setGitCode] = useState<null | string>(null);
|
||||
|
||||
const {
|
||||
query: { code },
|
||||
} = useRouter();
|
||||
// states
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||
const [gitCode, setGitCode] = useState<null | string>(null);
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (code && !gitCode) {
|
||||
@@ -35,13 +40,18 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full flex justify-center items-center px-[3px]">
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Link
|
||||
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-custom-border-100 p-2 text-sm font-medium text-custom-text-200 duration-300 hover:bg-custom-background-80">
|
||||
<Image src={githubImage} height={20} width={20} color="#000" alt="GitHub Logo" />
|
||||
<span>Sign In with Github</span>
|
||||
<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={theme === "dark" ? githubWhiteImage : githubBlackImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
<span>Sign in with GitHub</span>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
|
||||
// next
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
export interface IGoogleLoginButton {
|
||||
@@ -8,18 +8,18 @@ export interface IGoogleLoginButton {
|
||||
styles?: CSSProperties;
|
||||
}
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
const { handleSignIn } = props;
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
|
||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
||||
|
||||
const loadScript = useCallback(() => {
|
||||
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
||||
|
||||
window?.google?.accounts.id.initialize({
|
||||
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
||||
callback: handleSignIn,
|
||||
});
|
||||
|
||||
window?.google?.accounts.id.renderButton(
|
||||
googleSignInButton.current,
|
||||
{
|
||||
@@ -27,11 +27,13 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
theme: "outline",
|
||||
size: "large",
|
||||
logo_alignment: "center",
|
||||
width: "410",
|
||||
text: "continue_with",
|
||||
width: "360",
|
||||
text: "signin_with",
|
||||
} as GsiButtonConfiguration // customization attributes
|
||||
);
|
||||
|
||||
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
||||
|
||||
setGsiScriptLoaded(true);
|
||||
}, [handleSignIn, gsiScriptLoaded]);
|
||||
|
||||
@@ -48,7 +50,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
<>
|
||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||
<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"
|
||||
ref={googleSignInButton}
|
||||
/>
|
||||
|
||||
@@ -111,7 +111,7 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
|
||||
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 border border-custom-border-100 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">
|
||||
<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)}>
|
||||
<div>
|
||||
<Dialog.Title
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-100 bg-custom-background-80 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
|
||||
className="h-3 w-3 rounded"
|
||||
style={{
|
||||
|
||||
@@ -185,7 +185,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
<div
|
||||
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
|
||||
fullScreen
|
||||
? "border-l border-custom-border-100 md:h-full md:border-l md:border-custom-border-100 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"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -37,9 +37,9 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
<div className="flow-root">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<table className="min-w-full divide-y divide-custom-border-100 whitespace-nowrap border-y border-custom-border-100">
|
||||
<table className="min-w-full divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
|
||||
<thead className="bg-custom-background-80">
|
||||
<tr className="divide-x divide-custom-border-100 text-sm text-custom-text-100">
|
||||
<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">
|
||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
||||
</th>
|
||||
@@ -80,11 +80,11 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-custom-border-100">
|
||||
<tbody className="divide-y divide-custom-border-200">
|
||||
{barGraphData.data.map((item, index) => (
|
||||
<tr
|
||||
key={`table-row-${index}`}
|
||||
className="divide-x divide-custom-border-100 text-xs text-custom-text-200"
|
||||
className="divide-x divide-custom-border-200 text-xs text-custom-text-200"
|
||||
>
|
||||
<td
|
||||
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
|
||||
|
||||
@@ -155,7 +155,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-full flex-col overflow-hidden border-custom-border-100 bg-custom-background-100 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"
|
||||
}`}
|
||||
>
|
||||
@@ -186,12 +186,12 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
</div>
|
||||
</div>
|
||||
<Tab.Group as={Fragment}>
|
||||
<Tab.List as="div" className="space-x-2 border-b border-custom-border-100 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) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-custom-border-100 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected ? "bg-custom-background-80" : ""
|
||||
}`
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<div className="space-y-3 rounded-[10px] border border-custom-border-100 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>
|
||||
<div>
|
||||
<h4 className="text-custom-text-100 text-base font-medium">Total open tasks</h4>
|
||||
@@ -50,7 +50,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-custom-border-100 bg-custom-background-80 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-custom-text-200">
|
||||
<PlayIcon className="h-4 w-4 -rotate-90" aria-hidden="true" />
|
||||
<span>Estimate Demand:</span>
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||
<div className="p-3 border border-custom-border-100 rounded-[10px]">
|
||||
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
||||
<h6 className="text-base font-medium">{title}</h6>
|
||||
{users.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
|
||||
@@ -8,9 +8,9 @@ type Props = {
|
||||
};
|
||||
|
||||
export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<div className="rounded-[10px] border border-custom-border-100">
|
||||
<div className="rounded-[10px] border border-custom-border-200">
|
||||
<h5 className="p-3 text-xs text-green-500">SCOPE</h5>
|
||||
<div className="divide-y divide-custom-border-100">
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
<div>
|
||||
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
||||
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||
@@ -27,7 +27,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-custom-border-100 bg-custom-background-80 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-custom-text-200">
|
||||
{assignee
|
||||
? assignee.assignees__first_name + " " + assignee.assignees__last_name
|
||||
|
||||
@@ -9,54 +9,46 @@ type Props = {
|
||||
defaultAnalytics: IDefaultAnalyticsResponse;
|
||||
};
|
||||
|
||||
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
|
||||
const currentMonth = new Date().getMonth();
|
||||
const startMonth = Math.floor(currentMonth / 3) * 3 + 1;
|
||||
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2];
|
||||
|
||||
return (
|
||||
<div className="py-3 border border-custom-border-100 rounded-[10px]">
|
||||
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
|
||||
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
|
||||
<LineGraph
|
||||
data={[
|
||||
{
|
||||
id: "issues_closed",
|
||||
color: "rgb(var(--color-primary-100))",
|
||||
data: MONTHS_LIST.map((month) => ({
|
||||
x: month.label.substring(0, 3),
|
||||
y:
|
||||
defaultAnalytics.issue_completed_month_wise.find(
|
||||
(data) => data.month === month.value
|
||||
)?.count || 0,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => {
|
||||
if (quarterMonthsList.includes(data.month)) return data.count;
|
||||
|
||||
return 0;
|
||||
})}
|
||||
height="300px"
|
||||
colors={(datum) => datum.color}
|
||||
curve="monotoneX"
|
||||
margin={{ top: 20 }}
|
||||
enableSlices="x"
|
||||
sliceTooltip={(datum) => (
|
||||
<div className="rounded-md border border-custom-border-100 bg-custom-background-80 p-2 text-xs">
|
||||
{datum.slice.points[0].data.yFormatted}
|
||||
<span className="text-custom-text-200"> issues closed in </span>
|
||||
{datum.slice.points[0].data.xFormatted}
|
||||
</div>
|
||||
)}
|
||||
theme={{
|
||||
background: "rgb(var(--color-background-100))",
|
||||
}}
|
||||
enableArea
|
||||
/>
|
||||
) : (
|
||||
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<div className="py-3 border border-custom-border-200 rounded-[10px]">
|
||||
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
|
||||
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
|
||||
<LineGraph
|
||||
data={[
|
||||
{
|
||||
id: "issues_closed",
|
||||
color: "rgb(var(--color-primary-100))",
|
||||
data: MONTHS_LIST.map((month) => ({
|
||||
x: month.label.substring(0, 3),
|
||||
y:
|
||||
defaultAnalytics.issue_completed_month_wise.find(
|
||||
(data) => data.month === month.value
|
||||
)?.count || 0,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
|
||||
(data) => data.count
|
||||
)}
|
||||
height="300px"
|
||||
colors={(datum) => datum.color}
|
||||
curve="monotoneX"
|
||||
margin={{ top: 20 }}
|
||||
enableSlices="x"
|
||||
sliceTooltip={(datum) => (
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
{datum.slice.points[0].data.yFormatted}
|
||||
<span className="text-custom-text-200"> issues closed in </span>
|
||||
{datum.slice.points[0].data.xFormatted}
|
||||
</div>
|
||||
)}
|
||||
theme={{
|
||||
background: "rgb(var(--color-background-100))",
|
||||
}}
|
||||
enableArea
|
||||
/>
|
||||
) : (
|
||||
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-90 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">
|
||||
<Image
|
||||
src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const JoinProject: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-90 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">
|
||||
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./auto-close-automation";
|
||||
export * from "./auto-archive-automation";
|
||||
export * from "./select-month-modal";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-custom-sidebar-border-100 text-center text-sm hover:bg-custom-sidebar-background-90"
|
||||
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()}
|
||||
>
|
||||
<Icon
|
||||
@@ -41,7 +41,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
|
||||
<>
|
||||
{link ? (
|
||||
<Link href={link}>
|
||||
<a className="border-r-2 border-custom-sidebar-border-100 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" : ""}`}>
|
||||
{icon ?? null}
|
||||
{title}
|
||||
|
||||
@@ -380,7 +380,6 @@ export const CommandPalette: React.FC = () => {
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isIssueModalOpen}
|
||||
handleClose={() => setIsIssueModalOpen(false)}
|
||||
@@ -421,7 +420,7 @@ export const CommandPalette: React.FC = () => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-custom-border-100 divide-opacity-10 rounded-xl border border-custom-border-100 bg-custom-background-100 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
|
||||
filter={(value, search) => {
|
||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||
@@ -456,7 +455,7 @@ export const CommandPalette: React.FC = () => {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Command.Input
|
||||
className="w-full border-0 border-b border-custom-border-100 bg-transparent p-4 pl-11 text-custom-text-100 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}
|
||||
value={searchTerm}
|
||||
onValueChange={(e) => {
|
||||
|
||||
@@ -104,7 +104,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
</span>
|
||||
</Dialog.Title>
|
||||
<div>
|
||||
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-custom-border-100 bg-custom-background-90 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-custom-text-200" />
|
||||
<Input
|
||||
className="w-full border-none bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
|
||||
@@ -130,15 +130,15 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
{shortcut.keys.split(",").map((key, index) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
{key === "Ctrl" ? (
|
||||
<span className="flex h-full items-center rounded-sm border border-custom-border-100 bg-custom-background-90 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-custom-text-200" />
|
||||
</span>
|
||||
) : key === "Ctrl" ? (
|
||||
<kbd className="rounded-sm border border-custom-border-100 bg-custom-background-90 p-1.5 text-sm font-medium text-custom-text-200">
|
||||
<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-custom-text-200" />
|
||||
</kbd>
|
||||
) : (
|
||||
<kbd className="rounded-sm border border-custom-border-100 bg-custom-background-90 px-2 py-1 text-sm font-medium text-custom-text-200">
|
||||
<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}
|
||||
</kbd>
|
||||
)}
|
||||
@@ -173,15 +173,15 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
{keys.split(",").map((key, index) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
{key === "Ctrl" ? (
|
||||
<span className="flex h-full items-center rounded-sm border border-custom-border-100 bg-custom-background-90 p-1.5 text-custom-text-200">
|
||||
<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-custom-text-200" />
|
||||
</span>
|
||||
) : key === "Ctrl" ? (
|
||||
<kbd className="rounded-sm border border-custom-border-100 bg-custom-background-90 p-1.5 text-sm font-medium text-custom-text-200">
|
||||
<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-custom-text-200" />
|
||||
</kbd>
|
||||
) : (
|
||||
<kbd className="rounded-sm border border-custom-border-100 bg-custom-background-90 px-2 py-1 text-sm font-medium text-custom-text-200">
|
||||
<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}
|
||||
</kbd>
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
);
|
||||
})}
|
||||
{!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>
|
||||
<div className="space-y-3">
|
||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||
|
||||
@@ -60,6 +60,10 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -103,7 +107,11 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
</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) => (
|
||||
<Draggable
|
||||
key={issue.id}
|
||||
|
||||
@@ -356,7 +356,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
{properties.due_date && (
|
||||
{properties.due_date && issue.target_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -364,7 +364,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.labels && (
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -382,7 +382,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && (
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -391,8 +391,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
||||
@@ -401,8 +401,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.link && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.link && issue.link_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
@@ -411,8 +411,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.attachment_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.attachment_count && issue.attachment_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
||||
|
||||
@@ -92,7 +92,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 border-t border-custom-border-100 px-2">
|
||||
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
||||
{MONTHS_LIST.map((month) => (
|
||||
<button
|
||||
onClick={() =>
|
||||
@@ -152,7 +152,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
||||
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<button
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-100 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 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={() => {
|
||||
if (isMonthlyView) {
|
||||
updateDate(new Date());
|
||||
@@ -170,7 +170,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-100 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 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"}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
@@ -207,7 +207,7 @@ export const CalendarHeader: React.FC<Props> = ({
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-custom-border-100 py-2 px-1 text-sm text-custom-text-200">
|
||||
<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>
|
||||
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
||||
</div>
|
||||
|
||||
@@ -191,7 +191,7 @@ export const CalendarView: React.FC<Props> = ({
|
||||
{weeks.map((date, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center justify-start gap-2 border-custom-border-100 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200 ${
|
||||
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
|
||||
? showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
|
||||
@@ -49,7 +49,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
key={index}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-100 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${
|
||||
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"
|
||||
} ${
|
||||
showWeekEnds
|
||||
@@ -83,7 +83,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
{totalIssues > 4 && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded-md border border-custom-border-100 bg-custom-background-80 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)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
|
||||
@@ -163,7 +163,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={`w-full relative cursor-pointer rounded border border-custom-border-100 px-1.5 py-1.5 text-xs duration-300 hover:cursor-move hover:bg-custom-background-80 ${
|
||||
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-custom-background-80 shadow-lg" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -231,7 +231,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{properties.due_date && (
|
||||
{properties.due_date && issue.target_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -239,7 +239,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.labels && (
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -257,7 +257,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && (
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -266,8 +266,8 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
||||
@@ -276,8 +276,8 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.link && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.link && issue.link_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
@@ -286,8 +286,8 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.attachment_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.attachment_count && issue.attachment_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
||||
|
||||
@@ -5,19 +5,11 @@ import Link from "next/link";
|
||||
// icons
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CalendarDaysIcon,
|
||||
ChartBarIcon,
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
LinkIcon,
|
||||
PaperClipIcon,
|
||||
PlayIcon,
|
||||
RectangleGroupIcon,
|
||||
Squares2X2Icon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
} 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
|
||||
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
@@ -32,11 +24,11 @@ const activityDetails: {
|
||||
} = {
|
||||
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: {
|
||||
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: {
|
||||
message: "marked this issue being blocked by",
|
||||
@@ -48,14 +40,14 @@ const activityDetails: {
|
||||
},
|
||||
cycles: {
|
||||
message: "set the cycle to",
|
||||
icon: <CyclesIcon height="12" width="12" color="#6b7280" />,
|
||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
icon: <TagIcon height="12" width="12" color="#6b7280" />,
|
||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: "set the module to",
|
||||
icon: <RectangleGroupIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: "set the state to",
|
||||
@@ -63,47 +55,47 @@ const activityDetails: {
|
||||
},
|
||||
priority: {
|
||||
message: "set the priority to",
|
||||
icon: <ChartBarIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: "set the name to",
|
||||
icon: (
|
||||
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />
|
||||
),
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: "updated the description.",
|
||||
icon: (
|
||||
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />
|
||||
),
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: "set the estimate point to",
|
||||
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: "set the due date to",
|
||||
icon: <CalendarDaysIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: "set the parent to",
|
||||
icon: <UserIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: "deleted the issue.",
|
||||
icon: <TrashIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="delete" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate: {
|
||||
message: "updated the estimate",
|
||||
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: "updated the link",
|
||||
icon: <LinkIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: "updated the attachment",
|
||||
icon: <PaperClipIcon className="h-3 w-3 text-custom-text-200 " 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`;
|
||||
} else if (activity.field === "link") {
|
||||
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
|
||||
let value: any = activity.new_value ? activity.new_value : activity.old_value;
|
||||
@@ -205,7 +202,13 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div key={activity.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<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
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
@@ -247,7 +250,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
}
|
||||
editable={false}
|
||||
noBorder
|
||||
customClassName="text-xs border border-custom-border-100 bg-custom-background-100"
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,7 +274,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 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 ? (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
) : activity.actor_detail.avatar &&
|
||||
@@ -296,14 +299,23 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="text-xs text-custom-text-200">
|
||||
<span className="text-gray font-medium">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activity.actor_detail.last_name}
|
||||
</span>
|
||||
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : (
|
||||
<span className="text-gray font-medium">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activity.actor_detail.last_name}
|
||||
</span>
|
||||
)}
|
||||
<span> {action} </span>
|
||||
<span className="text-xs font-medium text-custom-text-100"> {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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
||||
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-100 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">
|
||||
<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
|
||||
|
||||
@@ -58,7 +58,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-x-2 rounded-full border border-custom-border-100 bg-custom-background-80 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-custom-text-200">
|
||||
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||
@@ -310,7 +310,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
return (
|
||||
<div
|
||||
key={date}
|
||||
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-100 bg-custom-background-100 px-1 py-0.5"
|
||||
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">
|
||||
@@ -381,7 +381,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
target_date: null,
|
||||
})
|
||||
}
|
||||
className="flex items-center gap-x-1 rounded-full border border-custom-border-100 bg-custom-background-80 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>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
|
||||
@@ -11,14 +11,16 @@ import useEstimateOption from "hooks/use-estimate-option";
|
||||
// components
|
||||
import { SelectFilters } from "components/views";
|
||||
// ui
|
||||
import { CustomMenu, Icon, ToggleSwitch } from "components/ui";
|
||||
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ListBulletIcon,
|
||||
Squares2X2Icon,
|
||||
CalendarDaysIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
CalendarMonthOutlined,
|
||||
FormatListBulletedOutlined,
|
||||
GridViewOutlined,
|
||||
TableChartOutlined,
|
||||
WaterfallChartOutlined,
|
||||
} from "@mui/icons-material";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
@@ -27,32 +29,33 @@ import { Properties, TIssueViewOptions } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
|
||||
const issueViewOptions: { type: TIssueViewOptions; icon: any }[] = [
|
||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
icon: <ListBulletIcon className="h-4 w-4" />,
|
||||
Icon: FormatListBulletedOutlined,
|
||||
},
|
||||
{
|
||||
type: "kanban",
|
||||
icon: <Squares2X2Icon className="h-4 w-4" />,
|
||||
Icon: GridViewOutlined,
|
||||
},
|
||||
{
|
||||
type: "calendar",
|
||||
icon: <CalendarDaysIcon className="h-4 w-4" />,
|
||||
Icon: CalendarMonthOutlined,
|
||||
},
|
||||
{
|
||||
type: "spreadsheet",
|
||||
icon: <Icon iconName="table_chart" />,
|
||||
Icon: TableChartOutlined,
|
||||
},
|
||||
{
|
||||
type: "gantt_chart",
|
||||
icon: <Icon iconName="waterfall_chart" className="rotate-90" />,
|
||||
Icon: WaterfallChartOutlined,
|
||||
},
|
||||
];
|
||||
|
||||
export const IssuesFilterView: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const {
|
||||
issueView,
|
||||
@@ -78,22 +81,36 @@ export const IssuesFilterView: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewOptions.map((option) => (
|
||||
<button
|
||||
key={option.type}
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
>
|
||||
{option.icon}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!isArchivedIssues && (
|
||||
<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
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
@@ -136,7 +153,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-100 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 ${
|
||||
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-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||
: "text-custom-sidebar-text-200"
|
||||
@@ -155,8 +172,8 @@ export const IssuesFilterView: React.FC = () => {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
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-custom-border-100 bg-custom-background-90 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-custom-border-100">
|
||||
<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-custom-border-200">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
@@ -167,7 +184,6 @@ export const IssuesFilterView: React.FC = () => {
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) =>
|
||||
issueView === "kanban" && option.key === null ? null : (
|
||||
@@ -188,7 +204,6 @@ export const IssuesFilterView: React.FC = () => {
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
@@ -213,7 +228,6 @@ export const IssuesFilterView: React.FC = () => {
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
@@ -282,7 +296,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-custom-primary bg-custom-primary text-white"
|
||||
: "border-custom-border-100"
|
||||
: "border-custom-border-200"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
|
||||
@@ -62,7 +62,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
return (
|
||||
<Popover className="relative z-[2]" ref={ref}>
|
||||
<Popover.Button
|
||||
className="rounded-md border border-custom-border-100 bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200"
|
||||
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)}
|
||||
>
|
||||
{label}
|
||||
@@ -76,8 +76,8 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-100 bg-custom-background-80 shadow-lg">
|
||||
<div className="h-96 w-80 overflow-auto rounded border border-custom-border-100 bg-custom-background-80 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||
<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-custom-border-200 bg-custom-background-80 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||
<Tab.Group>
|
||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||
{tabOptions.map((tab) => (
|
||||
|
||||
@@ -31,11 +31,12 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||
// ui
|
||||
import { EmptyState, PrimaryButton, Spinner } from "components/ui";
|
||||
import { EmptyState, PrimaryButton, Spinner, Icon } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// images
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
@@ -488,7 +489,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
{viewId ? "Update" : "Save"} view
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
{<div className="mt-3 border-t border-custom-border-100" />}
|
||||
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -577,9 +578,25 @@ export const IssuesView: React.FC<Props> = ({
|
||||
issueView === "gantt_chart" && <GanttChartView />
|
||||
)}
|
||||
</>
|
||||
) : router.pathname.includes("archived-issues") ? (
|
||||
<EmptyState
|
||||
title="Archived Issues will be shown here"
|
||||
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
|
||||
image={emptyIssueArchive}
|
||||
buttonText="Go to Automation Settings"
|
||||
onClick={() => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="Project issues will appear here"
|
||||
title={
|
||||
cycleId
|
||||
? "Cycle issues will appear here"
|
||||
: moduleId
|
||||
? "Module issues will appear here"
|
||||
: "Project issues will appear 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={emptyIssue}
|
||||
buttonText="New Issue"
|
||||
|
||||
@@ -84,6 +84,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -207,25 +212,21 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<a href={singleIssuePath} target="_blank" rel="noreferrer noopener">
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
</a>
|
||||
</ContextMenu>
|
||||
<div
|
||||
className="flex flex-wrap items-center justify-between px-4 py-2.5 gap-2 border-b border-custom-border-100 bg-custom-background-100 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) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
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">
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
@@ -247,7 +248,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</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 && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
@@ -266,7 +271,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.due_date && (
|
||||
{properties.due_date && issue.target_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -274,7 +279,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.labels && (
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -292,7 +297,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && (
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -301,8 +306,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LayerDiagonalIcon className="h-3.5 w-3.5" />
|
||||
@@ -311,8 +316,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.link && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.link && issue.link_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
@@ -321,8 +326,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.attachment_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-custom-border-100 px-2.5 py-1 text-xs shadow-sm">
|
||||
{properties.attachment_count && issue.attachment_count > 0 && (
|
||||
<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}`}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
|
||||
|
||||
@@ -67,6 +67,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
@@ -159,7 +160,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
</span>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
{type === "issue" ? (
|
||||
{isArchivedIssues ? (
|
||||
""
|
||||
) : type === "issue" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
|
||||
@@ -173,7 +173,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-100 bg-custom-background-100 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>
|
||||
<Combobox
|
||||
onChange={(val: string) => {
|
||||
@@ -201,7 +201,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-custom-border-100 overflow-y-auto"
|
||||
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
|
||||
@@ -154,7 +154,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-100 bg-custom-background-100 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
|
||||
as="div"
|
||||
onChange={(val: ISearchIssueResponse) => {
|
||||
@@ -182,7 +182,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
{selectedIssues.map((issue) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="flex items-center gap-1 text-xs border border-custom-border-100 bg-custom-background-80 pl-2 py-1 rounded-md text-custom-text-100 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}
|
||||
<button
|
||||
@@ -200,7 +200,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-min text-xs border border-custom-border-100 bg-custom-background-80 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
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -143,7 +143,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-100 bg-custom-background-100 p-4 shadow ${
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -118,7 +118,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
||||
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 overflow-hidden rounded-lg border border-custom-border-100 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
@@ -132,7 +132,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
||||
{...getRootProps()}
|
||||
className={`relative grid h-80 w-full cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-custom-primary focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-custom-border-100 hover:bg-custom-background-90"
|
||||
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
@@ -70,7 +70,7 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
||||
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 overflow-hidden rounded-lg bg-custom-background-80 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-200 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<div className="space-y-5">
|
||||
|
||||
@@ -49,7 +49,7 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
|
||||
)}
|
||||
<Link href={link.url}>
|
||||
<a
|
||||
className="relative flex gap-2 rounded-md bg-custom-background-100 p-2"
|
||||
className="relative flex gap-2 rounded-md bg-custom-background-90 p-2"
|
||||
target="_blank"
|
||||
>
|
||||
<div className="mt-0.5">
|
||||
|
||||
@@ -119,7 +119,7 @@ const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, tota
|
||||
gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))}
|
||||
enableSlices="x"
|
||||
sliceTooltip={(datum) => (
|
||||
<div className="rounded-md border border-custom-border-100 bg-custom-background-80 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}
|
||||
<span className="text-custom-text-200"> issues pending on </span>
|
||||
{datum.slice.points[0].data.xFormatted}
|
||||
|
||||
@@ -87,7 +87,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-custom-border-100" : "rounded"
|
||||
roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded"
|
||||
} px-3 py-1 text-custom-text-100 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
}`
|
||||
@@ -98,7 +98,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-custom-border-100" : "rounded"
|
||||
roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded"
|
||||
} px-3 py-1 text-custom-text-100 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
}`
|
||||
@@ -109,7 +109,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-custom-border-100" : "rounded"
|
||||
roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded"
|
||||
} px-3 py-1 text-custom-text-100 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
}`
|
||||
@@ -159,7 +159,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-100 bg-custom-background-80">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img
|
||||
src="/user.png"
|
||||
height="100%"
|
||||
|
||||
@@ -179,10 +179,10 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-100 w-full min-w-max"
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-100 w-full">
|
||||
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full">
|
||||
<div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
|
||||
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100 w-14">
|
||||
{properties.key && (
|
||||
@@ -198,7 +198,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||
content={
|
||||
<div
|
||||
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-100 bg-custom-background-90`}
|
||||
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -270,7 +270,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</Link>
|
||||
</div>
|
||||
{properties.state && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -284,7 +284,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{properties.priority && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -297,7 +297,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -310,7 +310,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -324,7 +324,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
{properties.due_date && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -336,7 +336,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{properties.estimate && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
@@ -348,12 +348,12 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{properties.created_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.created_at)}
|
||||
</div>
|
||||
)}
|
||||
{properties.updated_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.updated_at)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -56,7 +56,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
className="!w-full"
|
||||
customButton={
|
||||
<div
|
||||
className={`relative group flex items-center justify-start gap-1.5 cursor-pointer text-sm text-custom-text-200 text-current hover:text-custom-text-100 w-full py-3 px-2 ${
|
||||
className={`relative group flex items-center justify-start gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
|
||||
activeSortingProperty === col.propertyName ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -90,16 +90,9 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
}
|
||||
menuItemsWhiteBg
|
||||
width="xl"
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
className={`${
|
||||
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
||||
? "bg-custom-background-80"
|
||||
: ""
|
||||
}`}
|
||||
key={col.propertyName}
|
||||
onClick={() => {
|
||||
handleOrderBy(col.ascendingOrder, col.propertyName);
|
||||
}}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||
<div className="sticky z-[2] top-0 border-b border-custom-border-100 bg-custom-background-90 w-full min-w-max">
|
||||
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
||||
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
||||
</div>
|
||||
{spreadsheetIssues ? (
|
||||
@@ -84,12 +84,12 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-100 w-full min-w-max"
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-100 w-full"
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
@@ -104,7 +104,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
className="sticky left-0 z-[1]"
|
||||
customButton={
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-100 w-full"
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
@@ -112,7 +112,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
menuItemsClassName="left-5 !w-36"
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
|
||||
@@ -81,7 +81,7 @@ export const ColorPickerInput: React.FC<Props> = ({ name, watch, setValue, error
|
||||
>
|
||||
{watch(name) && watch(name) !== "" ? (
|
||||
<span
|
||||
className="h-4 w-4 rounded border border-custom-border-100"
|
||||
className="h-4 w-4 rounded border border-custom-border-200"
|
||||
style={{
|
||||
backgroundColor: `${watch(name)}`,
|
||||
}}
|
||||
|
||||
@@ -21,11 +21,11 @@ type Props = {
|
||||
};
|
||||
|
||||
const defaultValues: ICustomTheme = {
|
||||
background: "#fff7f7",
|
||||
text: "#ffc9c9",
|
||||
primary: "#fe5050",
|
||||
sidebarBackground: "#ffffff",
|
||||
sidebarText: "#000000",
|
||||
background: "#0d101b",
|
||||
text: "#c5c5c5",
|
||||
primary: "#3f76ff",
|
||||
sidebarBackground: "#0d101b",
|
||||
sidebarText: "#c5c5c5",
|
||||
darkPalette: false,
|
||||
palette: "",
|
||||
};
|
||||
@@ -69,8 +69,8 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||
return { ...prevData, ...res };
|
||||
}, false);
|
||||
|
||||
applyTheme(payload.palette, darkPalette);
|
||||
setTheme("custom");
|
||||
applyTheme(payload.palette, darkPalette);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
@@ -72,8 +72,21 @@ export const ThemeSwitch: React.FC<Props> = ({
|
||||
onChange={({ value, type }: { value: string; type: string }) => {
|
||||
if (value === "custom") {
|
||||
if (user?.theme.palette) {
|
||||
setPreLoadedData(user.theme);
|
||||
setPreLoadedData({
|
||||
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
|
||||
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
|
||||
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
|
||||
sidebarBackground:
|
||||
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
|
||||
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
|
||||
darkPalette: false,
|
||||
palette:
|
||||
user.theme.palette !== ",,,,"
|
||||
? user.theme.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
});
|
||||
}
|
||||
|
||||
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
|
||||
} else {
|
||||
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
|
||||
@@ -81,11 +94,14 @@ export const ThemeSwitch: React.FC<Props> = ({
|
||||
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
|
||||
document.documentElement.style.removeProperty(`--color-background-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-text-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-border-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-primary-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-background-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-text-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-border-${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(value);
|
||||
document.documentElement.style.setProperty("color-scheme", type);
|
||||
}}
|
||||
|
||||
@@ -209,8 +209,8 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="grid-row-2 grid rounded-[10px] shadow divide-y bg-custom-background-100 border border-custom-border-100">
|
||||
<div className="grid grid-cols-1 divide-y border-custom-border-100 lg:divide-y-0 lg:divide-x lg:grid-cols-3">
|
||||
<div className="grid-row-2 grid rounded-[10px] shadow divide-y bg-custom-background-100 border border-custom-border-200">
|
||||
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:divide-y-0 lg:divide-x lg:grid-cols-3">
|
||||
<div className="flex flex-col text-xs">
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-60 flex-col gap-5 justify-between rounded-b-[10px] p-4">
|
||||
@@ -362,8 +362,8 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid col-span-2 grid-cols-1 divide-y border-custom-border-100 md:divide-y-0 md:divide-x md:grid-cols-2">
|
||||
<div className="flex h-60 flex-col border-custom-border-100">
|
||||
<div className="grid col-span-2 grid-cols-1 divide-y border-custom-border-200 md:divide-y-0 md:divide-x md:grid-cols-2">
|
||||
<div className="flex h-60 flex-col border-custom-border-200">
|
||||
<div className="flex h-full w-full flex-col text-custom-text-200 p-4">
|
||||
<div className="flex w-full items-center gap-2 py-1">
|
||||
<span>Progress</span>
|
||||
@@ -391,12 +391,12 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-custom-border-100 h-60 overflow-y-scroll">
|
||||
<div className="border-custom-border-200 h-60 overflow-y-scroll">
|
||||
<ActiveCycleProgressStats cycle={cycle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 divide-y border-custom-border-100 lg:divide-y-0 lg:divide-x lg:grid-cols-2">
|
||||
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:divide-y-0 lg:divide-x lg:grid-cols-2">
|
||||
<div className="flex flex-col justify-between p-4">
|
||||
<div>
|
||||
<div className="text-custom-primary">High Priority Issues</div>
|
||||
@@ -406,7 +406,7 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
issues.map((issue) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="flex flex-wrap rounded-md items-center justify-between gap-2 border border-custom-border-100 bg-custom-background-90 px-3 py-1.5"
|
||||
className="flex flex-wrap rounded-md items-center justify-between gap-2 border border-custom-border-200 bg-custom-background-90 px-3 py-1.5"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
@@ -444,7 +444,7 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-200"
|
||||
className="group flex items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs text-custom-text-200"
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
@@ -517,7 +517,7 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-between border-custom-border-100 p-4">
|
||||
<div className="flex flex-col justify-between border-custom-border-200 p-4">
|
||||
<div className="flex items-start justify-between gap-4 py-1.5 text-xs">
|
||||
<div className="flex items-center gap-3 text-custom-text-100">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`px-3 py-1 text-custom-text-100 rounded-3xl border border-custom-border-100 ${
|
||||
`px-3 py-1 text-custom-text-100 rounded-3xl border border-custom-border-200 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
}`
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`px-3 py-1 text-custom-text-100 rounded-3xl border border-custom-border-100 ${
|
||||
`px-3 py-1 text-custom-text-100 rounded-3xl border border-custom-border-200 ${
|
||||
selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80"
|
||||
}`
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-100 bg-custom-background-80">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img
|
||||
src="/user.png"
|
||||
height="100%"
|
||||
|
||||
@@ -22,7 +22,7 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles }) => {
|
||||
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
|
||||
<div
|
||||
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
|
||||
style={{ backgroundColor: "#858e96" }}
|
||||
style={{ backgroundColor: "rgb(var(--color-primary-100))" }}
|
||||
/>
|
||||
<div className="text-custom-text-100 text-sm">{data?.name}</div>
|
||||
</div>
|
||||
@@ -32,7 +32,10 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles }) => {
|
||||
const GanttBlockView = ({ data }: { data: ICycle }) => (
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${data?.id}`}>
|
||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
||||
<div className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: "#858e96" }} />
|
||||
<div
|
||||
className="flex-shrink-0 w-[4px] h-full"
|
||||
style={{ backgroundColor: "rgb(var(--color-primary-100))" }}
|
||||
/>
|
||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
||||
{data?.name}
|
||||
|
||||
@@ -173,10 +173,10 @@ export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
viewType === "list" ? (
|
||||
<div className="divide-y divide-custom-border-100">
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-custom-background-80">
|
||||
<div className="flex flex-col border-custom-border-100">
|
||||
<div className="flex flex-col border-custom-border-200">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
|
||||
@@ -124,7 +124,7 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
|
||||
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 overflow-hidden rounded-lg border border-custom-border-100 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
|
||||
|
||||
@@ -107,7 +107,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-100 px-5 pt-5">
|
||||
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-200 px-5 pt-5">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{status
|
||||
|
||||
@@ -26,7 +26,7 @@ export const CycleIssuesGanttChartView: FC<Props> = ({}) => {
|
||||
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
|
||||
<div
|
||||
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
|
||||
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
|
||||
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
|
||||
/>
|
||||
<div className="text-custom-text-100 text-sm">{data?.name}</div>
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@ export const CycleIssuesGanttChartView: FC<Props> = ({}) => {
|
||||
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
|
||||
<div
|
||||
className="flex-shrink-0 w-[4px] h-full"
|
||||
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
|
||||
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
|
||||
/>
|
||||
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
|
||||
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
|
||||
|
||||
@@ -247,7 +247,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
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 border border-custom-border-100 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">
|
||||
<Dialog.Panel className="relative 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">
|
||||
<CycleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
|
||||
@@ -66,7 +66,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Button
|
||||
className={`flex cursor-pointer items-center gap-1 rounded-md border border-custom-border-100 px-2 py-1 text-xs shadow-sm duration-300 hover:bg-custom-background-90 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
|
||||
className={`flex cursor-pointer items-center gap-1 rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm duration-300 hover:bg-custom-background-90 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
|
||||
>
|
||||
<CyclesIcon className="h-3 w-3 text-custom-text-200" />
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
|
||||
@@ -292,14 +292,14 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
<div
|
||||
className={`fixed top-[66px] ${
|
||||
isOpen ? "right-0" : "-right-[24rem]"
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-200 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
|
||||
>
|
||||
{cycle ? (
|
||||
<>
|
||||
<div className="flex flex-col items-start justify-center">
|
||||
<div className="flex gap-2.5 px-5 text-sm">
|
||||
<div className="flex items-center">
|
||||
<span className="flex items-center rounded border-[0.5px] border-custom-border-100 bg-custom-background-90 px-2 py-1 text-center text-xs capitalize">
|
||||
<span className="flex items-center rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-center text-xs capitalize">
|
||||
{capitalizeFirstLetter(cycleStatus)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -309,7 +309,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
<>
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-100 bg-custom-background-90 px-2 py-1 text-xs ${
|
||||
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
|
||||
cycle.start_date ? "" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
@@ -359,7 +359,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
<>
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-100 bg-custom-background-90 px-2 py-1 text-xs ${
|
||||
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
|
||||
cycle.end_date ? "" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
@@ -477,7 +477,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-100 p-6">
|
||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 p-6">
|
||||
<Disclosure defaultOpen>
|
||||
{({ open }) => (
|
||||
<div
|
||||
@@ -561,7 +561,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-100 p-6">
|
||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 p-6">
|
||||
<Disclosure defaultOpen>
|
||||
{({ open }) => (
|
||||
<div
|
||||
|
||||
@@ -128,7 +128,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col rounded-[10px] bg-custom-background-100 border border-custom-border-100 text-xs shadow">
|
||||
<div className="flex flex-col rounded-[10px] bg-custom-background-100 border border-custom-border-200 text-xs shadow">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
@@ -321,7 +321,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<div
|
||||
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-custom-border-100 bg-custom-background-80 text-custom-text-200 ${
|
||||
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-custom-border-200 bg-custom-background-80 text-custom-text-200 ${
|
||||
open ? "" : "flex-row"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -119,7 +119,7 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-b border-custom-border-100 px-5 pb-3">
|
||||
<div className="flex items-center gap-2 border-b border-custom-border-200 px-5 pb-3">
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-custom-text-200" />
|
||||
<input
|
||||
className="bg-custom-background-90 outline-none"
|
||||
|
||||
@@ -63,8 +63,8 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange, onIconColorC
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-100 bg-custom-background-80 shadow-lg">
|
||||
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-100 bg-custom-background-80 p-2 shadow-xl">
|
||||
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-200 bg-custom-background-80 p-2 shadow-xl">
|
||||
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
||||
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
|
||||
{tabOptions.map((tab) => (
|
||||
@@ -107,7 +107,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange, onIconColorC
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<hr className="mb-2 h-[1px] w-full border-custom-border-100" />
|
||||
<hr className="mb-2 h-[1px] w-full border-custom-border-200" />
|
||||
<div>
|
||||
<div className="grid grid-cols-8 gap-x-2 gap-y-3">
|
||||
{emojis.map((emoji) => (
|
||||
@@ -173,7 +173,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange, onIconColorC
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mb-1 h-[1px] w-full border-custom-border-100" />
|
||||
<hr className="mb-1 h-[1px] w-full border-custom-border-200" />
|
||||
<div className="mt-1 ml-1 grid grid-cols-8 gap-x-2 gap-y-3">
|
||||
{icons.material_rounded.map((icon, index) => (
|
||||
<button
|
||||
|
||||
@@ -254,7 +254,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
||||
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 border border-custom-border-100 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">
|
||||
<Dialog.Panel className="relative 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 onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium leading-6">
|
||||
|
||||
@@ -60,7 +60,7 @@ export const DeleteEstimateModal: React.FC<Props> = ({
|
||||
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 overflow-hidden rounded-lg border border-custom-border-100 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
|
||||
@@ -44,7 +44,7 @@ export const GanttChartBlocks: FC<{
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded shadow-sm bg-custom-background-100 overflow-hidden relative flex items-center h-[34px] border border-custom-border-100"
|
||||
className="rounded shadow-sm bg-custom-background-100 overflow-hidden relative flex items-center h-[34px] border border-custom-border-200"
|
||||
style={{
|
||||
width: `${block?.position?.width}px`,
|
||||
}}
|
||||
@@ -68,7 +68,7 @@ export const GanttChartBlocks: FC<{
|
||||
</div>
|
||||
|
||||
{/* sidebar */}
|
||||
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-custom-border-100 border-r border-custom-border-100 overflow-y-auto">
|
||||
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-custom-border-200 border-r border-custom-border-200 overflow-y-auto">
|
||||
{blocks &&
|
||||
blocks.length > 0 &&
|
||||
blocks.map((block: any, _idx: number) => (
|
||||
|
||||
@@ -7,18 +7,18 @@ export const BiWeekChartView: FC<any> = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100">
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-100">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-100">
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
@@ -29,7 +29,7 @@ export const BiWeekChartView: FC<any> = () => {
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||
_item?.today ? `text-red-500 border-red-500` : `border-custom-border-100`
|
||||
_item?.today ? `text-red-500 border-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
|
||||
@@ -7,18 +7,18 @@ export const DayChartView: FC<any> = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100">
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-100">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-100">
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
@@ -29,7 +29,7 @@ export const DayChartView: FC<any> = () => {
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
|
||||
_item?.today ? `text-red-500 border-red-500` : `border-custom-border-100`
|
||||
_item?.today ? `text-red-500 border-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user