Merge pull request #993 from makeplane/stage-release

promote: stage-release to prod
This commit is contained in:
Vamsi Kurama 2023-05-03 01:03:31 +05:30 committed by GitHub
commit 563bb12b9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
342 changed files with 7652 additions and 4061 deletions

View File

@ -3,15 +3,19 @@ DJANGO_SETTINGS_MODULE="plane.settings.production"
DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane
# Cache # Cache
REDIS_URL=redis://redis:6379/ REDIS_URL=redis://redis:6379/
# SMPT # SMTP
EMAIL_HOST="" EMAIL_HOST=""
EMAIL_HOST_USER="" EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD="" EMAIL_HOST_PASSWORD=""
# AWS EMAIL_PORT="587"
EMAIL_USE_TLS="1"
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
# AWS
AWS_REGION="" AWS_REGION=""
AWS_ACCESS_KEY_ID="" AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY="" AWS_SECRET_ACCESS_KEY=""
AWS_S3_BUCKET_NAME="" AWS_S3_BUCKET_NAME=""
AWS_S3_ENDPOINT_URL=""
# FE # FE
WEB_URL="localhost/" WEB_URL="localhost/"
# OAUTH # OAUTH
@ -21,4 +25,4 @@ DISABLE_COLLECTSTATIC=1
DOCKERIZED=1 DOCKERIZED=1
# GPT Envs # GPT Envs
OPENAI_API_KEY=0 OPENAI_API_KEY=0
GPT_ENGINE=0 GPT_ENGINE=0

View File

@ -3,7 +3,15 @@ import uuid
import random import random
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from plane.db.models import ProjectIdentifier from plane.db.models import ProjectIdentifier
from plane.db.models import Issue, IssueComment, User, Project, ProjectMember, Label from plane.db.models import (
Issue,
IssueComment,
User,
Project,
ProjectMember,
Label,
Integration,
)
# Update description and description html values for old descriptions # Update description and description html values for old descriptions
@ -174,3 +182,29 @@ def update_label_color():
except Exception as e: except Exception as e:
print(e) print(e)
print("Failed") print("Failed")
def create_slack_integration():
try:
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_integration_verified():
try:
integrations = Integration.objects.all()
updated_integrations = []
for integration in integrations:
integration.verified = True
updated_integrations.append(integration)
Integration.objects.bulk_update(
updated_integrations, ["verified"], batch_size=10
)
print("Sucess")
except Exception as e:
print(e)
print("Failed")

View File

@ -62,10 +62,11 @@ from .integration import (
GithubRepositorySerializer, GithubRepositorySerializer,
GithubRepositorySyncSerializer, GithubRepositorySyncSerializer,
GithubCommentSyncSerializer, GithubCommentSyncSerializer,
SlackProjectSyncSerializer,
) )
from .importer import ImporterSerializer from .importer import ImporterSerializer
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
from .estimate import EstimateSerializer, EstimatePointSerializer from .estimate import EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer

View File

@ -23,3 +23,16 @@ class EstimatePointSerializer(BaseSerializer):
"workspace", "workspace",
"project", "project",
] ]
class EstimateReadSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]

View File

@ -5,3 +5,4 @@ from .github import (
GithubIssueSyncSerializer, GithubIssueSyncSerializer,
GithubCommentSyncSerializer, GithubCommentSyncSerializer,
) )
from .slack import SlackProjectSyncSerializer

View File

@ -0,0 +1,14 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.db.models import SlackProjectSync
class SlackProjectSyncSerializer(BaseSerializer):
class Meta:
model = SlackProjectSync
fields = "__all__"
read_only_fields = [
"project",
"workspace",
"workspace_integration",
]

View File

@ -81,8 +81,6 @@ from plane.api.views import (
StateViewSet, StateViewSet,
## End States ## End States
# Estimates # Estimates
EstimateViewSet,
EstimatePointViewSet,
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint, BulkEstimatePointEndpoint,
## End Estimates ## End Estimates
@ -133,6 +131,7 @@ from plane.api.views import (
GithubIssueSyncViewSet, GithubIssueSyncViewSet,
GithubCommentSyncViewSet, GithubCommentSyncViewSet,
BulkCreateGithubIssueSyncEndpoint, BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
## End Integrations ## End Integrations
# Importer # Importer
ServiceIssueImportSummaryEndpoint, ServiceIssueImportSummaryEndpoint,
@ -146,6 +145,9 @@ from plane.api.views import (
# Gpt # Gpt
GPTIntegrationEndpoint, GPTIntegrationEndpoint,
## End Gpt ## End Gpt
# Release Notes
ReleaseNotesEndpoint,
## End Release Notes
) )
@ -507,62 +509,34 @@ urlpatterns = [
name="project-state", name="project-state",
), ),
# End States ## # End States ##
# States # Estimates
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
EstimateViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-estimates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:pk>/",
EstimateViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-estimates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<uuid:pk>/",
EstimatePointViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-estimates",
),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(), ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points", name="project-estimate-points",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/bulk-estimate-points/", "workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(), BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="bulk-create-estimate-points", name="bulk-create-estimate-points",
), ),
# End States ## path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="bulk-create-estimate-points",
),
# End Estimates ##
# Shortcuts # Shortcuts
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/", "workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
@ -1237,6 +1211,26 @@ urlpatterns = [
), ),
), ),
## End Github Integrations ## End Github Integrations
# Slack Integration
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
SlackProjectSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
SlackProjectSyncViewSet.as_view(
{
"delete": "destroy",
"get": "retrieve",
}
),
),
## End Slack Integration
## End Integrations ## End Integrations
# Importer # Importer
path( path(
@ -1284,4 +1278,11 @@ urlpatterns = [
name="importer", name="importer",
), ),
## End Gpt ## End Gpt
# Release Notes
path(
"release-notes/",
ReleaseNotesEndpoint.as_view(),
name="release-notes",
),
## End Release Notes
] ]

View File

@ -106,6 +106,7 @@ from .integration import (
GithubCommentSyncViewSet, GithubCommentSyncViewSet,
GithubRepositoriesEndpoint, GithubRepositoriesEndpoint,
BulkCreateGithubIssueSyncEndpoint, BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
) )
from .importer import ( from .importer import (
@ -133,8 +134,9 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint from .gpt import GPTIntegrationEndpoint
from .estimate import ( from .estimate import (
EstimateViewSet,
EstimatePointViewSet,
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint, BulkEstimatePointEndpoint,
) )
from .release import ReleaseNotesEndpoint

View File

@ -10,7 +10,7 @@ from rest_framework.views import APIView
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import NotFound from rest_framework.exceptions import NotFound
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
# Module imports # Module imports
@ -39,7 +39,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
try: try:
return self.model.objects.all() return self.model.objects.all()
except Exception as e: except Exception as e:
print(e) capture_exception(e)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST) raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):

View File

@ -48,6 +48,28 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"), owned_by=self.request.user project_id=self.kwargs.get("project_id"), owned_by=self.request.user
) )
def perform_destroy(self, instance):
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in cycle_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self): def get_queryset(self):
subquery = CycleFavorite.objects.filter( subquery = CycleFavorite.objects.filter(
user=self.request.user, user=self.request.user,
@ -181,6 +203,22 @@ class CycleIssueViewSet(BaseViewSet):
cycle_id=self.kwargs.get("cycle_id"), cycle_id=self.kwargs.get("cycle_id"),
) )
def perform_destroy(self, instance):
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
super() super()
@ -286,9 +324,9 @@ class CycleIssueViewSet(BaseViewSet):
# Get all CycleIssues already created # Get all CycleIssues already created
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues)) cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
records_to_update = []
update_cycle_issue_activity = [] update_cycle_issue_activity = []
record_to_create = [] record_to_create = []
records_to_update = []
for issue in issues: for issue in issues:
cycle_issue = [ cycle_issue = [
@ -333,7 +371,7 @@ class CycleIssueViewSet(BaseViewSet):
# Capture Issue Activity # Capture Issue Activity
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": issues}), requested_data=json.dumps({"cycles_list": issues}),
actor_id=str(self.request.user.id), actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)), issue_id=str(self.kwargs.get("pk", None)),

View File

@ -10,110 +10,11 @@ from sentry_sdk import capture_exception
from .base import BaseViewSet, BaseAPIView from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Project, Estimate, EstimatePoint from plane.db.models import Project, Estimate, EstimatePoint
from plane.api.serializers import EstimateSerializer, EstimatePointSerializer from plane.api.serializers import (
EstimateSerializer,
EstimatePointSerializer,
class EstimateViewSet(BaseViewSet): EstimateReadSerializer,
permission_classes = [ )
ProjectEntityPermission,
]
model = Estimate
serializer_class = EstimateSerializer
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.distinct()
)
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
class EstimatePointViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
model = EstimatePoint
serializer_class = EstimatePointSerializer
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.filter(estimate_id=self.kwargs.get("estimate_id"))
.select_related("project")
.select_related("workspace")
.distinct()
)
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
estimate_id=self.kwargs.get("estimate_id"),
)
def create(self, request, slug, project_id, estimate_id):
try:
serializer = EstimatePointSerializer(data=request.data)
if serializer.is_valid():
serializer.save(estimate_id=estimate_id, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The estimate point is already taken"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, estimate_id, pk):
try:
estimate_point = EstimatePoint.objects.get(
pk=pk,
estimate_id=estimate_id,
project_id=project_id,
workspace__slug=slug,
)
serializer = EstimatePointSerializer(
estimate_point, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save(estimate_id=estimate_id, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except EstimatePoint.DoesNotExist:
return Response(
{"error": "Estimate Point does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The estimate point value is already taken"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectEstimatePointEndpoint(BaseAPIView): class ProjectEstimatePointEndpoint(BaseAPIView):
@ -141,17 +42,35 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
) )
class BulkEstimatePointEndpoint(BaseAPIView): class BulkEstimatePointEndpoint(BaseViewSet):
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]
model = Estimate
serializer_class = EstimateSerializer
def post(self, request, slug, project_id, estimate_id): def list(self, request, slug, project_id):
try: try:
estimate = Estimate.objects.get( estimates = Estimate.objects.filter(
pk=estimate_id, workspace__slug=slug, project=project_id workspace__slug=slug, project_id=project_id
).prefetch_related("points")
serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
) )
def create(self, request, slug, project_id):
try:
if not request.data.get("estimate", False):
return Response(
{"error": "Estimate is required"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points = request.data.get("estimate_points", []) estimate_points = request.data.get("estimate_points", [])
if not len(estimate_points) or len(estimate_points) > 8: if not len(estimate_points) or len(estimate_points) > 8:
@ -160,6 +79,18 @@ class BulkEstimatePointEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
try:
estimate = estimate_serializer.save(project_id=project_id)
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points = EstimatePoint.objects.bulk_create( estimate_points = EstimatePoint.objects.bulk_create(
[ [
EstimatePoint( EstimatePoint(
@ -178,9 +109,17 @@ class BulkEstimatePointEndpoint(BaseAPIView):
ignore_conflicts=True, ignore_conflicts=True,
) )
serializer = EstimatePointSerializer(estimate_points, many=True) estimate_point_serializer = EstimatePointSerializer(
estimate_points, many=True
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(
{
"estimate": estimate_serializer.data,
"estimate_points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist: except Estimate.DoesNotExist:
return Response( return Response(
{"error": "Estimate does not exist"}, {"error": "Estimate does not exist"},
@ -193,14 +132,58 @@ class BulkEstimatePointEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
def patch(self, request, slug, project_id, estimate_id): def retrieve(self, request, slug, project_id, estimate_id):
try: try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateReadSerializer(estimate)
return Response(
serializer.data,
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, estimate_id):
try:
if not request.data.get("estimate", False):
return Response(
{"error": "Estimate is required"},
status=status.HTTP_400_BAD_REQUEST,
)
if not len(request.data.get("estimate_points", [])): if not len(request.data.get("estimate_points", [])):
return Response( return Response(
{"error": "Estimate points are required"}, {"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
estimate = Estimate.objects.get(pk=estimate_id)
estimate_serializer = EstimateSerializer(
estimate, data=request.data.get("estimate"), partial=True
)
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
try:
estimate = estimate_serializer.save()
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points_data = request.data.get("estimate_points", []) estimate_points_data = request.data.get("estimate_points", [])
estimate_points = EstimatePoint.objects.filter( estimate_points = EstimatePoint.objects.filter(
@ -212,7 +195,6 @@ class BulkEstimatePointEndpoint(BaseAPIView):
estimate_id=estimate_id, estimate_id=estimate_id,
) )
print(estimate_points)
updated_estimate_points = [] updated_estimate_points = []
for estimate_point in estimate_points: for estimate_point in estimate_points:
# Find the data for that estimate point # Find the data for that estimate point
@ -221,24 +203,50 @@ class BulkEstimatePointEndpoint(BaseAPIView):
for point in estimate_points_data for point in estimate_points_data
if point.get("id") == str(estimate_point.id) if point.get("id") == str(estimate_point.id)
] ]
print(estimate_point_data)
if len(estimate_point_data): if len(estimate_point_data):
estimate_point.value = estimate_point_data[0].get( estimate_point.value = estimate_point_data[0].get(
"value", estimate_point.value "value", estimate_point.value
) )
updated_estimate_points.append(estimate_point) updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update( try:
updated_estimate_points, ["value"], batch_size=10 EstimatePoint.objects.bulk_update(
updated_estimate_points, ["value"], batch_size=10
)
except IntegrityError as e:
return Response(
{"error": "Values need to be unique for each key"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(
{
"estimate": estimate_serializer.data,
"estimate_points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK,
) )
serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Estimate.DoesNotExist: except Estimate.DoesNotExist:
return Response( return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST {"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
) )
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, estimate_id):
try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
estimate.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,

View File

@ -28,6 +28,7 @@ from plane.db.models import (
Module, Module,
ModuleLink, ModuleLink,
ModuleIssue, ModuleIssue,
Label,
) )
from plane.api.serializers import ( from plane.api.serializers import (
ImporterSerializer, ImporterSerializer,
@ -104,7 +105,7 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -235,9 +236,20 @@ class ImportServiceEndpoint(BaseAPIView):
def delete(self, request, slug, service, pk): def delete(self, request, slug, service, pk):
try: try:
importer = Importer.objects.filter( importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug pk=pk, service=service, workspace__slug=slug
) )
# Delete all imported Issues
imported_issues = importer.imported_data.get("issues", [])
Issue.objects.filter(id__in=imported_issues).delete()
# Delete all imported Labels
imported_labels = importer.imported_data.get("labels", [])
Label.objects.filter(id__in=imported_labels).delete()
if importer.service == "jira":
imported_modules = importer.imported_data.get("modules", [])
Module.objects.filter(id__in=imported_modules).delete()
importer.delete() importer.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e: except Exception as e:
@ -247,6 +259,27 @@ class ImportServiceEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
def patch(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
serializer = ImporterSerializer(importer, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Importer.DoesNotExist:
return Response(
{"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UpdateServiceImportStatusEndpoint(BaseAPIView): class UpdateServiceImportStatusEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service, importer_id): def post(self, request, slug, project_id, service, importer_id):
@ -487,48 +520,59 @@ class BulkImportModulesEndpoint(BaseAPIView):
ignore_conflicts=True, ignore_conflicts=True,
) )
_ = ModuleLink.objects.bulk_create( modules = Module.objects.filter(id__in=[module.id for module in modules])
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get("url", "https://plane.so"),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
bulk_module_issues = [] if len(modules) == len(modules_data):
for module, module_data in zip(modules, modules_data): _ = ModuleLink.objects.bulk_create(
module_issues_list = module_data.get("module_issues_list", []) [
bulk_module_issues = bulk_module_issues + [ ModuleLink(
ModuleIssue( module=module,
issue_id=issue, url=module_data.get("link", {}).get(
module=module, "url", "https://plane.so"
project_id=project_id, ),
workspace_id=project.workspace_id, title=module_data.get("link", {}).get(
created_by=request.user, "title", "Original Issue"
updated_by=request.user, ),
) project_id=project_id,
for issue in module_issues_list workspace_id=project.workspace_id,
] created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
_ = ModuleIssue.objects.bulk_create( bulk_module_issues = []
bulk_module_issues, batch_size=100, ignore_conflicts=True for module, module_data in zip(modules, modules_data):
) module_issues_list = module_data.get("module_issues_list", [])
bulk_module_issues = bulk_module_issues + [
ModuleIssue(
issue_id=issue,
module=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for issue in module_issues_list
]
serializer = ModuleSerializer(modules, many=True) _ = ModuleIssue.objects.bulk_create(
return Response( bulk_module_issues, batch_size=100, ignore_conflicts=True
{"modules": serializer.data}, status=status.HTTP_201_CREATED )
)
serializer = ModuleSerializer(modules, many=True)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
else:
return Response(
{"message": "Modules created but issues could not be imported"},
status=status.HTTP_200_OK,
)
except Project.DoesNotExist: except Project.DoesNotExist:
return Response( return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND

View File

@ -6,3 +6,4 @@ from .github import (
GithubCommentSyncViewSet, GithubCommentSyncViewSet,
GithubRepositoriesEndpoint, GithubRepositoriesEndpoint,
) )
from .slack import SlackProjectSyncViewSet

View File

@ -27,6 +27,7 @@ from plane.utils.integrations.github import (
) )
from plane.api.permissions import WorkSpaceAdminPermission from plane.api.permissions import WorkSpaceAdminPermission
class IntegrationViewSet(BaseViewSet): class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer serializer_class = IntegrationSerializer
model = Integration model = Integration
@ -101,7 +102,6 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
WorkSpaceAdminPermission, WorkSpaceAdminPermission,
] ]
def get_queryset(self): def get_queryset(self):
return ( return (
super() super()
@ -112,21 +112,30 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
def create(self, request, slug, provider): def create(self, request, slug, provider):
try: try:
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
integration = Integration.objects.get(provider=provider) integration = Integration.objects.get(provider=provider)
config = {} config = {}
if provider == "github": if provider == "github":
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
metadata = get_github_metadata(installation_id) metadata = get_github_metadata(installation_id)
config = {"installation_id": installation_id} config = {"installation_id": installation_id}
if provider == "slack":
metadata = request.data.get("metadata", {})
access_token = metadata.get("access_token", False)
team_id = metadata.get("team", {}).get("id", False)
if not metadata or not access_token or not team_id:
return Response(
{"error": "Access token and team id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
config = {"team_id": team_id, "access_token": access_token}
# Create a bot user # Create a bot user
bot_user = User.objects.create( bot_user = User.objects.create(
email=f"{uuid.uuid4().hex}@plane.so", email=f"{uuid.uuid4().hex}@plane.so",

View File

@ -0,0 +1,59 @@
# Django import
from django.db import IntegrityError
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
from plane.api.serializers import SlackProjectSyncSerializer
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
class SlackProjectSyncViewSet(BaseViewSet):
permission_classes = [
ProjectBasePermission,
]
serializer_class = SlackProjectSyncSerializer
model = SlackProjectSync
def create(self, request, slug, project_id, workspace_integration_id):
try:
serializer = SlackProjectSyncSerializer(data=request.data)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if serializer.is_valid():
serializer.save(
project_id=project_id,
workspace_integration_id=workspace_integration_id,
)
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id, workspace__slug=slug
)
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -1,13 +1,14 @@
# Python imports # Python imports
import json import json
import random import random
from itertools import groupby, chain from itertools import chain
# Django imports # Django imports
from django.db.models import Prefetch, OuterRef, Func, F, Q from django.db.models import Prefetch, OuterRef, Func, F, Q, Count
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.db.models.functions import Coalesce
# Third Party imports # Third Party imports
from rest_framework.response import Response from rest_framework.response import Response
@ -46,6 +47,7 @@ from plane.db.models import (
Label, Label,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
State,
) )
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
@ -590,8 +592,31 @@ class SubIssuesEndpoint(BaseAPIView):
.prefetch_related("labels") .prefetch_related("labels")
) )
serializer = IssueLiteSerializer(sub_issues, many=True) state_distribution = (
return Response(serializer.data, status=status.HTTP_200_OK) State.objects.filter(workspace__slug=slug, project_id=project_id)
.annotate(
state_count=Count(
"state_issue",
filter=Q(state_issue__parent_id=issue_id),
)
)
.order_by("group")
.values("group", "state_count")
)
result = {item["group"]: item["state_count"] for item in state_distribution}
serializer = IssueLiteSerializer(
sub_issues,
many=True,
)
return Response(
{
"sub_issues": serializer.data,
"state_distribution": result,
},
status=status.HTTP_200_OK,
)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
return Response( return Response(

View File

@ -109,6 +109,28 @@ class ModuleViewSet(BaseViewSet):
.order_by("-is_favorite", "name") .order_by("-is_favorite", "name")
) )
def perform_destroy(self, instance):
module_issues = list(
ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try: try:
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
@ -158,6 +180,22 @@ class ModuleIssueViewSet(BaseViewSet):
module_id=self.kwargs.get("module_id"), module_id=self.kwargs.get("module_id"),
) )
def perform_destroy(self, instance):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("module_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
super() super()
@ -302,7 +340,7 @@ class ModuleIssueViewSet(BaseViewSet):
# Capture Issue Activity # Capture Issue Activity
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="module.activity.created",
requested_data=json.dumps({"modules_list": issues}), requested_data=json.dumps({"modules_list": issues}),
actor_id=str(self.request.user.id), actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)), issue_id=str(self.kwargs.get("pk", None)),

View File

@ -14,7 +14,7 @@ from rest_framework.permissions import AllowAny
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework import status from rest_framework import status
from sentry_sdk import capture_exception
# sso authentication # sso authentication
from google.oauth2 import id_token from google.oauth2 import id_token
from google.auth.transport import requests as google_auth_request from google.auth.transport import requests as google_auth_request
@ -48,7 +48,7 @@ def validate_google_token(token, client_id):
} }
return data return data
except Exception as e: except Exception as e:
print(e) capture_exception(e)
raise exceptions.AuthenticationFailed("Error with Google connection.") raise exceptions.AuthenticationFailed("Error with Google connection.")
@ -305,8 +305,7 @@ class OauthEndpoint(BaseAPIView):
) )
return Response(data, status=status.HTTP_201_CREATED) return Response(data, status=status.HTTP_201_CREATED)
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{ {
"error": "Something went wrong. Please try again later or contact the support team." "error": "Something went wrong. Please try again later or contact the support team."

View File

@ -96,6 +96,36 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
def partial_update(self, request, slug, project_id, pk):
try:
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# Only update access if the page owner is the requesting user
if (
page.access != request.data.get("access", page.access)
and page.owned_by_id != request.user.id
):
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = PageSerializer(page, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Page.DoesNotExist:
return Response(
{"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class PageBlockViewSet(BaseViewSet): class PageBlockViewSet(BaseViewSet):
serializer_class = PageBlockSerializer serializer_class = PageBlockSerializer
@ -344,7 +374,7 @@ class RecentPagesEndpoint(BaseAPIView):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,

View File

@ -0,0 +1,21 @@
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
from plane.utils.integrations.github import get_release_notes
class ReleaseNotesEndpoint(BaseAPIView):
def get(self, request):
try:
release_notes = get_release_notes()
return Response(release_notes, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -195,7 +195,7 @@ class GlobalSearchEndpoint(BaseAPIView):
return Response({"results": results}, status=status.HTTP_200_OK) return Response({"results": results}, status=status.HTTP_200_OK)
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,

View File

@ -1,6 +1,9 @@
# Python imports # Python imports
from itertools import groupby from itertools import groupby
# Django imports
from django.db import IntegrityError
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
@ -8,10 +11,10 @@ from sentry_sdk import capture_exception
# Module imports # Module imports
from . import BaseViewSet from . import BaseViewSet, BaseAPIView
from plane.api.serializers import StateSerializer from plane.api.serializers import StateSerializer
from plane.api.permissions import ProjectEntityPermission from plane.api.permissions import ProjectEntityPermission
from plane.db.models import State from plane.db.models import State, Issue
class StateViewSet(BaseViewSet): class StateViewSet(BaseViewSet):
@ -36,6 +39,25 @@ class StateViewSet(BaseViewSet):
.distinct() .distinct()
) )
def create(self, request, slug, project_id):
try:
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "State with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
try: try:
state_dict = dict() state_dict = dict()
@ -66,6 +88,17 @@ class StateViewSet(BaseViewSet):
{"error": "Default state cannot be deleted"}, status=False {"error": "Default state cannot be deleted"}, status=False
) )
# Check for any issues in the state
issue_exist = Issue.objects.filter(state=pk).exists()
if issue_exist:
return Response(
{
"error": "The state is not empty, only empty states can be deleted"
},
status=status.HTTP_400_BAD_REQUEST,
)
state.delete() state.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except State.DoesNotExist: except State.DoesNotExist:

View File

@ -145,7 +145,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e: except Exception as e:
print(e)
capture_exception(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
@ -333,7 +332,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
status=status.HTTP_404_NOT_FOUND, status=status.HTTP_404_NOT_FOUND,
) )
except Exception as e: except Exception as e:
print(e)
capture_exception(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
@ -780,7 +778,7 @@ class WorkspaceThemeViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.conf import settings
# Third party imports # Third party imports
from celery import shared_task from celery import shared_task
@ -20,7 +21,7 @@ def email_verification(first_name, email, token, current_site):
realtivelink = "/request-email-verification/" + "?token=" + str(token) realtivelink = "/request-email-verification/" + "?token=" + str(token)
abs_url = "http://" + current_site + realtivelink abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>" from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!" subject = f"Verify your Email!"

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.conf import settings
# Third party imports # Third party imports
from celery import shared_task from celery import shared_task
@ -18,7 +19,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/" realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/"
abs_url = "http://" + current_site + realtivelink abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>" from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!" subject = f"Verify your Email!"

View File

@ -27,6 +27,7 @@ from plane.db.models import (
User, User,
) )
from .workspace_invitation_task import workspace_invitation from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_email
@shared_task @shared_task
@ -40,7 +41,7 @@ def service_importer(service, importer_id):
# Check if we need to import users as well # Check if we need to import users as well
if len(users): if len(users):
# For all invited users create the uers # For all invited users create the users
new_users = User.objects.bulk_create( new_users = User.objects.bulk_create(
[ [
User( User(
@ -56,6 +57,15 @@ def service_importer(service, importer_id):
ignore_conflicts=True, ignore_conflicts=True,
) )
[
send_welcome_email.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
workspace_users = User.objects.filter( workspace_users = User.objects.filter(
email__in=[ email__in=[
user.get("email").strip().lower() user.get("email").strip().lower()

View File

@ -506,119 +506,6 @@ def track_blockings(
) )
def track_cycles(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
# Updated Records:
updated_records = current_instance.get("updated_cycle_issues", [])
created_records = json.loads(current_instance.get("created_cycle_issues", []))
for updated_record in updated_records:
old_cycle = Cycle.objects.filter(
pk=updated_record.get("old_cycle_id", None)
).first()
new_cycle = Cycle.objects.filter(
pk=updated_record.get("new_cycle_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_cycle.name,
new_value=new_cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
)
)
for created_record in created_records:
cycle = Cycle.objects.filter(
pk=created_record.get("fields").get("cycle")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added cycle {cycle.name}",
new_identifier=cycle.id,
)
)
def track_modules(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
# Updated Records:
updated_records = current_instance.get("updated_module_issues", [])
created_records = json.loads(current_instance.get("created_module_issues", []))
for updated_record in updated_records:
old_module = Module.objects.filter(
pk=updated_record.get("old_module_id", None)
).first()
new_module = Module.objects.filter(
pk=updated_record.get("new_module_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_module.name,
new_value=new_module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}",
old_identifier=old_module.id,
new_identifier=new_module.id,
)
)
for created_record in created_records:
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added module {module.name}",
new_identifier=module.id,
)
)
def create_issue_activity( def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities requested_data, current_instance, issue_id, project, actor, issue_activities
): ):
@ -683,8 +570,6 @@ def update_issue_activity(
"assignees_list": track_assignees, "assignees_list": track_assignees,
"blocks_list": track_blocks, "blocks_list": track_blocks,
"blockers_list": track_blockings, "blockers_list": track_blockings,
"cycles_list": track_cycles,
"modules_list": track_modules,
"estimate_point": track_estimate_points, "estimate_point": track_estimate_points,
} }
@ -788,6 +673,177 @@ def delete_comment_activity(
) )
def create_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_cycle_issues", [])
created_records = json.loads(current_instance.get("created_cycle_issues", []))
for updated_record in updated_records:
old_cycle = Cycle.objects.filter(
pk=updated_record.get("old_cycle_id", None)
).first()
new_cycle = Cycle.objects.filter(
pk=updated_record.get("new_cycle_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_cycle.name,
new_value=new_cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
)
)
for created_record in created_records:
cycle = Cycle.objects.filter(
pk=created_record.get("fields").get("cycle")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added cycle {cycle.name}",
new_identifier=cycle.id,
)
)
def delete_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
cycle_id = requested_data.get("cycle_id", "")
cycle = Cycle.objects.filter(pk=cycle_id).first()
issues = requested_data.get("issues")
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=cycle.name if cycle is not None else "",
new_value="",
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {cycle.name if cycle is not None else None}",
old_identifier=cycle.id if cycle is not None else None,
)
)
def create_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_module_issues", [])
created_records = json.loads(current_instance.get("created_module_issues", []))
for updated_record in updated_records:
old_module = Module.objects.filter(
pk=updated_record.get("old_module_id", None)
).first()
new_module = Module.objects.filter(
pk=updated_record.get("new_module_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_module.name,
new_value=new_module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}",
old_identifier=old_module.id,
new_identifier=new_module.id,
)
)
for created_record in created_records:
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added module {module.name}",
new_identifier=module.id,
)
)
def delete_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
module_id = requested_data.get("module_id", "")
module = Module.objects.filter(pk=module_id).first()
issues = requested_data.get("issues")
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=module.name if module is not None else "",
new_value="",
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {module.name if module is not None else None}",
old_identifier=module.id if module is not None else None,
)
)
def create_link_activity( def create_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities requested_data, current_instance, issue_id, project, actor, issue_activities
): ):
@ -910,6 +966,10 @@ def issue_activity(
"comment.activity.created": create_comment_activity, "comment.activity.created": create_comment_activity,
"comment.activity.updated": update_comment_activity, "comment.activity.updated": update_comment_activity,
"comment.activity.deleted": delete_comment_activity, "comment.activity.deleted": delete_comment_activity,
"cycle.activity.created": create_cycle_issue_activity,
"cycle.activity.deleted": delete_cycle_issue_activity,
"module.activity.created": create_module_issue_activity,
"module.activity.deleted": delete_module_issue_activity,
"link.activity.created": create_link_activity, "link.activity.created": create_link_activity,
"link.activity.updated": update_link_activity, "link.activity.updated": update_link_activity,
"link.activity.deleted": delete_link_activity, "link.activity.deleted": delete_link_activity,
@ -947,6 +1007,5 @@ def issue_activity(
) )
return return
except Exception as e: except Exception as e:
print(e)
capture_exception(e) capture_exception(e)
return return

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.conf import settings
# Third party imports # Third party imports
from celery import shared_task from celery import shared_task
@ -14,7 +15,7 @@ def magic_link(email, key, token, current_site):
realtivelink = f"/magic-sign-in/?password={token}&key={key}" realtivelink = f"/magic-sign-in/?password={token}&key={key}"
abs_url = "http://" + current_site + realtivelink abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>" from_email_string = settings.EMAIL_FROM
subject = f"Login for Plane" subject = f"Login for Plane"
@ -29,6 +30,5 @@ def magic_link(email, key, token, current_site):
msg.send() msg.send()
return return
except Exception as e: except Exception as e:
print(e)
capture_exception(e) capture_exception(e)
return return

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.conf import settings
# Third party imports # Third party imports
from celery import shared_task from celery import shared_task
@ -22,7 +23,7 @@ def project_invitation(email, project_id, token, current_site):
relativelink = f"/project-member-invitation/{project_member_invite.id}" relativelink = f"/project-member-invitation/{project_member_invite.id}"
abs_url = "http://" + current_site + relativelink abs_url = "http://" + current_site + relativelink
from_email_string = f"Team Plane <team@mailer.plane.so>" from_email_string = settings.EMAIL_FROM
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane" subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
@ -49,6 +50,5 @@ def project_invitation(email, project_id, token, current_site):
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e: except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
return return
except Exception as e: except Exception as e:
print(e)
capture_exception(e) capture_exception(e)
return return

View File

@ -0,0 +1,56 @@
# Django imports
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import User
@shared_task
def send_welcome_email(user_id, created, message):
try:
instance = User.objects.get(pk=user_id)
if created and not instance.is_bot:
first_name = instance.first_name.capitalize()
to_email = instance.email
from_email_string = settings.EMAIL_FROM
subject = f"Welcome to Plane ✈️!"
context = {"first_name": first_name, "email": instance.email}
html_content = render_to_string(
"emails/auth/user_welcome_email.html", context
)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(
subject, text_content, from_email_string, [to_email]
)
msg.attach_alternative(html_content, "text/html")
msg.send()
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=message,
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
capture_exception(e)
return

View File

@ -27,7 +27,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
) )
abs_url = "http://" + current_site + realtivelink abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>" from_email_string = settings.EMAIL_FROM
subject = f"{invitor or email} invited you to join {workspace.name} on Plane" subject = f"{invitor or email} invited you to join {workspace.name} on Plane"

View File

@ -0,0 +1,58 @@
# Generated by Django 3.2.18 on 2023-05-01 19:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0028_auto_20230414_1703'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='importer',
name='imported_data',
field=models.JSONField(null=True),
),
migrations.AddField(
model_name='module',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.CreateModel(
name='SlackProjectSync',
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)),
('access_token', models.CharField(max_length=300)),
('scopes', models.TextField()),
('bot_user_id', models.CharField(max_length=50)),
('webhook_url', models.URLField(max_length=1000)),
('data', models.JSONField(default=dict)),
('team_id', models.CharField(max_length=30)),
('team_name', models.CharField(max_length=300)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_slackprojectsync', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_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_slackprojectsync', to='db.workspace')),
('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slack_syncs', to='db.workspaceintegration')),
],
options={
'verbose_name': 'Slack Project Sync',
'verbose_name_plural': 'Slack Project Syncs',
'db_table': 'slack_project_syncs',
'ordering': ('-created_at',),
'unique_together': {('team_id', 'project')},
},
),
]

View File

@ -59,6 +59,7 @@ from .integration import (
GithubRepositorySync, GithubRepositorySync,
GithubIssueSync, GithubIssueSync,
GithubCommentSync, GithubCommentSync,
SlackProjectSync,
) )
from .importer import Importer from .importer import Importer

View File

@ -16,6 +16,7 @@ class Cycle(ProjectBaseModel):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="owned_by_cycle", related_name="owned_by_cycle",
) )
view_props = models.JSONField(default=dict)
class Meta: class Meta:
verbose_name = "Cycle" verbose_name = "Cycle"

View File

@ -33,6 +33,7 @@ class Importer(ProjectBaseModel):
token = models.ForeignKey( token = models.ForeignKey(
"db.APIToken", on_delete=models.CASCADE, related_name="importer" "db.APIToken", on_delete=models.CASCADE, related_name="importer"
) )
imported_data = models.JSONField(null=True)
class Meta: class Meta:
verbose_name = "Importer" verbose_name = "Importer"

View File

@ -1,2 +1,3 @@
from .base import Integration, WorkspaceIntegration from .base import Integration, WorkspaceIntegration
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
from .slack import SlackProjectSync

View File

@ -0,0 +1,32 @@
# Python imports
import uuid
# Django imports
from django.db import models
# Module imports
from plane.db.models import ProjectBaseModel
class SlackProjectSync(ProjectBaseModel):
access_token = models.CharField(max_length=300)
scopes = models.TextField()
bot_user_id = models.CharField(max_length=50)
webhook_url = models.URLField(max_length=1000)
data = models.JSONField(default=dict)
team_id = models.CharField(max_length=30)
team_name = models.CharField(max_length=300)
workspace_integration = models.ForeignKey(
"db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE
)
def __str__(self):
"""Return the repo name"""
return f"{self.project.name}"
class Meta:
unique_together = ["team_id", "project"]
verbose_name = "Slack Project Sync"
verbose_name_plural = "Slack Project Syncs"
db_table = "slack_project_syncs"
ordering = ("-created_at",)

View File

@ -39,6 +39,7 @@ class Module(ProjectBaseModel):
through="ModuleMember", through="ModuleMember",
through_fields=("module", "member"), through_fields=("module", "member"),
) )
view_props = models.JSONField(default=dict)
class Meta: class Meta:
unique_together = ["name", "project"] unique_together = ["name", "project"]

View File

@ -109,7 +109,7 @@ def send_welcome_email(sender, instance, created, **kwargs):
if created and not instance.is_bot: if created and not instance.is_bot:
first_name = instance.first_name.capitalize() first_name = instance.first_name.capitalize()
to_email = instance.email to_email = instance.email
from_email_string = f"Team Plane <team@mailer.plane.so>" from_email_string = settings.EMAIL_FROM
subject = f"Welcome to Plane ✈️!" subject = f"Welcome to Plane ✈️!"

View File

@ -174,11 +174,12 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Host for sending e-mail. # Host for sending e-mail.
EMAIL_HOST = os.environ.get("EMAIL_HOST") EMAIL_HOST = os.environ.get("EMAIL_HOST")
# Port for sending e-mail. # Port for sending e-mail.
EMAIL_PORT = 587 EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
# Optional SMTP authentication information for EMAIL_HOST. # Optional SMTP authentication information for EMAIL_HOST.
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = True EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1"
EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>")
SIMPLE_JWT = { SIMPLE_JWT = {
@ -210,4 +211,4 @@ SIMPLE_JWT = {
CELERY_TIMEZONE = TIME_ZONE CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_ACCEPT_CONTENT = ['application/json']

View File

@ -83,3 +83,6 @@ LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL")
CELERY_BROKER_URL = os.environ.get("REDIS_URL") CELERY_BROKER_URL = os.environ.get("REDIS_URL")
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)

View File

@ -105,7 +105,7 @@ if (
AWS_S3_ADDRESSING_STYLE = "auto" AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL. # The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = "" AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator. # A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = "" AWS_S3_KEY_PREFIX = ""
@ -240,7 +240,15 @@ SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
redis_url = os.environ.get("REDIS_URL") redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
CELERY_RESULT_BACKEND = broker_url if DOCKERIZED:
CELERY_BROKER_URL = broker_url CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
else:
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)

View File

@ -80,7 +80,7 @@ AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
AWS_S3_ADDRESSING_STYLE = "auto" AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL. # The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = "" AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator. # A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = "" AWS_S3_KEY_PREFIX = ""
@ -203,4 +203,6 @@ redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
CELERY_RESULT_BACKEND = broker_url CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)

View File

@ -5,6 +5,7 @@ from urllib.parse import urlparse, parse_qs
from datetime import datetime, timedelta from datetime import datetime, timedelta
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from django.conf import settings
def get_jwt_token(): def get_jwt_token():
@ -128,3 +129,24 @@ def get_github_repo_details(access_tokens_url, owner, repo):
).json() ).json()
return open_issues, total_labels, collaborators return open_issues, total_labels, collaborators
def get_release_notes():
token = settings.GITHUB_ACCESS_TOKEN
if token:
headers = {
"Authorization": "Bearer " + str(token),
"Accept": "application/vnd.github.v3+json",
}
else:
headers = {
"Accept": "application/vnd.github.v3+json",
}
url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1"
response = requests.get(url, headers=headers)
if response.status_code != 200:
return {"error": "Unable to render information from Github Repository"}
return response.json()

View File

@ -13,6 +13,17 @@ def filter_state(params, filter, method):
return filter return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
if len(estimate_points) and "" not in estimate_points:
filter["estimate_point__in"] = estimate_points
else:
if params.get("estimate_point", None) and len(params.get("estimate_point")):
filter["estimate_point__in"] = params.get("estimate_point")
return filter
def filter_priority(params, filter, method): def filter_priority(params, filter, method):
if method == "GET": if method == "GET":
priorties = params.get("priority").split(",") priorties = params.get("priority").split(",")
@ -192,6 +203,7 @@ def issue_filters(query_params, method):
ISSUE_FILTER = { ISSUE_FILTER = {
"state": filter_state, "state": filter_state,
"estimate_point": filter_estimate_point,
"priority": filter_priority, "priority": filter_priority,
"parent": filter_parent, "parent": filter_parent,
"labels": filter_labels, "labels": filter_labels,

View File

@ -1,5 +1,6 @@
# Replace with your instance Public IP # Replace with your instance Public IP
# NEXT_PUBLIC_API_BASE_URL = "http://localhost" # NEXT_PUBLIC_API_BASE_URL = "http://localhost"
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
NEXT_PUBLIC_GOOGLE_CLIENTID="" NEXT_PUBLIC_GOOGLE_CLIENTID=""
NEXT_PUBLIC_GITHUB_APP_NAME="" NEXT_PUBLIC_GITHUB_APP_NAME=""
NEXT_PUBLIC_GITHUB_ID="" NEXT_PUBLIC_GITHUB_ID=""
@ -7,4 +8,5 @@ NEXT_PUBLIC_SENTRY_DSN=""
NEXT_PUBLIC_ENABLE_OAUTH=0 NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_SENTRY=0 NEXT_PUBLIC_ENABLE_SENTRY=0
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
NEXT_PUBLIC_TRACK_EVENTS=0 NEXT_PUBLIC_TRACK_EVENTS=0
NEXT_PUBLIC_SLACK_CLIENT_ID=""

View File

@ -92,13 +92,13 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
<> <>
<form className="space-y-5 py-5 px-5"> <form className="space-y-5 py-5 px-5">
{(codeSent || codeResent) && ( {(codeSent || codeResent) && (
<div className="rounded-md bg-green-50 p-4"> <div className="rounded-md bg-green-500/20 p-4">
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<CheckCircleIcon className="h-5 w-5 text-green-400" aria-hidden="true" /> <CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
</div> </div>
<div className="ml-3"> <div className="ml-3">
<p className="text-sm font-medium text-green-800"> <p className="text-sm font-medium text-green-500">
{codeResent {codeResent
? "Please check your mail for new code." ? "Please check your mail for new code."
: "Please check your mail for code."} : "Please check your mail for code."}
@ -141,7 +141,9 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
<button <button
type="button" type="button"
className={`mt-5 flex w-full justify-end text-xs outline-none ${ className={`mt-5 flex w-full justify-end text-xs outline-none ${
isResendDisabled ? "cursor-default text-gray-400" : "cursor-pointer text-theme" isResendDisabled
? "cursor-default text-brand-secondary"
: "cursor-pointer text-brand-accent"
} `} } `}
onClick={() => { onClick={() => {
setIsCodeResending(true); setIsCodeResending(true);
@ -174,7 +176,8 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
className="w-full text-center" className="w-full text-center"
size="md" size="md"
onClick={handleSubmit(handleSignin)} onClick={handleSubmit(handleSignin)}
loading={isSubmitting || (!isValid && isDirty)} disabled={!isValid && isDirty}
loading={isSubmitting}
> >
{isSubmitting ? "Signing in..." : "Sign in"} {isSubmitting ? "Signing in..." : "Sign in"}
</PrimaryButton> </PrimaryButton>

View File

@ -50,7 +50,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
if (!error?.response?.data) return; if (!error?.response?.data) return;
Object.keys(error.response.data).forEach((key) => { Object.keys(error.response.data).forEach((key) => {
const err = error.response.data[key]; const err = error.response.data[key];
console.log("err", err); console.log(err);
setError(key as keyof EmailPasswordFormValues, { setError(key as keyof EmailPasswordFormValues, {
type: "manual", type: "manual",
message: Array.isArray(err) ? err.join(", ") : err, message: Array.isArray(err) ? err.join(", ") : err,
@ -94,7 +94,9 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<div className="mt-2 flex items-center justify-between"> <div className="mt-2 flex items-center justify-between">
<div className="ml-auto text-sm"> <div className="ml-auto text-sm">
<Link href={"/forgot-password"}> <Link href={"/forgot-password"}>
<a className="font-medium text-theme hover:text-indigo-500">Forgot your password?</a> <a className="font-medium text-brand-accent hover:text-brand-accent">
Forgot your password?
</a>
</Link> </Link>
</div> </div>
</div> </div>
@ -102,7 +104,8 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<SecondaryButton <SecondaryButton
type="submit" type="submit"
className="w-full text-center" className="w-full text-center"
loading={isSubmitting || (!isValid && isDirty)} disabled={!isValid && isDirty}
loading={isSubmitting}
> >
{isSubmitting ? "Signing in..." : "Sign In"} {isSubmitting ? "Signing in..." : "Sign In"}
</SecondaryButton> </SecondaryButton>

View File

@ -33,11 +33,11 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
}, []); }, []);
return ( return (
<div className="px-1 w-full"> <div className="w-full px-1">
<Link <Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`} href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
> >
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-gray-200 p-2 text-sm font-medium text-gray-600 duration-300 hover:bg-gray-50"> <button className="flex w-full items-center justify-center gap-3 rounded-md border border-brand-base p-2 text-sm font-medium text-brand-secondary duration-300 hover:bg-brand-surface-2">
<Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" /> <Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" />
<span>Sign In with Github</span> <span>Sign In with Github</span>
</button> </button>

View File

@ -47,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
return ( return (
<> <>
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} /> <Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
<div className="h-12" id="googleSignInButton" ref={googleSignInButton} /> <div className="overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
</> </>
); );
}; };

View File

@ -36,16 +36,14 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
alt="ProjectSettingImg" alt="ProjectSettingImg"
/> />
</div> </div>
<h1 className="text-xl font-medium text-gray-900"> <h1 className="text-xl font-medium">Oops! You are not authorized to view this page</h1>
Oops! You are not authorized to view this page
</h1>
<div className="w-full text-base text-gray-500 max-w-md "> <div className="w-full max-w-md text-base text-brand-secondary">
{user ? ( {user ? (
<p> <p>
You have signed in as {user.email}. <br /> You have signed in as {user.email}. <br />
<Link href={`/signin?next=${currentPath}`}> <Link href={`/signin?next=${currentPath}`}>
<a className="text-gray-900 font-medium">Sign in</a> <a className="font-medium text-brand-base">Sign in</a>
</Link>{" "} </Link>{" "}
with different account that has access to this page. with different account that has access to this page.
</p> </p>
@ -53,7 +51,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
<p> <p>
You need to{" "} You need to{" "}
<Link href={`/signin?next=${currentPath}`}> <Link href={`/signin?next=${currentPath}`}>
<a className="text-gray-900 font-medium">Sign in</a> <a className="font-medium text-brand-base">Sign in</a>
</Link>{" "} </Link>{" "}
with an account that has access to this page. with an account that has access to this page.
</p> </p>

View File

@ -31,7 +31,7 @@ export const JoinProject: React.FC = () => {
project_ids: [projectId as string], project_ids: [projectId as string],
}) })
.then(async () => { .then(async () => {
await mutate(USER_PROJECT_VIEW(workspaceSlug.toString())); await mutate(USER_PROJECT_VIEW(projectId.toString()));
setIsJoiningProject(false); setIsJoiningProject(false);
}) })
.catch((err) => { .catch((err) => {
@ -45,9 +45,9 @@ export const JoinProject: React.FC = () => {
<div className="h-44 w-72"> <div className="h-44 w-72">
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" /> <Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
</div> </div>
<h1 className="text-xl font-medium text-gray-900">You are not a member of this project</h1> <h1 className="text-xl font-medium">You are not a member of this project</h1>
<div className="w-full max-w-md text-base text-gray-500 "> <div className="w-full max-w-md text-base text-brand-secondary">
<p className="mx-auto w-full text-sm md:w-3/4"> <p className="mx-auto w-full text-sm md:w-3/4">
You are not a member of this project, but you can join this project by clicking the button You are not a member of this project, but you can join this project by clicking the button
below. below.

View File

@ -20,12 +20,12 @@ export const NotAWorkspaceMember = () => {
<div className="space-y-8 text-center"> <div className="space-y-8 text-center">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-lg font-semibold">Not Authorized!</h3> <h3 className="text-lg font-semibold">Not Authorized!</h3>
<p className="text-sm text-gray-500 w-1/2 mx-auto"> <p className="mx-auto w-1/2 text-sm text-brand-secondary">
You{"'"}re not a member of this workspace. Please contact the workspace admin to get You{"'"}re not a member of this workspace. Please contact the workspace admin to get
an invitation or check your pending invitations. an invitation or check your pending invitations.
</p> </p>
</div> </div>
<div className="flex items-center gap-2 justify-center"> <div className="flex items-center justify-center gap-2">
<Link href="/invitations"> <Link href="/invitations">
<a> <a>
<SecondaryButton>Check pending invites</SecondaryButton> <SecondaryButton>Check pending invites</SecondaryButton>

View File

@ -16,7 +16,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
<div className="flex items-center"> <div className="flex items-center">
<button <button
type="button" type="button"
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-gray-300 text-center text-sm hover:bg-gray-100" className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
onClick={() => router.back()} onClick={() => router.back()}
> >
<ArrowLeftIcon className="h-3 w-3" /> <ArrowLeftIcon className="h-3 w-3" />
@ -37,7 +37,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
<> <>
{link ? ( {link ? (
<Link href={link}> <Link href={link}>
<a className="border-r-2 border-gray-300 px-3 text-sm"> <a className="border-r-2 border-brand-base px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}> <p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon ?? null} {icon ?? null}
{title} {title}

View File

@ -0,0 +1,45 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
// cmdk
import { Command } from "cmdk";
import { THEMES_OBJ } from "constants/themes";
import { useTheme } from "next-themes";
import { SettingIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
};
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
const [mounted, setMounted] = useState(false);
const { setTheme } = useTheme();
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
{THEMES_OBJ.map((theme) => (
<Command.Item
key={theme.value}
onSelect={() => {
setTheme(theme.value);
setIsPaletteOpen(false);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
{theme.label}
</div>
</Command.Item>
))}
</>
);
};

View File

@ -14,7 +14,7 @@ import stateService from "services/state.service";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// fetch keys // fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATE_LIST } from "constants/fetch-keys"; import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
// icons // icons
import { CheckIcon, getStateGroupIcon } from "components/icons"; import { CheckIcon, getStateGroupIcon } from "components/icons";
@ -28,7 +28,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR( const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string) ? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null : null

View File

@ -44,6 +44,7 @@ import {
ChangeIssueState, ChangeIssueState,
ChangeIssuePriority, ChangeIssuePriority,
ChangeIssueAssignee, ChangeIssueAssignee,
ChangeInterfaceTheme,
} from "components/command-palette"; } from "components/command-palette";
import { BulkDeleteIssuesModal } from "components/core"; import { BulkDeleteIssuesModal } from "components/core";
import { CreateUpdateCycleModal } from "components/cycles"; import { CreateUpdateCycleModal } from "components/cycles";
@ -379,7 +380,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" /> <div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20"> <div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
@ -392,7 +393,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-surface-2 shadow-2xl transition-all">
<Command <Command
filter={(value, search) => { filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1; if (value.toLowerCase().includes(search.toLowerCase())) return 1;
@ -415,7 +416,7 @@ export const CommandPalette: React.FC = () => {
> >
{issueId && issueDetails && ( {issueId && issueDetails && (
<div className="flex p-3"> <div className="flex p-3">
<p className="overflow-hidden truncate rounded-md bg-gray-100 p-1 px-2 text-xs font-medium text-gray-500"> <p className="overflow-hidden truncate rounded-md bg-brand-surface-1 p-1 px-2 text-xs font-medium text-brand-secondary">
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "} {issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails?.name} {issueDetails?.name}
</p> </p>
@ -423,11 +424,11 @@ export const CommandPalette: React.FC = () => {
)} )}
<div className="relative"> <div className="relative">
<MagnifyingGlassIcon <MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40" className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-secondary"
aria-hidden="true" aria-hidden="true"
/> />
<Command.Input <Command.Input
className="w-full border-0 border-b bg-transparent p-4 pl-11 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm" className="w-full border-0 border-b border-brand-base bg-transparent p-4 pl-11 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder={placeholder} placeholder={placeholder}
value={searchTerm} value={searchTerm}
onValueChange={(e) => { onValueChange={(e) => {
@ -441,7 +442,9 @@ export const CommandPalette: React.FC = () => {
resultsCount === 0 && resultsCount === 0 &&
searchTerm !== "" && searchTerm !== "" &&
debouncedSearchTerm !== "" && ( debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-gray-500">No results found.</div> <div className="my-4 text-center text-brand-secondary">
No results found.
</div>
)} )}
{(isLoading || isSearching) && ( {(isLoading || isSearching) && (
@ -502,8 +505,11 @@ export const CommandPalette: React.FC = () => {
value={value} value={value}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 overflow-hidden text-gray-700"> <div className="flex items-center gap-2 overflow-hidden text-brand-secondary">
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" /> <Icon
className="h-4 w-4 text-brand-secondary"
color="#6b7280"
/>
<p className="block flex-1 truncate">{item.name}</p> <p className="block flex-1 truncate">{item.name}</p>
</div> </div>
</Command.Item> </Command.Item>
@ -528,8 +534,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<Squares2X2Icon className="h-4 w-4 text-gray-500" /> <Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
Change state... Change state...
</div> </div>
</Command.Item> </Command.Item>
@ -541,8 +547,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<ChartBarIcon className="h-4 w-4 text-gray-500" /> <ChartBarIcon className="h-4 w-4 text-brand-secondary" />
Change priority... Change priority...
</div> </div>
</Command.Item> </Command.Item>
@ -554,8 +560,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<UsersIcon className="h-4 w-4 text-gray-500" /> <UsersIcon className="h-4 w-4 text-brand-secondary" />
Assign to... Assign to...
</div> </div>
</Command.Item> </Command.Item>
@ -566,15 +572,15 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
{issueDetails?.assignees.includes(user.id) ? ( {issueDetails?.assignees.includes(user.id) ? (
<> <>
<UserMinusIcon className="h-4 w-4 text-gray-500" /> <UserMinusIcon className="h-4 w-4 text-brand-secondary" />
Un-assign from me Un-assign from me
</> </>
) : ( ) : (
<> <>
<UserPlusIcon className="h-4 w-4 text-gray-500" /> <UserPlusIcon className="h-4 w-4 text-brand-secondary" />
Assign to me Assign to me
</> </>
)} )}
@ -582,8 +588,8 @@ export const CommandPalette: React.FC = () => {
</Command.Item> </Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none"> <Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<TrashIcon className="h-4 w-4 text-gray-500" /> <TrashIcon className="h-4 w-4 text-brand-secondary" />
Delete issue Delete issue
</div> </div>
</Command.Item> </Command.Item>
@ -594,16 +600,19 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<LinkIcon className="h-4 w-4 text-gray-500" /> <LinkIcon className="h-4 w-4 text-brand-secondary" />
Copy issue URL to clipboard Copy issue URL to clipboard
</div> </div>
</Command.Item> </Command.Item>
</> </>
)} )}
<Command.Group heading="Issue"> <Command.Group heading="Issue">
<Command.Item onSelect={createNewIssue} className="focus:bg-gray-200"> <Command.Item
<div className="flex items-center gap-2 text-gray-700"> onSelect={createNewIssue}
className="focus:bg-brand-surface-2"
>
<div className="flex items-center gap-2 text-brand-secondary">
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" /> <LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
Create new issue Create new issue
</div> </div>
@ -617,7 +626,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewProject} onSelect={createNewProject}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" /> <AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
Create new project Create new project
</div> </div>
@ -633,7 +642,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewCycle} onSelect={createNewCycle}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<ContrastIcon className="h-4 w-4" color="#6b7280" /> <ContrastIcon className="h-4 w-4" color="#6b7280" />
Create new cycle Create new cycle
</div> </div>
@ -646,7 +655,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewModule} onSelect={createNewModule}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" /> <PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
Create new module Create new module
</div> </div>
@ -656,7 +665,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="View"> <Command.Group heading="View">
<Command.Item onSelect={createNewView} className="focus:outline-none"> <Command.Item onSelect={createNewView} className="focus:outline-none">
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<ViewListIcon className="h-4 w-4" color="#6b7280" /> <ViewListIcon className="h-4 w-4" color="#6b7280" />
Create new view Create new view
</div> </div>
@ -666,7 +675,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="Page"> <Command.Group heading="Page">
<Command.Item onSelect={createNewPage} className="focus:outline-none"> <Command.Item onSelect={createNewPage} className="focus:outline-none">
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<DocumentTextIcon className="h-4 w-4" color="#6b7280" /> <DocumentTextIcon className="h-4 w-4" color="#6b7280" />
Create new page Create new page
</div> </div>
@ -685,7 +694,7 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4" color="#6b7280" /> <SettingIcon className="h-4 w-4" color="#6b7280" />
Search settings... Search settings...
</div> </div>
@ -696,11 +705,24 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewWorkspace} onSelect={createNewWorkspace}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<FolderPlusIcon className="h-4 w-4 text-gray-500" /> <FolderPlusIcon className="h-4 w-4 text-brand-secondary" />
Create new workspace Create new workspace
</div> </div>
</Command.Item> </Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages([...pages, "change-interface-theme"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Change interface theme...
</div>
</Command.Item>
</Command.Group> </Command.Group>
<Command.Group heading="Help"> <Command.Group heading="Help">
<Command.Item <Command.Item
@ -713,8 +735,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<RocketLaunchIcon className="h-4 w-4 text-gray-500" /> <RocketLaunchIcon className="h-4 w-4 text-brand-secondary" />
Open keyboard shortcuts Open keyboard shortcuts
</div> </div>
</Command.Item> </Command.Item>
@ -725,8 +747,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<DocumentIcon className="h-4 w-4 text-gray-500" /> <DocumentIcon className="h-4 w-4 text-brand-secondary" />
Open Plane documentation Open Plane documentation
</div> </div>
</Command.Item> </Command.Item>
@ -737,7 +759,7 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<DiscordIcon className="h-4 w-4" color="#6b7280" /> <DiscordIcon className="h-4 w-4" color="#6b7280" />
Join our Discord Join our Discord
</div> </div>
@ -752,7 +774,7 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<GithubIcon className="h-4 w-4" color="#6b7280" /> <GithubIcon className="h-4 w-4" color="#6b7280" />
Report a bug Report a bug
</div> </div>
@ -764,8 +786,8 @@ export const CommandPalette: React.FC = () => {
}} }}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" /> <ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" />
Chat with us Chat with us
</div> </div>
</Command.Item> </Command.Item>
@ -779,8 +801,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings()} onSelect={() => goToSettings()}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-gray-500" /> <SettingIcon className="h-4 w-4 text-brand-secondary" />
General General
</div> </div>
</Command.Item> </Command.Item>
@ -788,8 +810,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("members")} onSelect={() => goToSettings("members")}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-gray-500" /> <SettingIcon className="h-4 w-4 text-brand-secondary" />
Members Members
</div> </div>
</Command.Item> </Command.Item>
@ -797,8 +819,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("billing")} onSelect={() => goToSettings("billing")}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-gray-500" /> <SettingIcon className="h-4 w-4 text-brand-secondary" />
Billings and Plans Billings and Plans
</div> </div>
</Command.Item> </Command.Item>
@ -806,8 +828,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("integrations")} onSelect={() => goToSettings("integrations")}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-gray-500" /> <SettingIcon className="h-4 w-4 text-brand-secondary" />
Integrations Integrations
</div> </div>
</Command.Item> </Command.Item>
@ -815,8 +837,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("import-export")} onSelect={() => goToSettings("import-export")}
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-gray-500" /> <SettingIcon className="h-4 w-4 text-brand-secondary" />
Import/Export Import/Export
</div> </div>
</Command.Item> </Command.Item>
@ -842,6 +864,9 @@ export const CommandPalette: React.FC = () => {
setIsPaletteOpen={setIsPaletteOpen} setIsPaletteOpen={setIsPaletteOpen}
/> />
)} )}
{page === "change-interface-theme" && (
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
)}
</Command.List> </Command.List>
</Command> </Command>
</Dialog.Panel> </Dialog.Panel>

View File

@ -3,3 +3,4 @@ export * from "./shortcuts-modal";
export * from "./change-issue-state"; export * from "./change-issue-state";
export * from "./change-issue-priority"; export * from "./change-issue-priority";
export * from "./change-issue-assignee"; export * from "./change-issue-assignee";
export * from "./change-interface-theme";

View File

@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
// icons // icons
import { XMarkIcon } from "@heroicons/react/20/solid"; import { XMarkIcon } from "@heroicons/react/20/solid";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { MacCommandIcon } from "components/icons"; import { CommandIcon } from "components/icons";
// ui // ui
import { Input } from "components/ui"; import { Input } from "components/ui";
@ -71,7 +71,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto"> <div className="fixed inset-0 z-20 overflow-y-auto">
@ -85,29 +85,29 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-white p-5"> <div className="bg-brand-surface-2 p-5">
<div className="sm:flex sm:items-start"> <div className="sm:flex sm:items-start">
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left"> <div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="flex justify-between text-lg font-medium leading-6 text-gray-900" className="flex justify-between text-lg font-medium leading-6 text-brand-base"
> >
<span>Keyboard Shortcuts</span> <span>Keyboard Shortcuts</span>
<span> <span>
<button type="button" onClick={() => setIsOpen(false)}> <button type="button" onClick={() => setIsOpen(false)}>
<XMarkIcon <XMarkIcon
className="h-6 w-6 text-gray-400 hover:text-gray-500" className="h-6 w-6 text-gray-400 hover:text-brand-secondary"
aria-hidden="true" aria-hidden="true"
/> />
</button> </button>
</span> </span>
</Dialog.Title> </Dialog.Title>
<div> <div>
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-gray-200 bg-gray-100 px-3 py-2"> <div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-brand-base bg-brand-surface-1 px-3 py-2">
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-gray-500" /> <MagnifyingGlassIcon className="h-3.5 w-3.5 text-brand-secondary" />
<Input <Input
className="w-full border-none bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none" className="w-full border-none bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none"
id="search" id="search"
name="search" name="search"
type="text" type="text"
@ -123,17 +123,23 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div key={shortcut.keys} className="flex w-full flex-col"> <div key={shortcut.keys} className="flex w-full flex-col">
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-500">{shortcut.description}</p> <p className="text-sm text-brand-secondary">
{shortcut.description}
</p>
<div className="flex items-center gap-x-2.5"> <div className="flex items-center gap-x-2.5">
{shortcut.keys.split(",").map((key, index) => ( {shortcut.keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1"> <span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? ( {key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2"> <span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5">
<MacCommandIcon /> <CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</span> </span>
) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</kbd>
) : ( ) : (
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800"> <kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key === "Ctrl" ? <MacCommandIcon /> : key} {key}
</kbd> </kbd>
)} )}
</span> </span>
@ -145,7 +151,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
)) ))
) : ( ) : (
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
<p className="text-sm text-gray-500"> <p className="text-sm text-brand-secondary">
No shortcuts found for{" "} No shortcuts found for{" "}
<span className="font-semibold italic"> <span className="font-semibold italic">
{`"`} {`"`}
@ -162,17 +168,21 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
{shortcuts.map(({ keys, description }, index) => ( {shortcuts.map(({ keys, description }, index) => (
<div key={index} className="flex items-center justify-between"> <div key={index} className="flex items-center justify-between">
<p className="text-sm text-gray-500">{description}</p> <p className="text-sm text-brand-secondary">{description}</p>
<div className="flex items-center gap-x-2.5"> <div className="flex items-center gap-x-2.5">
{keys.split(",").map((key, index) => ( {keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1"> <span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? ( {key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2"> <span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-brand-secondary">
<MacCommandIcon /> <CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</span> </span>
) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</kbd>
) : ( ) : (
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800"> <kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key === "Ctrl" ? <MacCommandIcon /> : key} {key}
</kbd> </kbd>
)} )}
</span> </span>

View File

@ -17,7 +17,7 @@ type Props = {
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -81,7 +81,7 @@ export const AllBoards: React.FC<Props> = ({
return ( return (
<div <div
key={index} key={index}
className="flex items-center justify-between gap-2 rounded bg-white p-2 shadow" className="flex items-center justify-between gap-2 rounded bg-brand-surface-1 p-2 shadow"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{currentState && {currentState &&
@ -92,7 +92,7 @@ export const AllBoards: React.FC<Props> = ({
: addSpaceIfCamelCase(singleGroup)} : addSpaceIfCamelCase(singleGroup)}
</h4> </h4>
</div> </div>
<span className="text-xs text-gray-500">0</span> <span className="text-xs text-brand-secondary">0</span>
</div> </div>
); );
})} })}

View File

@ -96,17 +96,17 @@ export const BoardHeader: React.FC<Props> = ({
switch (selectedGroup) { switch (selectedGroup) {
case "state": case "state":
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor); icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
break; break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center"); icon = getPriorityIcon(groupTitle, "text-lg");
break; break;
case "labels": case "labels":
const labelColor = const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000"; issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = ( icon = (
<span <span
className="h-[18px] w-[18px] flex-shrink-0 rounded-full" className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: labelColor }} style={{ backgroundColor: labelColor }}
/> />
); );
@ -123,8 +123,8 @@ export const BoardHeader: React.FC<Props> = ({
return ( return (
<div <div
className={`flex justify-between items-center px-1 ${ className={`flex items-center justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : "" !isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : ""
}`} }`}
> >
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
@ -143,7 +143,9 @@ export const BoardHeader: React.FC<Props> = ({
{getGroupTitle()} {getGroupTitle()}
</h2> </h2>
<span <span
className={`${isCollapsed ? "ml-0.5" : ""} rounded-full bg-gray-100 py-1 px-3 text-sm`} className={`${
isCollapsed ? "ml-0.5" : ""
} min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs`}
> >
{groupedByIssues?.[groupTitle].length ?? 0} {groupedByIssues?.[groupTitle].length ?? 0}
</span> </span>
@ -153,7 +155,7 @@ export const BoardHeader: React.FC<Props> = ({
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button <button
type="button" type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100" className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
onClick={() => { onClick={() => {
setIsCollapsed((prevData) => !prevData); setIsCollapsed((prevData) => !prevData);
}} }}
@ -167,7 +169,7 @@ export const BoardHeader: React.FC<Props> = ({
{!isCompleted && ( {!isCompleted && (
<button <button
type="button" type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100" className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />

View File

@ -29,7 +29,7 @@ type Props = {
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -66,7 +66,7 @@ export const SingleBoard: React.FC<Props> = ({
}, [currentState]); }, [currentState]);
return ( return (
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}> <div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}> <div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
<BoardHeader <BoardHeader
addIssueToState={addIssueToState} addIssueToState={addIssueToState}
@ -81,7 +81,7 @@ export const SingleBoard: React.FC<Props> = ({
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className={`relative h-full overflow-y-auto p-1 ${ className={`relative h-full overflow-y-auto p-1 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : "" snapshot.isDraggingOver ? "bg-brand-base/20" : ""
} ${!isCollapsed ? "hidden" : "block"}`} } ${!isCollapsed ? "hidden" : "block"}`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
@ -91,15 +91,17 @@ export const SingleBoard: React.FC<Props> = ({
<div <div
className={`absolute ${ className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden" snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-gray-100 opacity-50`} } pointer-events-none top-0 left-0 z-[99] h-full w-full bg-brand-surface-1 opacity-50`}
/> />
<div <div
className={`absolute ${ className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden" snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-white p-2 text-xs`} } pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-brand-base p-2 text-xs`}
> >
This board is ordered by{" "} This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase(orderBy ?? "created_at")} {replaceUnderscoreIfSnakeCase(
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
)}
</div> </div>
</> </>
)} )}
@ -128,7 +130,8 @@ export const SingleBoard: React.FC<Props> = ({
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox} handleTrashBox={handleTrashBox}
removeIssue={() => { removeIssue={() => {
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id); if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
}} }}
isCompleted={isCompleted} isCompleted={isCompleted}
userAuth={userAuth} userAuth={userAuth}
@ -146,7 +149,7 @@ export const SingleBoard: React.FC<Props> = ({
{type === "issue" ? ( {type === "issue" ? (
<button <button
type="button" type="button"
className="flex items-center gap-2 font-medium text-theme outline-none" className="flex items-center gap-2 font-medium text-brand-accent outline-none"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
@ -158,7 +161,7 @@ export const SingleBoard: React.FC<Props> = ({
customButton={ customButton={
<button <button
type="button" type="button"
className="flex items-center gap-2 font-medium text-theme outline-none" className="flex items-center gap-2 font-medium text-brand-accent outline-none"
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue

View File

@ -261,8 +261,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
</a> </a>
</ContextMenu> </ContextMenu>
<div <div
className={`mb-3 rounded bg-white shadow ${ className={`mb-3 rounded bg-brand-base shadow ${
snapshot.isDragging ? "border-2 border-theme shadow-lg" : "" snapshot.isDragging ? "border-2 border-brand-accent shadow-lg" : ""
}`} }`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
@ -312,12 +312,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a> <a>
{properties.key && ( {properties.key && (
<div className="mb-2.5 text-xs font-medium text-gray-700"> <div className="mb-2.5 text-xs font-medium text-brand-secondary">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
)} )}
<h5 <h5
className="break-all text-sm group-hover:text-theme" className="break-all text-sm group-hover:text-brand-accent"
style={{ lineClamp: 3, WebkitLineClamp: 3 }} style={{ lineClamp: 3, WebkitLineClamp: 3 }}
> >
{truncateText(issue.name, 100)} {truncateText(issue.name, 100)}
@ -349,7 +349,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
/> />
)} )}
{properties.sub_issue_count && ( {properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm"> <div className="flex flex-shrink-0 items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div> </div>
)} )}
@ -358,7 +358,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{issue.label_details.map((label) => ( {issue.label_details.map((label) => (
<div <div
key={label.id} key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs" className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
> >
<span <span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full" className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
@ -389,20 +389,20 @@ export const SingleBoardIssue: React.FC<Props> = ({
/> />
)} )}
{properties.link && ( {properties.link && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-gray-500" /> <LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
{issue.link_count} {issue.link_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.attachment_count && ( {properties.attachment_count && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" /> <PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
{issue.attachment_count} {issue.attachment_count}
</div> </div>
</Tooltip> </Tooltip>

View File

@ -121,7 +121,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-brand-surface-2 shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<form> <form>
<Combobox <Combobox
onChange={(val: string) => { onChange={(val: string) => {
@ -136,12 +136,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
> >
<div className="relative m-1"> <div className="relative m-1">
<MagnifyingGlassIcon <MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40" className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
aria-hidden="true" aria-hidden="true"
/> />
<input <input
type="text" type="text"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm" className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..." placeholder="Search..."
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
/> />
@ -154,7 +154,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
{filteredIssues.length > 0 ? ( {filteredIssues.length > 0 ? (
<li className="p-2"> <li className="p-2">
{query === "" && ( {query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900"> <h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base">
Select issues to delete Select issues to delete
</h2> </h2>
)} )}
@ -166,7 +166,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
value={issue.id} value={issue.id}
className={({ active }) => className={({ active }) =>
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${ `flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" active ? "bg-gray-900 bg-opacity-5 text-brand-base" : ""
}` }`
} }
> >
@ -182,7 +182,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
backgroundColor: issue.state_detail.color, backgroundColor: issue.state_detail.color,
}} }}
/> />
<span className="flex-shrink-0 text-xs text-gray-500"> <span className="flex-shrink-0 text-xs text-brand-secondary">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</span> </span>
<span>{issue.name}</span> <span>{issue.name}</span>
@ -194,9 +194,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
) : ( ) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center"> <div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" /> <LayerDiagonalIcon height="56" width="56" />
<h3 className="text-gray-500"> <h3 className="text-brand-secondary">
No issues found. Create a new issue with{" "} No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-gray-200 px-2 py-1">C</pre>. <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
</h3> </h3>
</div> </div>
)} )}

View File

@ -26,14 +26,17 @@ import {
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CustomMenu } from "components/ui"; import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
// icon // icon
import { import {
CheckIcon, CheckIcon,
ChevronDownIcon, ChevronDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
PlusIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// hooks
import useIssuesView from "hooks/use-issues-view";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import cyclesService from "services/cycles.service"; import cyclesService from "services/cycles.service";
@ -48,20 +51,28 @@ import { IIssue } from "types";
// constant // constant
import { monthOptions, yearOptions } from "constants/calendar"; import { monthOptions, yearOptions } from "constants/calendar";
import modulesService from "services/modules.service"; import modulesService from "services/modules.service";
import { getStateGroupIcon } from "components/icons";
type Props = {
addIssueToDate: (date: string) => void;
};
interface ICalendarRange { interface ICalendarRange {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
} }
export const CalendarView = () => { export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false); const [showWeekEnds, setShowWeekEnds] = useState(false);
const [currentDate, setCurrentDate] = useState<Date>(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true); const [isMonthlyView, setIsMonthlyView] = useState(true);
const [showAllIssues, setShowAllIssues] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { params } = useIssuesView();
const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({ const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({
startDate: startOfWeek(currentDate), startDate: startOfWeek(currentDate),
endDate: lastDayOfWeek(currentDate), endDate: lastDayOfWeek(currentDate),
@ -77,11 +88,13 @@ export const CalendarView = () => {
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null, workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => ? () =>
issuesService.getIssuesWithParams( issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, {
workspaceSlug as string, ...params,
projectId as string, target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
targetDateFilter calendarDateRange.endDate
) )};before`,
group_by: null,
})
: null : null
); );
@ -95,7 +108,13 @@ export const CalendarView = () => {
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
cycleId as string, cycleId as string,
targetDateFilter {
...params,
target_date: `${renderDateFormat(
calendarDateRange.startDate
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
group_by: null,
}
) )
: null : null
); );
@ -110,7 +129,13 @@ export const CalendarView = () => {
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
moduleId as string, moduleId as string,
targetDateFilter {
...params,
target_date: `${renderDateFormat(
calendarDateRange.startDate
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
group_by: null,
}
) )
: null : null
); );
@ -127,12 +152,16 @@ export const CalendarView = () => {
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays; const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
const calendarIssues = cycleCalendarIssues ?? moduleCalendarIssues ?? projectCalendarIssues; const calendarIssues = cycleId
? (cycleCalendarIssues as IIssue[])
: moduleId
? (moduleCalendarIssues as IIssue[])
: (projectCalendarIssues as IIssue[]);
const currentViewDaysData = currentViewDays.map((date: Date) => { const currentViewDaysData = currentViewDays.map((date: Date) => {
const filterIssue = const filterIssue =
calendarIssues && calendarIssues.length > 0 calendarIssues && calendarIssues.length > 0
? (calendarIssues as IIssue[]).filter( ? calendarIssues.filter(
(issue) => (issue) =>
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date) issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
) )
@ -198,17 +227,17 @@ export const CalendarView = () => {
}); });
}; };
return ( return calendarIssues ? (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<div className="h-full overflow-y-auto rounded-lg text-gray-600 -m-2"> <div className="-m-2 h-full overflow-y-auto rounded-lg text-brand-secondary">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="relative flex h-full w-full gap-2 items-center justify-start text-sm "> <div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
<Popover className="flex h-full items-center justify-start rounded-lg"> <Popover className="flex h-full items-center justify-start rounded-lg">
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button className={`group flex h-full items-start gap-1 text-gray-800`}> <Popover.Button className={`group flex h-full items-start gap-1 text-brand-base`}>
<div className="flex items-center justify-center gap-2 text-2xl font-semibold"> <div className="flex items-center justify-center gap-2 text-2xl font-semibold">
<span className="text-black">{formatDate(currentDate, "Month")}</span>{" "} <span>{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "yyyy")}</span> <span>{formatDate(currentDate, "yyyy")}</span>
</div> </div>
</Popover.Button> </Popover.Button>
@ -222,30 +251,30 @@ export const CalendarView = () => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute top-10 left-0 z-20 w-full max-w-xs flex flex-col transform overflow-hidden bg-white shadow-lg rounded-[10px]"> <Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
<div className="flex justify-center items-center text-sm gap-5 px-2 py-2"> <div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
{yearOptions.map((year) => ( {yearOptions.map((year) => (
<button <button
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))} onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
className={` ${ className={` ${
isSameYear(year.value, currentDate) isSameYear(year.value, currentDate)
? "text-sm font-medium text-gray-800" ? "text-sm font-medium text-brand-base"
: "text-xs text-gray-400 " : "text-xs text-brand-secondary "
} hover:text-sm hover:text-gray-800 hover:font-medium `} } hover:text-sm hover:font-medium hover:text-brand-base`}
> >
{year.label} {year.label}
</button> </button>
))} ))}
</div> </div>
<div className="grid grid-cols-4 px-2 border-t border-gray-200"> <div className="grid grid-cols-4 border-t border-brand-base px-2">
{monthOptions.map((month) => ( {monthOptions.map((month) => (
<button <button
onClick={() => onClick={() =>
updateDate(updateDateWithMonth(month.value, currentDate)) updateDate(updateDateWithMonth(month.value, currentDate))
} }
className={`text-gray-400 text-xs px-2 py-2 hover:font-medium hover:text-gray-800 ${ className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${
isSameMonth(month.value, currentDate) isSameMonth(month.value, currentDate)
? "font-medium text-gray-800" ? "font-medium text-brand-base"
: "" : ""
}`} }`}
> >
@ -295,9 +324,9 @@ export const CalendarView = () => {
</div> </div>
</div> </div>
<div className="flex w-full gap-2 items-center justify-end"> <div className="flex w-full items-center justify-end gap-2">
<button <button
className="group flex cursor-pointer items-center gap-2 rounded-md border bg-white px-4 py-1.5 text-sm hover:bg-gray-100 hover:text-gray-900 focus:outline-none" className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
onClick={() => { onClick={() => {
if (isMonthlyView) { if (isMonthlyView) {
updateDate(new Date()); updateDate(new Date());
@ -310,13 +339,12 @@ export const CalendarView = () => {
} }
}} }}
> >
Today{" "} Today
</button> </button>
<CustomMenu <CustomMenu
customButton={ customButton={
<div <div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
className={`group flex cursor-pointer items-center gap-2 rounded-md border bg-white px-3 py-1.5 text-sm hover:bg-gray-100 hover:text-gray-900 focus:outline-none `}
>
{isMonthlyView ? "Monthly" : "Weekly"} {isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" /> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div> </div>
@ -330,7 +358,7 @@ export const CalendarView = () => {
endDate: lastDayOfWeek(currentDate), endDate: lastDayOfWeek(currentDate),
}); });
}} }}
className="w-52 text-sm text-gray-600" className="w-52 text-sm text-brand-secondary"
> >
<div className="flex w-full max-w-[260px] items-center justify-between gap-2"> <div className="flex w-full max-w-[260px] items-center justify-between gap-2">
<span className="flex items-center gap-2">Monthly View</span> <span className="flex items-center gap-2">Monthly View</span>
@ -349,7 +377,7 @@ export const CalendarView = () => {
endDate: getCurrentWeekEndDate(currentDate), endDate: getCurrentWeekEndDate(currentDate),
}); });
}} }}
className="w-52 text-sm text-gray-600" className="w-52 text-sm text-brand-secondary"
> >
<div className="flex w-full items-center justify-between gap-2"> <div className="flex w-full items-center justify-between gap-2">
<span className="flex items-center gap-2">Weekly View</span> <span className="flex items-center gap-2">Weekly View</span>
@ -360,25 +388,12 @@ export const CalendarView = () => {
/> />
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<div className="mt-1 flex w-52 items-center justify-between border-t border-gray-200 py-2 px-1 text-sm text-gray-600"> <div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
<h4>Show weekends</h4> <h4>Show weekends</h4>
<button <ToggleSwitch
type="button" value={showWeekEnds}
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${ onChange={() => setShowWeekEnds(!showWeekEnds)}
showWeekEnds ? "bg-green-500" : "bg-gray-200" />
}`}
role="switch"
aria-checked={showWeekEnds}
onClick={() => setShowWeekEnds(!showWeekEnds)}
>
<span className="sr-only">Show weekends</span>
<span
aria-hidden="true"
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
showWeekEnds ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
</div> </div>
</CustomMenu> </CustomMenu>
</div> </div>
@ -392,7 +407,7 @@ export const CalendarView = () => {
{weeks.map((date, index) => ( {weeks.map((date, index) => (
<div <div
key={index} key={index}
className={`flex items-center justify-start p-1.5 gap-2 border-gray-300 bg-gray-100 text-base font-medium text-gray-600 ${ className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-brand-secondary ${
!isMonthlyView !isMonthlyView
? showWeekEnds ? showWeekEnds
? (index + 1) % 7 === 0 ? (index + 1) % 7 === 0
@ -417,54 +432,93 @@ export const CalendarView = () => {
showWeekEnds ? "grid-cols-7" : "grid-cols-5" showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `} } `}
> >
{currentViewDaysData.map((date, index) => ( {currentViewDaysData.map((date, index) => {
<StrictModeDroppable droppableId={date.date}> const totalIssues = date.issues.length;
{(provided, snapshot) => (
<div return (
key={index} <StrictModeDroppable droppableId={date.date}>
ref={provided.innerRef} {(provided) => (
{...provided.droppableProps} <div
className={`flex flex-col gap-1.5 border-t border-gray-300 p-2.5 text-left text-sm font-medium hover:bg-gray-100 ${ key={index}
showWeekEnds ref={provided.innerRef}
? (index + 1) % 7 === 0 {...provided.droppableProps}
className={`group relative flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? "" ? ""
: "border-r" : "border-r"
: (index + 1) % 5 === 0 }`}
? "" >
: "border-r" {isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
}`} {totalIssues > 0 &&
> date.issues
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>} .slice(0, showAllIssues ? totalIssues : 4)
{date.issues.length > 0 && .map((issue: IIssue, index) => (
date.issues.map((issue: IIssue, index) => ( <Draggable draggableId={issue.id} index={index}>
<Draggable draggableId={issue.id} index={index}> {(provided, snapshot) => (
{(provided, snapshot) => ( <div
<div key={index}
key={index} ref={provided.innerRef}
ref={provided.innerRef} {...provided.draggableProps}
{...provided.draggableProps} {...provided.dragHandleProps}
{...provided.dragHandleProps} className={`w-full cursor-pointer truncate rounded border border-brand-base px-1.5 py-1 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
className={`w-full cursor-pointer truncate rounded bg-white p-1.5 hover:scale-105 ${ snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
snapshot.isDragging ? "shadow-lg" : "" }`}
}`} >
> <Link
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`} >
className="w-full" <a className="flex w-full items-center gap-2">
> {getStateGroupIcon(
{issue.name} issue.state_detail.group,
</Link> "12",
</div> "12",
)} issue.state_detail.color
</Draggable> )}
))} {issue.name}
{provided.placeholder} </a>
</div> </Link>
)} </div>
</StrictModeDroppable> )}
))} </Draggable>
))}
{totalIssues > 4 && (
<button
type="button"
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
onClick={() => setShowAllIssues((prevData) => !prevData)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
</button>
)}
<div
className={`absolute ${
isMonthlyView ? "bottom-2" : "top-2"
} right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
>
<button
className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)}
>
<PlusIcon className="h-3 w-3 text-brand-secondary" />
Add issue
</button>
</div>
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
);
})}
</div> </div>
</div> </div>
</DragDropContext> </DragDropContext>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
); );
}; };

View File

@ -0,0 +1,267 @@
import { useEffect, useState } from "react";
// react-hook-form
import { useForm } from "react-hook-form";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
const defaultValues = {
palette: "",
};
export const ThemeForm: React.FC<any> = ({ handleFormSubmit, handleClose, status, data }) => {
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
} = useForm<any>({
defaultValues,
});
const [darkPalette, setDarkPalette] = useState(false);
const handleUpdateTheme = async (formData: any) => {
await handleFormSubmit({ ...formData, darkPalette });
reset({
...defaultValues,
});
};
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, reset]);
// --color-bg-base: 25, 27, 27;
// --color-bg-surface-1: 31, 32, 35;
// --color-bg-surface-2: 39, 42, 45;
// --color-border: 46, 50, 52;
// --color-bg-sidebar: 19, 20, 22;
// --color-accent: 60, 133, 217;
// --color-text-base: 255, 255, 255;
// --color-text-secondary: 142, 148, 146;
return (
<form onSubmit={handleSubmit(handleUpdateTheme)}>
<div className="space-y-5">
<h3 className="text-lg font-medium leading-6 text-brand-base">Customize your theme</h3>
<div className="space-y-4">
<div className="mt-6 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-6">
<div className="sm:col-span-2">
<Input
id="bgBase"
label="Background"
name="bgBase"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.bgBase}
register={register}
validations={{
required: "Background color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Background color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="bgSurface1"
label="Background surface 1"
name="bgSurface1"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.bgSurface1}
register={register}
validations={{
required: "Background surface 1 color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Background surface 1 color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="bgSurface2"
label="Background surface 2"
name="bgSurface1"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.bgSurface1}
register={register}
validations={{
required: "Background surface 2 color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Background surface 2 color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="border"
label="Border"
name="border"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.border}
register={register}
validations={{
required: "Border color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Border color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="sidebar"
label="Sidebar"
name="sidebar"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.sidebar}
register={register}
validations={{
required: "Sidebar color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Sidebar color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="accent"
label="Accent"
name="accent"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.accent}
register={register}
validations={{
required: "Accent color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Accent color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-3">
<Input
id="textBase"
label="Text primary"
name="textBase"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.textBase}
register={register}
validations={{
required: "Text primary color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Text primary color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-3">
<Input
id="textSecondary"
label="Text secondary"
name="textSecondary"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.textSecondary}
register={register}
validations={{
required: "Text secondary color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Text secondary color should be hex format",
},
}}
/>
</div>
</div>
<div>
<Input
id="palette"
label="All colors"
name="palette"
type="name"
placeholder="Enter comma separated hex colors"
autoComplete="off"
error={errors.palette}
register={register}
validations={{
required: "Color values is required",
pattern: {
value: /^(#(?:[0-9a-fA-F]{3}){1,2},){7}#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Color values should be hex format, separated by commas",
},
}}
/>
</div>
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setDarkPalette((prevData) => !prevData)}
>
<span className="text-xs">Dark palette</span>
<button
type="button"
className={`pointer-events-none relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent ${
darkPalette ? "bg-brand-accent" : "bg-gray-300"
} transition-colors duration-300 ease-in-out focus:outline-none`}
role="switch"
aria-checked="false"
>
<span className="sr-only">Dark palette</span>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-3 w-3 ${
darkPalette ? "translate-x-3" : "translate-x-0"
} transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-300 ease-in-out`}
/>
</button>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{status
? isSubmitting
? "Updating Theme..."
: "Update Theme"
: isSubmitting
? "Creating Theme..."
: "Set Theme"}
</PrimaryButton>
</div>
</form>
);
};

View File

@ -0,0 +1,65 @@
import React from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// components
import { ThemeForm } from "./custom-theme-form";
// helpers
import { applyTheme } from "helpers/theme.helper";
// fetch-keys
type Props = {
isOpen: boolean;
handleClose: () => void;
};
export const CustomThemeModal: React.FC<Props> = ({ isOpen, handleClose }) => {
const onClose = () => {
handleClose();
};
const handleFormSubmit = async (formData: any) => {
applyTheme(formData.palette, formData.darkPalette);
onClose();
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<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-20 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-brand-surface-1 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<ThemeForm
handleClose={handleClose}
handleFormSubmit={handleFormSubmit}
status={false}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -117,7 +117,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20"> <div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
@ -130,7 +130,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
<form> <form>
<Controller <Controller
control={control} control={control}
@ -139,11 +139,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
<Combobox as="div" {...field} multiple> <Combobox as="div" {...field} multiple>
<div className="relative m-1"> <div className="relative m-1">
<MagnifyingGlassIcon <MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40" className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
aria-hidden="true" aria-hidden="true"
/> />
<Combobox.Input <Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm" className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..." placeholder="Search..."
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
/> />
@ -151,26 +151,26 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
<Combobox.Options <Combobox.Options
static static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto" className="max-h-80 scroll-py-2 divide-y divide-brand-base overflow-y-auto"
> >
{filteredIssues.length > 0 ? ( {filteredIssues.length > 0 ? (
<li className="p-2"> <li className="p-2">
{query === "" && ( {query === "" && (
<h2 className="mb-2 px-3 text-xs font-semibold text-gray-900"> <h2 className="mb-2 px-3 text-xs font-medium text-brand-base">
Select issues to add Select issues to add
</h2> </h2>
)} )}
<ul className="text-sm text-gray-700"> <ul className="text-sm text-brand-base">
{filteredIssues.map((issue) => ( {filteredIssues.map((issue) => (
<Combobox.Option <Combobox.Option
key={issue.id} key={issue.id}
as="label" as="label"
htmlFor={`issue-${issue.id}`} htmlFor={`issue-${issue.id}`}
value={issue.id} value={issue.id}
className={({ active }) => className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${ `flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" active ? "bg-brand-surface-2 text-brand-base" : ""
}` } ${selected ? "text-brand-base" : ""}`
} }
> >
{({ selected }) => ( {({ selected }) => (
@ -182,7 +182,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
backgroundColor: issue.state_detail.color, backgroundColor: issue.state_detail.color,
}} }}
/> />
<span className="flex-shrink-0 text-xs text-gray-500"> <span className="flex-shrink-0 text-xs">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</span> </span>
{issue.name} {issue.name}
@ -194,10 +194,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
</li> </li>
) : ( ) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center"> <div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" /> <LayerDiagonalIcon height="52" width="52" />
<h3 className="text-gray-500"> <h3 className="text-sm text-brand-secondary">
No issues found. Create a new issue with{" "} No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-gray-200 px-2 py-1">C</pre>. <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>
.
</h3> </h3>
</div> </div>
)} )}

View File

@ -55,43 +55,47 @@ const activityDetails: {
}, },
modules: { modules: {
message: "set the module to", message: "set the module to",
icon: <RectangleGroupIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <RectangleGroupIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
}, },
state: { state: {
message: "set the state to", message: "set the state to",
icon: <Squares2X2Icon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <Squares2X2Icon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
}, },
priority: { priority: {
message: "set the priority to", message: "set the priority to",
icon: <ChartBarIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <ChartBarIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
}, },
name: { name: {
message: "set the name to", message: "set the name to",
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: (
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
),
}, },
description: { description: {
message: "updated the description.", message: "updated the description.",
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: (
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
),
}, },
estimate_point: { estimate_point: {
message: "set the estimate point to", message: "set the estimate point to",
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />, icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
}, },
target_date: { target_date: {
message: "set the due date to", message: "set the due date to",
icon: <CalendarDaysIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <CalendarDaysIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
}, },
parent: { parent: {
message: "set the parent to", message: "set the parent to",
icon: <UserIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <UserIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
}, },
issue: { issue: {
message: "deleted the issue.", message: "deleted the issue.",
icon: <TrashIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <TrashIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
}, },
estimate: { estimate: {
message: "updated the estimate", message: "updated the estimate",
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />, icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
}, },
link: { link: {
message: "updated the link", message: "updated the link",
@ -153,11 +157,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
) { ) {
const { workspace_detail, project, issue } = activity; const { workspace_detail, project, issue } = activity;
value = ( value = (
<span className="text-gray-600"> <span className="text-brand-secondary">
created{" "} created{" "}
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}> <Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
<a className="inline-flex items-center hover:underline"> <a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="h-3.5 w-3.5 ml-1" /> this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a> </a>
</Link> </Link>
</span> </span>
@ -198,7 +202,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
if (activity.field === "comment") { if (activity.field === "comment") {
return ( return (
<div key={activity.id}> <div key={activity.id} className="mt-2">
<div className="relative flex items-start space-x-3"> <div className="relative flex items-start space-x-3">
<div className="relative px-1"> <div className="relative px-1">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( {activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
@ -217,9 +221,9 @@ export const Feeds: React.FC<any> = ({ activities }) => (
</div> </div>
)} )}
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-white px-0.5 py-px"> <span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
<ChatBubbleLeftEllipsisIcon <ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-gray-400" className="h-3.5 w-3.5 text-brand-secondary"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@ -230,7 +234,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
{activity.actor_detail.first_name} {activity.actor_detail.first_name}
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name} {activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
</div> </div>
<p className="mt-0.5 text-xs text-gray-500"> <p className="mt-0.5 text-xs text-brand-secondary">
Commented {timeAgo(activity.created_at)} Commented {timeAgo(activity.created_at)}
</p> </p>
</div> </div>
@ -242,9 +246,8 @@ export const Feeds: React.FC<any> = ({ activities }) => (
: activity.old_value : activity.old_value
} }
editable={false} editable={false}
onBlur={() => ({})}
noBorder noBorder
customClassName="text-xs bg-gray-100" customClassName="text-xs border border-brand-base bg-brand-base"
/> />
</div> </div>
</div> </div>
@ -259,7 +262,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div className="relative pb-1"> <div className="relative pb-1">
{activities.length > 1 && activityIdx !== activities.length - 1 ? ( {activities.length > 1 && activityIdx !== activities.length - 1 ? (
<span <span
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-gray-200" className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-brand-surface-2"
aria-hidden="true" aria-hidden="true"
/> />
) : null} ) : null}
@ -268,7 +271,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div> <div>
<div className="relative px-1.5"> <div className="relative px-1.5">
<div className="mt-1.5"> <div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 ring-white"> <div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-2 ring-white">
{activity.field ? ( {activity.field ? (
activityDetails[activity.field as keyof typeof activityDetails]?.icon activityDetails[activity.field as keyof typeof activityDetails]?.icon
) : activity.actor_detail.avatar && ) : activity.actor_detail.avatar &&
@ -292,7 +295,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
</div> </div>
</div> </div>
<div className="min-w-0 flex-1 py-3"> <div className="min-w-0 flex-1 py-3">
<div className="text-xs text-gray-500"> <div className="text-xs text-brand-secondary">
<span className="text-gray font-medium"> <span className="text-gray font-medium">
{activity.actor_detail.first_name} {activity.actor_detail.first_name}
{activity.actor_detail.is_bot {activity.actor_detail.is_bot
@ -300,7 +303,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
: " " + activity.actor_detail.last_name} : " " + activity.actor_detail.last_name}
</span> </span>
<span> {action} </span> <span> {action} </span>
<span className="text-xs font-medium text-gray-900"> {value} </span> <span className="text-xs font-medium text-brand-base"> {value} </span>
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span> <span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@ import issuesService from "services/issues.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
import stateService from "services/state.service"; import stateService from "services/state.service";
// types // types
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
import { IIssueFilterOptions } from "types"; import { IIssueFilterOptions } from "types";
export const FilterList: React.FC<any> = ({ filters, setFilters }) => { export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
@ -37,7 +37,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
); );
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string) ? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null : null
@ -57,9 +57,9 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return ( return (
<div <div
key={key} key={key}
className="flex items-center gap-x-2 rounded-full border bg-white px-2 py-1" className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
> >
<span className="font-medium capitalize text-gray-500"> <span className="capitalize text-brand-secondary">
{replaceUnderscoreIfSnakeCase(key)}: {replaceUnderscoreIfSnakeCase(key)}:
</span> </span>
{filters[key as keyof IIssueFilterOptions] === null || {filters[key as keyof IIssueFilterOptions] === null ||
@ -75,7 +75,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return ( return (
<p <p
key={state?.id} key={state?.id}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium text-white" className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
style={{ style={{
color: state?.color, color: state?.color,
backgroundColor: `${state?.color}20`, backgroundColor: `${state?.color}20`,
@ -122,16 +122,16 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
{filters.priority?.map((priority: any) => ( {filters.priority?.map((priority: any) => (
<p <p
key={priority} key={priority}
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium capitalize text-white ${ className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
priority === "urgent" priority === "urgent"
? "bg-red-100 text-red-600 hover:bg-red-100" ? "bg-red-500/20 text-red-500"
: priority === "high" : priority === "high"
? "bg-orange-100 text-orange-500 hover:bg-orange-100" ? "bg-orange-500/20 text-orange-500"
: priority === "medium" : priority === "medium"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100" ? "bg-yellow-500/20 text-yellow-500"
: priority === "low" : priority === "low"
? "bg-green-100 text-green-500 hover:bg-green-100" ? "bg-green-500/20 text-green-500"
: "bg-gray-100 text-gray-700 hover:bg-gray-100" : "bg-brand-surface-1 text-brand-secondary"
}`} }`}
> >
<span>{getPriorityIcon(priority)}</span> <span>{getPriorityIcon(priority)}</span>
@ -170,7 +170,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return ( return (
<div <div
key={memberId} key={memberId}
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize" className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
> >
<Avatar user={member} /> <Avatar user={member} />
<span>{member?.first_name}</span> <span>{member?.first_name}</span>
@ -203,7 +203,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
<XMarkIcon className="h-3 w-3" /> <XMarkIcon className="h-3 w-3" />
</button> </button>
</div> </div>
) : (key as keyof IIssueFilterOptions) === "created_by" ? ( ) : key === "created_by" ? (
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
{filters.created_by?.map((memberId: string) => { {filters.created_by?.map((memberId: string) => {
const member = members?.find((m) => m.member.id === memberId)?.member; const member = members?.find((m) => m.member.id === memberId)?.member;
@ -211,7 +211,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return ( return (
<div <div
key={`${memberId}-${key}`} key={`${memberId}-${key}`}
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize" className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
> >
<Avatar user={member} /> <Avatar user={member} />
<span>{member?.first_name}</span> <span>{member?.first_name}</span>
@ -253,25 +253,20 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
const color = label.color !== "" ? label.color : "#0f172a"; const color = label.color !== "" ? label.color : "#0f172a";
return ( return (
<div <div
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium" className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
style={{ style={{
background: `${color}33`, // add 20% opacity color: color,
backgroundColor: `${color}20`, // add 20% opacity
}} }}
key={labelId} key={labelId}
> >
<div <div
className="h-2 w-2 rounded-full" className="h-1.5 w-1.5 rounded-full"
style={{ style={{
backgroundColor: color, backgroundColor: color,
}} }}
/> />
<span <span>{label.name}</span>
style={{
color: color,
}}
>
{label.name}
</span>
<span <span
className="cursor-pointer" className="cursor-pointer"
onClick={() => onClick={() =>
@ -339,10 +334,10 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
created_by: null, created_by: null,
}) })
} }
className="flex items-center gap-x-1 rounded-full border bg-white px-3 py-1.5 text-xs" className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
> >
<span className="font-medium">Clear all filters</span> <span>Clear all filters</span>
<XMarkIcon className="h-4 w-4" /> <XMarkIcon className="h-3 w-3" />
</button> </button>
)} )}
</div> </div>

View File

@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC<Props> = ({
return ( return (
<div <div
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border bg-white p-4 shadow ${ className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-brand-base bg-brand-surface-2 p-4 shadow ${
isOpen ? "block" : "hidden" isOpen ? "block" : "hidden"
}`} }`}
> >

View File

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

View File

@ -110,7 +110,7 @@ export const ImageUploadModal: React.FC<Props> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto"> <div className="fixed inset-0 z-30 overflow-y-auto">
@ -124,9 +124,9 @@ export const ImageUploadModal: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white 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 bg-brand-surface-2 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"> <div className="space-y-5">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
Upload Image Upload Image
</Dialog.Title> </Dialog.Title>
<div className="space-y-3"> <div className="space-y-3">
@ -135,7 +135,7 @@ export const ImageUploadModal: React.FC<Props> = ({
{...getRootProps()} {...getRootProps()}
className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${ className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
(image === null && isDragActive) || !value (image === null && isDragActive) || !value
? "border-2 border-dashed border-gray-300 hover:border-gray-400" ? "border-2 border-dashed border-brand-base hover:border-gray-400"
: "" : ""
}`} }`}
> >
@ -143,7 +143,7 @@ export const ImageUploadModal: React.FC<Props> = ({
<> <>
<button <button
type="button" type="button"
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600" className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-brand-surface-1 px-2 py-0.5 text-xs font-medium text-gray-600"
> >
Edit Edit
</button> </button>
@ -157,7 +157,7 @@ export const ImageUploadModal: React.FC<Props> = ({
) : ( ) : (
<> <>
<UserCircleIcon className="mx-auto h-16 w-16 text-gray-400" /> <UserCircleIcon className="mx-auto h-16 w-16 text-gray-400" />
<span className="mt-2 block text-sm font-medium text-gray-900"> <span className="mt-2 block text-sm font-medium text-brand-base">
{isDragActive {isDragActive
? "Drop image here to upload" ? "Drop image here to upload"
: "Drag & drop image here"} : "Drag & drop image here"}

View File

@ -11,3 +11,4 @@ export * from "./link-modal";
export * from "./image-picker-popover"; export * from "./image-picker-popover";
export * from "./filter-list"; export * from "./filter-list";
export * from "./feeds"; export * from "./feeds";
export * from "./theme-switch";

View File

@ -10,10 +10,14 @@ import { Popover, Transition } from "@headlessui/react";
// components // components
import { SelectFilters } from "components/views"; import { SelectFilters } from "components/views";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu, ToggleSwitch } from "components/ui";
// icons // icons
import { ChevronDownIcon, ListBulletIcon, CalendarDaysIcon } from "@heroicons/react/24/outline"; import {
import { Squares2X2Icon } from "@heroicons/react/20/solid"; ChevronDownIcon,
ListBulletIcon,
Squares2X2Icon,
CalendarDaysIcon,
} from "@heroicons/react/24/outline";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types // types
@ -53,30 +57,30 @@ export const IssuesFilterView: React.FC = () => {
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<button <button
type="button" type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${ className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
issueView === "list" ? "bg-gray-200" : "" issueView === "list" ? "bg-brand-surface-2" : ""
}`} }`}
onClick={() => setIssueView("list")} onClick={() => setIssueView("list")}
> >
<ListBulletIcon className="h-4 w-4" /> <ListBulletIcon className="h-4 w-4 text-brand-secondary" />
</button> </button>
<button <button
type="button" type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${ className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
issueView === "kanban" ? "bg-gray-200" : "" issueView === "kanban" ? "bg-brand-surface-2" : ""
}`} }`}
onClick={() => setIssueView("kanban")} onClick={() => setIssueView("kanban")}
> >
<Squares2X2Icon className="h-4 w-4" /> <Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
</button> </button>
<button <button
type="button" type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${ className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
issueView === "calendar" ? "bg-gray-200" : "" issueView === "calendar" ? "bg-brand-surface-2" : ""
}`} }`}
onClick={() => setIssueView("calendar")} onClick={() => setIssueView("calendar")}
> >
<CalendarDaysIcon className="h-4 w-4" /> <CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
</button> </button>
</div> </div>
<SelectFilters <SelectFilters
@ -113,8 +117,8 @@ export const IssuesFilterView: React.FC = () => {
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
className={`group flex items-center gap-2 rounded-md border bg-transparent px-3 py-1.5 text-xs hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${ className={`group flex items-center gap-2 rounded-md border border-brand-base bg-transparent px-3 py-1.5 text-xs hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none ${
open ? "bg-gray-100 text-gray-900" : "text-gray-500" open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
}`} }`}
> >
View View
@ -130,55 +134,59 @@ export const IssuesFilterView: React.FC = () => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg"> <Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg">
<div className="relative divide-y-2"> <div className="relative divide-y-2 divide-brand-base">
<div className="space-y-4 pb-3 text-xs"> <div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Group by</h4>
<CustomMenu
label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
width="lg"
>
{GROUP_BY_OPTIONS.map((option) =>
issueView === "kanban" && option.key === null ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Order by</h4>
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="lg"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
</>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-gray-600">Group by</h4> <h4 className="text-brand-secondary">Issue type</h4>
<CustomMenu
label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
"Select"
}
width="lg"
>
{GROUP_BY_OPTIONS.map((option) =>
issueView === "kanban" && option.key === null ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-gray-600">Order by</h4>
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="lg"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-gray-600">Issue type</h4>
<CustomMenu <CustomMenu
label={ label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type) FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
@ -200,62 +208,56 @@ export const IssuesFilterView: React.FC = () => {
))} ))}
</CustomMenu> </CustomMenu>
</div> </div>
<div className="flex items-center justify-between">
<h4 className="text-gray-600">Show empty states</h4>
<button
type="button"
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
showEmptyGroups ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked={showEmptyGroups}
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
>
<span className="sr-only">Show empty groups</span>
<span
aria-hidden="true"
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
</div>
<div className="relative flex justify-end gap-x-3">
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
</button>
<button
type="button"
className="font-medium text-theme"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</div>
</div>
<div className="space-y-2 py-3">
<h4 className="text-sm text-gray-600">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
return ( {issueView !== "calendar" && (
<button <>
key={key} <div className="flex items-center justify-between">
type="button" <h4 className="text-brand-secondary">Show empty states</h4>
className={`rounded border px-2 py-1 text-xs capitalize ${ <ToggleSwitch
properties[key as keyof Properties] value={showEmptyGroups}
? "border-theme bg-theme text-white" onChange={() => setShowEmptyGroups(!showEmptyGroups)}
: "border-gray-300" />
}`} </div>
onClick={() => setProperties(key as keyof Properties)} <div className="relative flex justify-end gap-x-3">
> <button type="button" onClick={() => resetFilterToDefault()}>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)} Reset to default
</button> </button>
); <button
})} type="button"
</div> className="font-medium text-brand-accent"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</div>
</>
)}
</div> </div>
{issueView !== "calendar" && (
<div className="space-y-2 py-3">
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
return (
<button
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-brand-accent bg-brand-accent text-white"
: "border-brand-base"
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
</div>
</div>
)}
</div> </div>
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>

View File

@ -46,7 +46,7 @@ import {
MODULE_DETAILS, MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
STATE_LIST, STATES_LIST,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// image // image
@ -103,7 +103,7 @@ export const IssuesView: React.FC<Props> = ({
} = useIssuesView(); } = useIssuesView();
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string) ? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null : null
@ -280,6 +280,17 @@ export const IssuesView: React.FC<Props> = ({
[setCreateIssueModal, setPreloadedData, selectedGroup] [setCreateIssueModal, setPreloadedData, selectedGroup]
); );
const addIssueToDate = useCallback(
(date: string) => {
setCreateIssueModal(true);
setPreloadedData({
target_date: date,
actionType: "createIssue",
});
},
[setCreateIssueModal, setPreloadedData]
);
const makeIssueCopy = useCallback( const makeIssueCopy = useCallback(
(issue: IIssue) => { (issue: IIssue) => {
setCreateIssueModal(true); setCreateIssueModal(true);
@ -303,10 +314,26 @@ export const IssuesView: React.FC<Props> = ({
); );
const removeIssueFromCycle = useCallback( const removeIssueFromCycle = useCallback(
(bridgeId: string) => { (bridgeId: string, issueId: string) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); mutate(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
}
return filteredData;
} else {
const filteredData = prevData.filter((i: any) => i.id !== issueId);
return filteredData;
}
},
false
);
issuesService issuesService
.removeIssueFromCycle( .removeIssueFromCycle(
@ -315,8 +342,12 @@ export const IssuesView: React.FC<Props> = ({
cycleId as string, cycleId as string,
bridgeId bridgeId
) )
.then((res) => { .then(() => {
console.log(res); setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
}) })
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
@ -326,10 +357,26 @@ export const IssuesView: React.FC<Props> = ({
); );
const removeIssueFromModule = useCallback( const removeIssueFromModule = useCallback(
(bridgeId: string) => { (bridgeId: string, issueId: string) => {
if (!workspaceSlug || !projectId || !moduleId) return; if (!workspaceSlug || !projectId || !moduleId) return;
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); mutate(
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
}
return filteredData;
} else {
const filteredData = prevData.filter((item: any) => item.id !== issueId);
return filteredData;
}
},
false
);
modulesService modulesService
.removeIssueFromModule( .removeIssueFromModule(
@ -338,8 +385,12 @@ export const IssuesView: React.FC<Props> = ({
moduleId as string, moduleId as string,
bridgeId bridgeId
) )
.then((res) => { .then(() => {
console.log(res); setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
}) })
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
@ -391,49 +442,48 @@ export const IssuesView: React.FC<Props> = ({
handleClose={() => setTransferIssuesModal(false)} handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal} isOpen={transferIssuesModal}
/> />
{issueView !== "calendar" && ( <>
<> <div
<div className={`flex items-center justify-between gap-2 ${
className={`flex items-center justify-between gap-2 ${ issueView === "list" ? (areFiltersApplied ? "mt-6 px-8" : "") : "-mt-2"
issueView === "list" && areFiltersApplied ? "px-8 mt-6" : "-mt-2" }`}
}`} >
> <FilterList filters={filters} setFilters={setFilters} />
<FilterList filters={filters} setFilters={setFilters} />
{areFiltersApplied && (
<PrimaryButton
onClick={() => {
if (viewId) {
setFilters({}, true);
setToastAlert({
title: "View updated",
message: "Your view has been updated",
type: "success",
});
} else
setCreateViewModal({
query: filters,
});
}}
className="flex items-center gap-2 text-sm"
>
{!viewId && <PlusIcon className="h-4 w-4" />}
{viewId ? "Update" : "Save"} view
</PrimaryButton>
)}
</div>
{areFiltersApplied && ( {areFiltersApplied && (
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} /> <PrimaryButton
onClick={() => {
if (viewId) {
setFilters({}, true);
setToastAlert({
title: "View updated",
message: "Your view has been updated",
type: "success",
});
} else
setCreateViewModal({
query: filters,
});
}}
className="flex items-center gap-2 text-sm"
>
{!viewId && <PlusIcon className="h-4 w-4" />}
{viewId ? "Update" : "Save"} view
</PrimaryButton>
)} )}
</> </div>
)} {areFiltersApplied && (
<div className={`${issueView === "list" ? "mt-4" : "my-4"} border-t border-brand-base`} />
)}
</>
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox"> <StrictModeDroppable droppableId="trashBox">
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className={`${ className={`${
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0" trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
} fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${ } fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-red-500/20 p-3 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500 text-white" : "" snapshot.isDraggingOver ? "bg-red-500/100 text-white" : ""
} duration-200`} } duration-200`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
@ -487,7 +537,7 @@ export const IssuesView: React.FC<Props> = ({
userAuth={memberRole} userAuth={memberRole}
/> />
) : ( ) : (
<CalendarView /> <CalendarView addIssueToDate={addIssueToDate} />
)} )}
</> </>
) : type === "issue" ? ( ) : type === "issue" ? (
@ -508,8 +558,8 @@ export const IssuesView: React.FC<Props> = ({
title="Create a new issue" title="Create a new issue"
description={ description={
<span> <span>
Use <pre className="inline rounded bg-gray-200 px-2 py-1">C</pre> shortcut to Use <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>{" "}
create a new issue shortcut to create a new issue
</span> </span>
} }
Icon={PlusIcon} Icon={PlusIcon}

View File

@ -56,7 +56,7 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-10 overflow-y-auto">
@ -70,11 +70,11 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white 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-brand-surface-2 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)}> <form onSubmit={handleSubmit(onSubmit)}>
<div> <div>
<div className="space-y-5"> <div className="space-y-5">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
Add Link Add Link
</Dialog.Title> </Dialog.Title>
<div className="mt-2 space-y-3"> <div className="mt-2 space-y-3">

View File

@ -14,7 +14,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -36,7 +36,7 @@ export const AllLists: React.FC<Props> = ({
return ( return (
<> <>
{groupedByIssues && ( {groupedByIssues && (
<div className="flex flex-col space-y-5 bg-white"> <div>
{Object.keys(groupedByIssues).map((singleGroup) => { {Object.keys(groupedByIssues).map((singleGroup) => {
const currentState = const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;

View File

@ -44,7 +44,6 @@ import {
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
import { DIVIDER } from "@blueprintjs/core/lib/esm/common/classes";
type Props = { type Props = {
type?: string; type?: string;
@ -216,149 +215,151 @@ export const SingleListIssue: React.FC<Props> = ({
</ContextMenu.Item> </ContextMenu.Item>
</a> </a>
</ContextMenu> </ContextMenu>
<div className="border-b mx-6 border-gray-300 last:border-b-0"> <div
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-brand-base bg-brand-base last:border-b-0"
className="flex items-center justify-between gap-2 py-3" onContextMenu={(e) => {
onContextMenu={(e) => { e.preventDefault();
e.preventDefault(); setContextMenu(true);
setContextMenu(true); setContextMenuPosition({ x: e.pageX, y: e.pageY });
setContextMenuPosition({ x: e.pageX, y: e.pageY }); }}
}} >
> <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> <div className="flex-grow cursor-pointer px-4 pt-2.5 md:py-2.5">
<a className="group relative flex items-center gap-2"> <a className="group relative flex items-center gap-2">
{properties.key && ( {properties.key && (
<Tooltip <Tooltip
tooltipHeading="Issue ID" tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
> >
<span className="flex-shrink-0 text-xs text-gray-400"> <span className="flex-shrink-0 text-xs text-brand-secondary">
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span> </span>
</Tooltip> </Tooltip>
)} )}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-sm text-gray-800">{truncateText(issue.name, 50)}</span> <span className="text-[0.825rem] text-brand-base">
{truncateText(issue.name, 50)}
</span>
</Tooltip> </Tooltip>
</a> </a>
</Link>
<div className="flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
<div className="flex items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.labels && issue.label_details.length > 0 ? (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
>
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
) : (
""
)}
{properties.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.estimate && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.link && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-gray-500">
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-gray-500">
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<div className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove from {type}</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div> </div>
</Link>
<div className="flex w-full flex-shrink flex-wrap items-center gap-2 px-4 pb-2.5 text-xs sm:w-auto md:px-0 md:py-2.5 md:pr-4">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
<div className="flex items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.labels && issue.label_details.length > 0 ? (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
key={label.id}
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
>
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
) : (
""
)}
{properties.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.estimate && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.link && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<div className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove from {type}</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -37,7 +37,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -104,17 +104,17 @@ export const SingleList: React.FC<Props> = ({
switch (selectedGroup) { switch (selectedGroup) {
case "state": case "state":
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor); icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
break; break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center"); icon = getPriorityIcon(groupTitle, "text-lg");
break; break;
case "labels": case "labels":
const labelColor = const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000"; issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = ( icon = (
<span <span
className="h-[18px] w-[18px] flex-shrink-0 rounded-full" className="h-3 w-3 flex-shrink-0 rounded-full"
style={{ backgroundColor: labelColor }} style={{ backgroundColor: labelColor }}
/> />
); );
@ -130,27 +130,23 @@ export const SingleList: React.FC<Props> = ({
}; };
return ( return (
<Disclosure key={groupTitle} as="div" defaultOpen> <Disclosure as="div" defaultOpen>
{({ open }) => ( {({ open }) => (
<div className="bg-white"> <div>
<div <div className="flex items-center justify-between px-4 py-2.5">
className={`flex items-center justify-between bg-gray-100 px-5 py-3 ${
open ? "" : "rounded-[10px]"
}`}
>
<Disclosure.Button> <Disclosure.Button>
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
{selectedGroup !== null && ( {selectedGroup !== null && (
<span className="flex items-center">{getGroupIcon()}</span> <div className="flex items-center">{getGroupIcon()}</div>
)} )}
{selectedGroup !== null ? ( {selectedGroup !== null ? (
<h2 className="text-base font-semibold capitalize leading-6 text-gray-800"> <h2 className="text-sm font-semibold capitalize leading-6 text-brand-base">
{getGroupTitle()} {getGroupTitle()}
</h2> </h2>
) : ( ) : (
<h2 className="font-medium leading-5">All Issues</h2> <h2 className="font-medium leading-5">All Issues</h2>
)} )}
<span className="rounded-full bg-gray-200 py-0.5 px-3 text-sm text-black"> <span className="text-brand-2 min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs">
{groupedByIssues[groupTitle as keyof IIssue].length} {groupedByIssues[groupTitle as keyof IIssue].length}
</span> </span>
</div> </div>
@ -158,7 +154,7 @@ export const SingleList: React.FC<Props> = ({
{type === "issue" ? ( {type === "issue" ? (
<button <button
type="button" type="button"
className="p-1 text-gray-500 hover:bg-gray-100" className="p-1 text-brand-secondary hover:bg-brand-surface-2"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
@ -168,7 +164,7 @@ export const SingleList: React.FC<Props> = ({
) : ( ) : (
<CustomMenu <CustomMenu
customButton={ customButton={
<div className="flex items-center cursor-pointer"> <div className="flex cursor-pointer items-center">
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
</div> </div>
} }
@ -208,14 +204,16 @@ export const SingleList: React.FC<Props> = ({
makeIssueCopy={() => makeIssueCopy(issue)} makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
removeIssue={() => { removeIssue={() => {
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id); if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
}} }}
isCompleted={isCompleted} isCompleted={isCompleted}
userAuth={userAuth} userAuth={userAuth}
/> />
)) ))
) : ( ) : (
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p> <p className="bg-brand-base px-4 py-2.5 text-sm text-brand-secondary">
No issues.
</p>
) )
) : ( ) : (
<div className="flex h-full w-full items-center justify-center">Loading...</div> <div className="flex h-full w-full items-center justify-center">Loading...</div>

View File

@ -1,8 +1,7 @@
import Link from "next/link"; import Link from "next/link";
// icons // icons
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline"; import { ArrowTopRightOnSquareIcon, LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
import { ExternalLinkIcon } from "components/icons";
// helpers // helpers
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
// types // types
@ -33,15 +32,15 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
<div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1"> <div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1">
<Link href={link.url}> <Link href={link.url}>
<a <a
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 outline-none" className="grid h-7 w-7 place-items-center rounded bg-brand-surface-1 p-1 outline-none hover:bg-brand-surface-2"
target="_blank" target="_blank"
> >
<ExternalLinkIcon width="14" height="14" /> <ArrowTopRightOnSquareIcon className="h-4 w-4 text-brand-secondary" />
</a> </a>
</Link> </Link>
<button <button
type="button" type="button"
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 text-red-500 outline-none duration-300 hover:bg-red-50" className="grid h-7 w-7 place-items-center rounded bg-brand-surface-1 p-1 text-red-500 outline-none duration-300 hover:bg-red-500/20"
onClick={() => handleDeleteLink(link.id)} onClick={() => handleDeleteLink(link.id)}
> >
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
@ -49,13 +48,13 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
</div> </div>
)} )}
<Link href={link.url}> <Link href={link.url}>
<a className="relative flex gap-2 rounded-md border bg-gray-50 p-2" target="_blank"> <a className="relative flex gap-2 rounded-md bg-brand-base p-2" target="_blank">
<div className="mt-0.5"> <div className="mt-0.5">
<LinkIcon className="h-3.5 w-3.5" /> <LinkIcon className="h-3.5 w-3.5" />
</div> </div>
<div> <div>
<h5 className="w-4/5 break-all">{link.title}</h5> <h5 className="w-4/5 break-all">{link.title}</h5>
<p className="mt-0.5 text-gray-500"> <p className="mt-0.5 text-brand-secondary">
Added {timeAgo(link.created_at)} Added {timeAgo(link.created_at)}
<br /> <br />
by{" "} by{" "}

View File

@ -40,7 +40,7 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
const CustomTooltip = ({ active, payload }: TooltipProps<ValueType, NameType>) => { const CustomTooltip = ({ active, payload }: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<div className="rounded-sm bg-gray-300 p-1 text-xs text-gray-800"> <div className="rounded-sm bg-brand-surface-1 p-1 text-xs text-brand-base">
<p>{payload[0].payload.currentDate}</p> <p>{payload[0].payload.currentDate}</p>
</div> </div>
); );
@ -68,20 +68,8 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
<stop offset="100%" stopColor="#3F76FF" stopOpacity={0} /> <stop offset="100%" stopColor="#3F76FF" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<XAxis <XAxis dataKey="currentDate" tickSize={10} minTickGap={10} />
dataKey="currentDate" <YAxis tickSize={10} minTickGap={10} allowDecimals={false} />
stroke="#9ca3af"
tick={{ fontSize: "12px", fill: "#1f2937" }}
tickSize={10}
minTickGap={10}
/>
<YAxis
stroke="#9ca3af"
tick={{ fontSize: "12px", fill: "#1f2937" }}
tickSize={10}
minTickGap={10}
allowDecimals={false}
/>
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Area <Area
type="monotone" type="monotone"

View File

@ -78,7 +78,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
return 2; return 2;
default: default:
return 3; return 0;
} }
}; };
return ( return (
@ -94,19 +94,19 @@ export const SidebarProgressStats: React.FC<Props> = ({
return setTab("States"); return setTab("States");
default: default:
return setTab("States"); return setTab("Assignees");
} }
}} }}
> >
<Tab.List <Tab.List
as="div" as="div"
className={`flex w-full items-center justify-between rounded-md bg-gray-100 px-1 py-1.5 className={`flex w-full items-center justify-between rounded-md bg-brand-surface-1 px-1 py-1.5
${module ? "text-xs" : "text-sm"} `} ${module ? "text-xs" : "text-sm"} `}
> >
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${ `w-full rounded px-3 py-1 text-brand-base ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray" selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
}` }`
} }
> >
@ -114,8 +114,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
</Tab> </Tab>
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${ `w-full rounded px-3 py-1 text-brand-base ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray" selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
}` }`
} }
> >
@ -123,8 +123,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
</Tab> </Tab>
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${ `w-full rounded px-3 py-1 text-brand-base ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray" selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
}` }`
} }
> >
@ -166,7 +166,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
<SingleProgressStats <SingleProgressStats
title={ title={
<> <>
<div className="h-5 w-5 rounded-full border-2 border-white bg-white"> <div className="h-5 w-5 rounded-full border-2 border-white bg-brand-surface-2">
<Image <Image
src={User} src={User}
height="100%" height="100%"

View File

@ -19,8 +19,8 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
}) => ( }) => (
<div <div
className={`flex w-full items-center justify-between rounded p-2 text-xs ${ className={`flex w-full items-center justify-between rounded p-2 text-xs ${
onClick ? "cursor-pointer hover:bg-gray-100" : "" onClick ? "cursor-pointer hover:bg-brand-surface-1" : ""
} ${selected ? "bg-gray-100" : ""}`} } ${selected ? "bg-brand-surface-1" : ""}`}
onClick={onClick} onClick={onClick}
> >
<div className="flex w-1/2 items-center justify-start gap-2">{title}</div> <div className="flex w-1/2 items-center justify-start gap-2">{title}</div>

View File

@ -0,0 +1,60 @@
import { useState, useEffect, ChangeEvent } from "react";
import { useTheme } from "next-themes";
import { THEMES_OBJ } from "constants/themes";
import { CustomSelect } from "components/ui";
import { CustomThemeModal } from "./custom-theme-modal";
export const ThemeSwitch = () => {
const [mounted, setMounted] = useState(false);
const [customThemeModal, setCustomThemeModal] = useState(false);
const { theme, setTheme } = useTheme();
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
<CustomSelect
value={theme}
label={theme ? THEMES_OBJ.find((t) => t.value === theme)?.label : "Select your theme"}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (!customThemeModal) setCustomThemeModal(true);
} else {
const cssVars = [
"--color-bg-base",
"--color-bg-surface-1",
"--color-bg-surface-2",
"--color-border",
"--color-bg-sidebar",
"--color-accent",
"--color-text-base",
"--color-text-secondary",
];
cssVars.forEach((cssVar) => document.documentElement.style.removeProperty(cssVar));
}
document.documentElement.style.setProperty("color-scheme", type);
setTheme(value);
}}
input
width="w-full"
position="right"
>
{THEMES_OBJ.map(({ value, label, type }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
{label}
</CustomSelect.Option>
))}
</CustomSelect>
{/* <CustomThemeModal isOpen={customThemeModal} handleClose={() => setCustomThemeModal(false)} /> */}
</>
);
};

View File

@ -64,7 +64,7 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
{completedCycles ? ( {completedCycles ? (
completedCycles.completed_cycles.length > 0 ? ( completedCycles.completed_cycles.length > 0 ? (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center gap-2 text-sm text-gray-500"> <div className="flex items-center gap-2 text-sm text-brand-secondary">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon height={14} width={14} />
<span>Completed cycles are not editable.</span> <span>Completed cycles are not editable.</span>
</div> </div>

View File

@ -25,7 +25,6 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
}) => { }) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>(); const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
const [showNoCurrentCycleMessage, setShowNoCurrentCycleMessage] = useState(true);
const handleDeleteCycle = (cycle: ICycle) => { const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
@ -61,14 +60,9 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
))} ))}
</div> </div>
) : type === "current" ? ( ) : type === "current" ? (
showNoCurrentCycleMessage && ( <div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
<div className="flex items-center justify-between bg-white w-full px-6 py-4 rounded-[10px]"> <h3 className="text-base font-medium text-brand-base ">No current cycle is present.</h3>
<h3 className="text-base font-medium text-black "> No current cycle is present.</h3> </div>
<button onClick={() => setShowNoCurrentCycleMessage(false)}>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
)
) : ( ) : (
<EmptyState <EmptyState
type="cycle" type="cycle"

View File

@ -139,7 +139,7 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto"> <div className="fixed inset-0 z-20 overflow-y-auto">
@ -153,30 +153,36 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start"> <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-100 sm:mx-0 sm:h-10 sm:w-10"> <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">
<ExclamationTriangleIcon <ExclamationTriangleIcon
className="h-6 w-6 text-red-600" className="h-6 w-6 text-red-600"
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-brand-base"
>
Delete Cycle Delete Cycle
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-brand-secondary">
Are you sure you want to delete cycle-{" "} Are you sure you want to delete cycle-{" "}
<span className="font-bold">{data?.name}</span>? All of the data related <span className="break-all font-medium text-brand-base">
to the cycle will be permanently removed. This action cannot be undone. {data?.name}
</span>
? All of the data related to the cycle will be permanently removed. This
action cannot be undone.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6"> <div className="flex justify-end gap-2 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}> <DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"} {isDeleteLoading ? "Deleting..." : "Delete"}

View File

@ -37,30 +37,30 @@ export const EmptyCycle = () => {
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center gap-5 "> <div className="flex h-full w-full flex-col items-center justify-center gap-5 ">
<div className="relative h-32 w-72"> <div className="relative h-32 w-72">
<div className="absolute right-0 top-0 flex w-64 flex-col rounded-[10px] bg-white text-xs shadow"> <div className="absolute right-0 top-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5"> <div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
<span className="text-sm font-semibold text-black">Cycle Name</span> <span className="text-sm font-semibold text-brand-base">Cycle Name</span>
<div className="flex h-full w-full items-center gap-4"> <div className="flex h-full w-full items-center gap-4">
<span className="h-2 w-20 rounded-full bg-gray-200" /> <span className="h-2 w-20 rounded-full bg-brand-surface-2" />
<span className="h-2 w-20 rounded-full bg-gray-200" /> <span className="h-2 w-20 rounded-full bg-brand-surface-2" />
</div> </div>
</div> </div>
<div className="border-t border-gray-200 bg-gray-100 px-4 py-3"> <div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
<LinearProgressIndicator data={emptyCycleData} /> <LinearProgressIndicator data={emptyCycleData} />
</div> </div>
</div> </div>
<div className="absolute left-0 bottom-0 flex w-64 flex-col rounded-[10px] bg-white text-xs shadow"> <div className="absolute left-0 bottom-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5"> <div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
<span className="text-sm font-semibold text-black">Cycle Name</span> <span className="text-sm font-semibold text-brand-base">Cycle Name</span>
<div className="flex h-full w-full items-center gap-4"> <div className="flex h-full w-full items-center gap-4">
<span className="h-2 w-20 rounded-full bg-gray-200" /> <span className="h-2 w-20 rounded-full bg-brand-surface-2" />
<span className="h-2 w-20 rounded-full bg-gray-200" /> <span className="h-2 w-20 rounded-full bg-brand-surface-2" />
</div> </div>
</div> </div>
<div className="border-t border-gray-200 bg-gray-100 px-4 py-3"> <div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
<LinearProgressIndicator data={emptyCycleData} /> <LinearProgressIndicator data={emptyCycleData} />
</div> </div>
</div> </div>
@ -68,7 +68,7 @@ export const EmptyCycle = () => {
<div className="flex flex-col items-center justify-center gap-4 text-center "> <div className="flex flex-col items-center justify-center gap-4 text-center ">
<h3 className="text-xl font-semibold">Create New Cycle</h3> <h3 className="text-xl font-semibold">Create New Cycle</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-brand-secondary">
Sprint more effectively with Cycles by confining your project <br /> to a fixed amount of Sprint more effectively with Cycles by confining your project <br /> to a fixed amount of
time. Create new cycle now. time. Create new cycle now.
</p> </p>

View File

@ -11,7 +11,11 @@ import useToast from "hooks/use-toast";
// ui // ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers // helpers
import { getDateRangeStatus, isDateRangeValid } from "helpers/date-time.helper"; import {
getDateRangeStatus,
isDateGreaterThanToday,
isDateRangeValid,
} from "helpers/date-time.helper";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
@ -60,24 +64,33 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
data?.start_date && data?.end_date ? getDateRangeStatus(data?.start_date, data?.end_date) : ""; data?.start_date && data?.end_date ? getDateRangeStatus(data?.start_date, data?.end_date) : "";
const dateChecker = async (payload: any) => { const dateChecker = async (payload: any) => {
await cyclesService if (isDateGreaterThanToday(payload.end_date)) {
.cycleDateCheck(workspaceSlug as string, projectId as string, payload) await cyclesService
.then((res) => { .cycleDateCheck(workspaceSlug as string, projectId as string, payload)
if (res.status) { .then((res) => {
setIsDateValid(true); if (res.status) {
} else { setIsDateValid(true);
setIsDateValid(false); } else {
setToastAlert({ setIsDateValid(false);
type: "error", setToastAlert({
title: "Error!", type: "error",
message: title: "Error!",
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", message:
}); "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
} });
}) }
.catch((err) => { })
console.log(err); .catch((err) => {
console.log(err);
});
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
}); });
}
}; };
const checkEmptyDate = const checkEmptyDate =
@ -94,13 +107,12 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
return ( return (
<form onSubmit={handleSubmit(handleCreateUpdateCycle)}> <form onSubmit={handleSubmit(handleCreateUpdateCycle)}>
<div className="space-y-5"> <div className="space-y-5">
<h3 className="text-lg font-medium leading-6 text-gray-900"> <h3 className="text-lg font-medium leading-6 text-brand-base">
{status ? "Update" : "Create"} Cycle {status ? "Update" : "Create"} Cycle
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<Input <Input
mode="transparent"
autoComplete="off" autoComplete="off"
id="name" id="name"
name="name" name="name"
@ -124,7 +136,6 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
name="description" name="description"
placeholder="Description" placeholder="Description"
className="h-32 resize-none text-sm" className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description} error={errors.description}
register={register} register={register}
/> />
@ -153,7 +164,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "The date you have entered is invalid. Please check and enter a valid date.", message:
"The date you have entered is invalid. Please check and enter a valid date.",
}); });
} }
} }
@ -184,7 +196,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "The date you have entered is invalid. Please check and enter a valid date.", message:
"The date you have entered is invalid. Please check and enter a valid date.",
}); });
} }
} }
@ -196,7 +209,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
</div> </div>
</div> </div>
</div> </div>
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t px-5 pt-5"> <div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-brand-base px-5 pt-5">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton <PrimaryButton
type="submit" type="submit"
@ -207,7 +220,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
? "cursor-pointer" ? "cursor-pointer"
: "cursor-not-allowed" : "cursor-not-allowed"
} }
loading={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true} disabled={checkEmptyDate ? false : isDateValid ? false : true}
loading={isSubmitting}
> >
{status {status
? isSubmitting ? isSubmitting

View File

@ -151,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto"> <div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> <div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
@ -164,7 +164,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg bg-white 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-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<CycleForm <CycleForm
handleFormSubmit={handleFormSubmit} handleFormSubmit={handleFormSubmit}
handleClose={handleClose} handleClose={handleClose}

View File

@ -59,9 +59,9 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
{({ open }) => ( {({ open }) => (
<> <>
<Listbox.Button <Listbox.Button
className={`flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 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-brand-base px-2 py-1 text-xs shadow-sm duration-300 hover:bg-brand-surface-1 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
> >
<CyclesIcon className="h-3 w-3 text-gray-500" /> <CyclesIcon className="h-3 w-3 text-brand-secondary" />
<div className="flex items-center gap-2 truncate"> <div className="flex items-center gap-2 truncate">
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"} {cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
</div> </div>
@ -75,7 +75,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options <Listbox.Options
className={`absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-white shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none`} className={`absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-brand-surface-2 shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none`}
> >
<div className="py-1"> <div className="py-1">
{options ? ( {options ? (
@ -93,7 +93,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
: "" : ""
} ${ } ${
active ? "bg-indigo-50" : "" active ? "bg-indigo-50" : ""
} relative cursor-pointer select-none p-2 text-gray-900` } relative cursor-pointer select-none p-2 text-brand-base`
} }
value={option.value} value={option.value}
> >
@ -103,14 +103,14 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
</Listbox.Option> </Listbox.Option>
)) ))
) : ( ) : (
<p className="text-center text-sm text-gray-500">No options</p> <p className="text-center text-sm text-brand-secondary">No options</p>
) )
) : ( ) : (
<p className="text-center text-sm text-gray-500">Loading...</p> <p className="text-center text-sm text-brand-secondary">Loading...</p>
)} )}
<button <button
type="button" type="button"
className="relative w-full flex select-none items-center gap-x-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-gray-900" className="relative w-full flex select-none items-center gap-x-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-brand-base"
onClick={openCycleModal} onClick={openCycleModal}
> >
<PlusIcon className="h-4 w-4 text-gray-400" aria-hidden="true" /> <PlusIcon className="h-4 w-4 text-gray-400" aria-hidden="true" />

View File

@ -92,13 +92,8 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
cyclesService cyclesService
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data) .patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
.then((res) => { .then(() => mutate(CYCLE_DETAILS(cycleId as string)))
console.log(res); .catch((e) => console.log(e));
mutate(CYCLE_DETAILS(cycleId as string));
})
.catch((e) => {
console.log(e);
});
}; };
const handleCopyText = () => { const handleCopyText = () => {
@ -140,31 +135,31 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<div <div
className={`fixed top-0 ${ className={`fixed top-0 ${
isOpen ? "right-0" : "-right-[24rem]" isOpen ? "right-0" : "-right-[24rem]"
} z-20 h-full w-[24rem] overflow-y-auto border-l bg-gray-50 py-5 duration-300`} } z-20 h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-sidebar py-5 duration-300`}
> >
{cycle ? ( {cycle ? (
<> <>
<div className="flex flex-col items-start justify-center"> <div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm"> <div className="flex gap-2.5 px-5 text-sm">
<div className="flex items-center "> <div className="flex items-center">
<span <span className="flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-center text-xs capitalize">
className={`flex items-center rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-center text-sm capitalize text-gray-800 `}
>
{capitalizeFirstLetter(cycleStatus)} {capitalizeFirstLetter(cycleStatus)}
</span> </span>
</div> </div>
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-gray-800"> <div className="relative flex h-full w-52 items-center gap-2">
<Popover className="flex h-full items-center justify-center rounded-lg"> <Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} disabled={isCompleted ?? false}
className={`group flex h-full items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${ className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
open ? "bg-gray-100" : "" cycle.start_date ? "" : "text-brand-secondary"
}`} }`}
> >
<CalendarDaysIcon className="h-3 w-3" /> <CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${cycle?.start_date}`))}</span> <span>
{renderShortDate(new Date(`${cycle?.start_date}`), "Start date")}
</span>
</Popover.Button> </Popover.Button>
<Transition <Transition
@ -214,20 +209,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
)} )}
</Popover> </Popover>
<span> <span>
<ArrowLongRightIcon className="h-3 w-3" /> <ArrowLongRightIcon className="h-3 w-3 text-brand-secondary" />
</span> </span>
<Popover className="flex h-full items-center justify-center rounded-lg"> <Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} disabled={isCompleted ?? false}
className={`group flex items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${ className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
open ? "bg-gray-100" : "" cycle.end_date ? "" : "text-brand-secondary"
}`} }`}
> >
<CalendarDaysIcon className="h-3 w-3 " /> <CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${cycle?.end_date}`))}</span> <span>{renderShortDate(new Date(`${cycle?.end_date}`), "End date")}</span>
</Popover.Button> </Popover.Button>
<Transition <Transition
@ -239,7 +234,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden"> <Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker <DatePicker
selected={ selected={
watch("end_date") ? new Date(`${watch("end_date")}`) : new Date() watch("end_date") ? new Date(`${watch("end_date")}`) : new Date()
@ -280,10 +275,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div> </div>
</div> </div>
<div className="flex flex-col gap-6 px-6 py-6 w-full"> <div className="flex w-full flex-col gap-6 px-6 py-6">
<div className="flex flex-col items-start justify-start gap-2 w-full"> <div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex items-start justify-between gap-2 w-full"> <div className="flex w-full items-start justify-between gap-2">
<h4 className="text-xl font-semibold text-gray-900">{cycle.name}</h4> <h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4>
<CustomMenu width="lg" ellipsis> <CustomMenu width="lg" ellipsis>
{!isCompleted && ( {!isCompleted && (
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}> <CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
@ -302,15 +297,15 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</CustomMenu> </CustomMenu>
</div> </div>
<span className="whitespace-normal text-sm leading-5 text-black"> <span className="whitespace-normal text-sm leading-5 text-brand-secondary">
{cycle.description} {cycle.description}
</span> </span>
</div> </div>
<div className="flex flex-col gap-4 text-sm"> <div className="flex flex-col gap-4 text-sm">
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2"> <div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
<UserCircleIcon className="h-5 w-5 text-gray-400" /> <UserCircleIcon className="h-5 w-5" />
<span>Lead</span> <span>Lead</span>
</div> </div>
@ -328,17 +323,17 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
{cycle.owned_by.first_name.charAt(0)} {cycle.owned_by.first_name.charAt(0)}
</span> </span>
)} )}
<span className="text-gray-900">{cycle.owned_by.first_name}</span> <span className="text-brand-secondary">{cycle.owned_by.first_name}</span>
</div> </div>
</div> </div>
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2"> <div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
<ChartPieIcon className="h-5 w-5 text-gray-400" /> <ChartPieIcon className="h-5 w-5" />
<span>Progress</span> <span>Progress</span>
</div> </div>
<div className="flex items-center gap-2.5 text-gray-800"> <div className="flex items-center gap-2.5 text-brand-secondary">
<span className="h-4 w-4"> <span className="h-4 w-4">
<ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} /> <ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} />
</span> </span>
@ -349,7 +344,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div> </div>
</div> </div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-6 py-6 "> <div className="flex w-full flex-col items-center justify-start gap-2 border-t border-brand-base p-6">
<Disclosure defaultOpen> <Disclosure defaultOpen>
{({ open }) => ( {({ open }) => (
<div <div
@ -357,7 +352,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
> >
<div className="flex w-full items-center justify-between gap-2 "> <div className="flex w-full items-center justify-between gap-2 ">
<div className="flex items-center justify-start gap-2 text-sm"> <div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-gray-500">Progress</span> <span className="font-medium text-brand-secondary">Progress</span>
{!open && progressPercentage ? ( {!open && progressPercentage ? (
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]"> <span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
{progressPercentage ? `${progressPercentage}%` : ""} {progressPercentage ? `${progressPercentage}%` : ""}
@ -376,7 +371,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon height={14} width={14} />
<span className="text-xs italic text-gray-500"> <span className="text-xs italic text-brand-secondary">
{cycleStatus === "upcoming" {cycleStatus === "upcoming"
? "Cycle is yet to start." ? "Cycle is yet to start."
: "Invalid date. Please enter valid date."} : "Invalid date. Please enter valid date."}
@ -391,7 +386,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<div className="flex items-start justify-between gap-4 py-2 text-xs"> <div className="flex items-start justify-between gap-4 py-2 text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span> <span>
<DocumentIcon className="h-3 w-3 text-gray-500" /> <DocumentIcon className="h-3 w-3 text-brand-secondary" />
</span> </span>
<span> <span>
Pending Issues -{" "} Pending Issues -{" "}
@ -400,7 +395,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</span> </span>
</div> </div>
<div className="flex items-center gap-3 text-gray-900"> <div className="flex items-center gap-3 text-brand-base">
<div className="flex items-center justify-center gap-1"> <div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" /> <span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span> <span>Ideal</span>
@ -429,7 +424,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</Disclosure> </Disclosure>
</div> </div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-6 py-6 "> <div className="flex w-full flex-col items-center justify-start gap-2 border-t border-brand-base p-6">
<Disclosure defaultOpen> <Disclosure defaultOpen>
{({ open }) => ( {({ open }) => (
<div <div
@ -437,7 +432,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
> >
<div className="flex w-full items-center justify-between gap-2"> <div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center justify-start gap-2 text-sm"> <div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-gray-500">Other Information</span> <span className="font-medium text-brand-secondary">Other Information</span>
</div> </div>
{cycle.total_issues > 0 ? ( {cycle.total_issues > 0 ? (
@ -450,7 +445,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon height={14} width={14} />
<span className="text-xs italic text-gray-500"> <span className="text-xs italic text-brand-secondary">
No issues found. Please add issue. No issues found. Please add issue.
</span> </span>
</div> </div>

View File

@ -238,7 +238,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
return ( return (
<div> <div>
<div className="flex flex-col rounded-[10px] bg-white text-xs shadow"> <div className="flex flex-col rounded-[10px] bg-brand-base text-xs shadow">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full"> <a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4"> <div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
@ -269,20 +269,20 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
)} )}
</div> </div>
<div className="flex items-center justify-start gap-5"> <div className="flex items-center justify-start gap-5 text-brand-secondary">
<div className="flex items-start gap-1 "> <div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4 text-gray-900" /> <CalendarDaysIcon className="h-4 w-4" />
<span className="text-gray-400">Start :</span> <span>Start :</span>
<span>{renderShortDateWithYearFormat(startDate)}</span> <span>{renderShortDateWithYearFormat(startDate)}</span>
</div> </div>
<div className="flex items-start gap-1 "> <div className="flex items-start gap-1 ">
<TargetIcon className="h-4 w-4 text-gray-900" /> <TargetIcon className="h-4 w-4" />
<span className="text-gray-400">End :</span> <span>End :</span>
<span>{renderShortDateWithYearFormat(endDate)}</span> <span>{renderShortDateWithYearFormat(endDate)}</span>
</div> </div>
</div> </div>
<div className="flex items-center justify-between mt-4"> <div className="mt-4 flex items-center justify-between text-brand-secondary">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image <Image
@ -293,11 +293,11 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
alt={cycle.owned_by.first_name} alt={cycle.owned_by.first_name}
/> />
) : ( ) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white"> <span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full capitalize">
{cycle.owned_by.first_name.charAt(0)} {cycle.owned_by.first_name.charAt(0)}
</span> </span>
)} )}
<span className="text-gray-900">{cycle.owned_by.first_name}</span> <span>{cycle.owned_by.first_name}</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
{!isCompleted && ( {!isCompleted && (
@ -306,7 +306,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
e.preventDefault(); e.preventDefault();
handleEditCycle(); handleEditCycle();
}} }}
className="flex cursor-pointer items-center rounded p-1 duration-300 hover:bg-gray-100" className="flex cursor-pointer items-center rounded p-1 duration-300 hover:bg-brand-surface-1"
> >
<span> <span>
<PencilIcon className="h-4 w-4" /> <PencilIcon className="h-4 w-4" />
@ -350,7 +350,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
<Disclosure> <Disclosure>
{({ open }) => ( {({ open }) => (
<div <div
className={`flex h-full w-full flex-col border-t border-gray-200 bg-gray-100 ${ className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-brand-base bg-brand-surface-2 text-brand-secondary ${
open ? "" : "flex-row" open ? "" : "flex-row"
}`} }`}
> >
@ -368,7 +368,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
</div> </div>
<Transition show={open}> <Transition show={open}>
<Disclosure.Panel> <Disclosure.Panel>
<div className="overflow-hidden rounded-b-md bg-white py-3 shadow"> <div className="overflow-hidden rounded-b-md bg-brand-surface-2 py-3 shadow">
<div className="col-span-2 space-y-3 px-4"> <div className="col-span-2 space-y-3 px-4">
<div className="space-y-3 text-xs"> <div className="space-y-3 text-xs">
{stateGroups.map((group) => ( {stateGroups.map((group) => (
@ -388,7 +388,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
<div> <div>
<span> <span>
{cycle[group.key as keyof ICycle] as number}{" "} {cycle[group.key as keyof ICycle] as number}{" "}
<span className="text-gray-500"> <span className="text-brand-secondary">
-{" "} -{" "}
{cycle.total_issues > 0 {cycle.total_issues > 0
? `${Math.round( ? `${Math.round(

View File

@ -89,7 +89,7 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10"> <div className="fixed inset-0 z-10">
@ -103,33 +103,33 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg bg-white py-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl"> <Dialog.Panel className="relative transform rounded-lg bg-brand-surface-1 py-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between px-5"> <div className="flex items-center justify-between px-5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<TransferIcon className="h-4 w-5" color="#495057" /> <TransferIcon className="h-4 w-5" color="#495057" />
<h4 className="text-gray-700 font-medium text-[1.50rem]">Transfer Issues</h4> <h4 className="text-xl font-medium text-brand-base">Transfer Issues</h4>
</div> </div>
<button onClick={handleClose}> <button onClick={handleClose}>
<XMarkIcon className="h-4 w-4" /> <XMarkIcon className="h-4 w-4" />
</button> </button>
</div> </div>
<div className="flex items-center gap-2 pb-3 mt-2 px-5 border-b border-gray-200"> <div className="flex items-center gap-2 border-b border-brand-base px-5 pb-3">
<MagnifyingGlassIcon className="h-4 w-4 text-gray-500" /> <MagnifyingGlassIcon className="h-4 w-4 text-brand-secondary" />
<input <input
className="outline-none" className="bg-brand-surface-1 outline-none"
placeholder="Search for a cycle..." placeholder="Search for a cycle..."
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
value={query} value={query}
/> />
</div> </div>
<div className="flex flex-col items-start w-full gap-2 px-5"> <div className="flex w-full flex-col items-start gap-2 px-5">
{filteredOptions ? ( {filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((option: ICycle) => ( filteredOptions.map((option: ICycle) => (
<button <button
key={option.id} key={option.id}
className="flex items-center gap-4 py-3 px-2 text-gray-600 text-sm rounded w-full hover:bg-gray-100" className="flex w-full items-center gap-4 rounded px-4 py-3 text-sm text-brand-secondary hover:bg-brand-surface-1"
onClick={() => { onClick={() => {
transferIssue({ transferIssue({
new_cycle_id: option?.id, new_cycle_id: option?.id,
@ -138,25 +138,25 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
}} }}
> >
<ContrastIcon className="h-5 w-5" /> <ContrastIcon className="h-5 w-5" />
<div className="flex justify-between w-full"> <div className="flex w-full justify-between">
<span>{option?.name}</span> <span>{option?.name}</span>
<span className=" flex bg-gray-200 capitalize px-2 rounded-full items-center"> <span className=" flex items-center rounded-full bg-brand-surface-2 px-2 capitalize">
{getDateRangeStatus(option?.start_date, option?.end_date)} {getDateRangeStatus(option?.start_date, option?.end_date)}
</span> </span>
</div> </div>
</button> </button>
)) ))
) : ( ) : (
<div className="flex items-center justify-center gap-4 p-5 text-sm w-full"> <div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon height={14} width={14} />
<span className="text-center text-gray-500"> <span className="text-center text-brand-secondary">
You dont have any current cycle. Please create one to transfer the You dont have any current cycle. Please create one to transfer the
issues. issues.
</span> </span>
</div> </div>
) )
) : ( ) : (
<p className="text-center text-gray-500">Loading...</p> <p className="text-center text-brand-secondary">Loading...</p>
)} )}
</div> </div>
</div> </div>

View File

@ -37,8 +37,8 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues ? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
: 0; : 0;
return ( return (
<div className="flex items-center justify-between -mt-4 mb-4"> <div className="-mt-2 mb-4 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-gray-500"> <div className="flex items-center gap-2 text-sm text-brand-secondary">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon height={14} width={14} />
<span>Completed cycles are not editable.</span> <span>Completed cycles are not editable.</span>
</div> </div>
@ -46,7 +46,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
{transferableIssuesCount > 0 && ( {transferableIssuesCount > 0 && (
<div> <div>
<PrimaryButton onClick={handleClick} className="flex items-center gap-3 rounded-lg"> <PrimaryButton onClick={handleClick} className="flex items-center gap-3 rounded-lg">
<TransferIcon className="h-4 w-4" color="white"/> <TransferIcon className="h-4 w-4" color="white" />
<span className="text-white">Transfer Issues</span> <span className="text-white">Transfer Issues</span>
</PrimaryButton> </PrimaryButton>
</div> </div>

View File

@ -36,7 +36,7 @@ const EmojiIconPicker: React.FC<Props> = ({
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [openColorPicker, setOpenColorPicker] = useState(false); const [openColorPicker, setOpenColorPicker] = useState(false);
const [activeColor, setActiveColor] = useState<string>("#020617"); const [activeColor, setActiveColor] = useState<string>("#858e96");
const [recentEmojis, setRecentEmojis] = useState<string[]>([]); const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
@ -55,7 +55,7 @@ const EmojiIconPicker: React.FC<Props> = ({
return ( return (
<Popover className="relative z-[1]" ref={ref}> <Popover className="relative z-[1]" ref={ref}>
<Popover.Button <Popover.Button
className="rounded-full bg-gray-100 p-2 outline-none sm:text-sm" className="rounded-full bg-brand-surface-1 p-2 outline-none sm:text-sm"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
> >
{label} {label}
@ -69,8 +69,8 @@ const EmojiIconPicker: React.FC<Props> = ({
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] bg-white shadow-lg"> <Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-brand-base bg-brand-surface-2 shadow-lg">
<div className="h-[230px] w-[250px] overflow-auto border rounded-[4px] bg-white p-2 shadow-xl"> <div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-brand-base bg-brand-surface-2 p-2 shadow-xl">
<Tab.Group as="div" className="flex h-full w-full flex-col"> <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"> <Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
{tabOptions.map((tab) => ( {tabOptions.map((tab) => (
@ -82,7 +82,7 @@ const EmojiIconPicker: React.FC<Props> = ({
setOpenColorPicker(false); setOpenColorPicker(false);
}} }}
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${ className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
selected ? "border-theme text-theme" : "border-transparent text-gray-500" selected ? "" : "border-transparent text-brand-secondary"
}`} }`}
> >
{tab.title} {tab.title}
@ -95,12 +95,12 @@ const EmojiIconPicker: React.FC<Props> = ({
<Tab.Panel> <Tab.Panel>
{recentEmojis.length > 0 && ( {recentEmojis.length > 0 && (
<div className="py-2"> <div className="py-2">
<h3 className="mb-2 ml-1 text-xs text-gray-400">Recent</h3> <h3 className="mb-2 text-xs text-brand-secondary">Recent</h3>
<div className="grid grid-cols-8 gap-2"> <div className="grid grid-cols-8 gap-2">
{recentEmojis.map((emoji) => ( {recentEmojis.map((emoji) => (
<button <button
type="button" type="button"
className="h-4 w-4 select-none text-base hover:bg-hover-gray flex items-center justify-between" className="flex h-4 w-4 select-none items-center justify-between text-sm"
key={emoji} key={emoji}
onClick={() => { onClick={() => {
onChange(emoji); onChange(emoji);
@ -113,13 +113,13 @@ const EmojiIconPicker: React.FC<Props> = ({
</div> </div>
</div> </div>
)} )}
<hr className="w-full h-[1px] mb-2" /> <hr className="mb-2 h-[1px] w-full border-brand-base" />
<div> <div>
<div className="grid grid-cols-8 gap-x-2 gap-y-3"> <div className="grid grid-cols-8 gap-x-2 gap-y-3">
{emojis.map((emoji) => ( {emojis.map((emoji) => (
<button <button
type="button" type="button"
className="h-4 w-4 ml-1 select-none text-base hover:bg-hover-gray flex justify-center items-center" className="mb-1 flex h-4 w-4 select-none items-center text-sm"
key={emoji} key={emoji}
onClick={() => { onClick={() => {
onChange(emoji); onChange(emoji);
@ -136,7 +136,7 @@ const EmojiIconPicker: React.FC<Props> = ({
<div className="py-2"> <div className="py-2">
<Tab.Panel className="flex h-full w-full flex-col justify-center"> <Tab.Panel className="flex h-full w-full flex-col justify-center">
<div className="relative"> <div className="relative">
<div className="pb-2 px-1 flex items-center justify-between"> <div className="flex items-center justify-between px-1 pb-2">
{[ {[
"#FF6B00", "#FF6B00",
"#8CC1FF", "#8CC1FF",
@ -147,7 +147,7 @@ const EmojiIconPicker: React.FC<Props> = ({
"#000000", "#000000",
].map((curCol) => ( ].map((curCol) => (
<span <span
className="w-4 h-4 rounded-full cursor-pointer" className="h-4 w-4 cursor-pointer rounded-full"
style={{ backgroundColor: curCol }} style={{ backgroundColor: curCol }}
onClick={() => setActiveColor(curCol)} onClick={() => setActiveColor(curCol)}
/> />
@ -158,14 +158,14 @@ const EmojiIconPicker: React.FC<Props> = ({
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<span <span
className="w-4 h-4 rounded-full conical-gradient" className="conical-gradient h-4 w-4 rounded-full"
style={{ backgroundColor: activeColor }} style={{ backgroundColor: activeColor }}
/> />
</button> </button>
</div> </div>
<div> <div>
<TwitterPicker <TwitterPicker
className={`m-2 !absolute top-4 left-4 z-10 ${ className={`!absolute top-4 left-4 z-10 m-2 ${
openColorPicker ? "block" : "hidden" openColorPicker ? "block" : "hidden"
}`} }`}
color={activeColor} color={activeColor}
@ -178,13 +178,12 @@ const EmojiIconPicker: React.FC<Props> = ({
/> />
</div> </div>
</div> </div>
<hr className="w-full h-[1px] mb-1" /> <hr className="mb-1 h-[1px] w-full border-brand-base" />
<div className="mt-1 ml-1 grid grid-cols-8 gap-x-2 gap-y-3">
<div className="grid grid-cols-8 mt-1 ml-1 gap-x-2 gap-y-3">
{icons.material_rounded.map((icon) => ( {icons.material_rounded.map((icon) => (
<button <button
type="button" type="button"
className="h-4 w-4 mb-1 select-none text-lg hover:bg-hover-gray flex items-center" className="mb-1 flex h-4 w-4 select-none items-center text-lg"
key={icon.name} key={icon.name}
onClick={() => { onClick={() => {
if (onIconsClick) onIconsClick(icon.name); if (onIconsClick) onIconsClick(icon.name);

View File

@ -6,19 +6,20 @@ import { mutate } from "swr";
// react-hook-form // react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services // services
import estimatesService from "services/estimates.service"; import estimatesService from "services/estimates.service";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
import { Dialog, Transition } from "@headlessui/react";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import { checkDuplicates } from "helpers/array.helper";
// types // types
import { IEstimate } from "types"; import { IEstimate, IEstimateFormData } from "types";
// fetch-keys // fetch-keys
import { ESTIMATES_LIST } from "constants/fetch-keys"; import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -26,66 +27,90 @@ type Props = {
data?: IEstimate; data?: IEstimate;
}; };
const defaultValues: Partial<IEstimate> = { type FormValues = {
name: string;
description: string;
value1: string;
value2: string;
value3: string;
value4: string;
value5: string;
value6: string;
};
const defaultValues: Partial<FormValues> = {
name: "", name: "",
description: "", description: "",
value1: "",
value2: "",
value3: "",
value4: "",
value5: "",
value6: "",
}; };
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => { export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { isSubmitting },
handleSubmit, handleSubmit,
reset, reset,
} = useForm<IEstimate>({ } = useForm<FormValues>({
defaultValues, defaultValues,
}); });
const onClose = () => {
handleClose();
reset();
};
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const createEstimate = async (formData: IEstimate) => { const createEstimate = async (payload: IEstimateFormData) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const payload = {
name: formData.name,
description: formData.description,
};
await estimatesService await estimatesService
.createEstimate(workspaceSlug as string, projectId as string, payload) .createEstimate(workspaceSlug as string, projectId as string, payload)
.then((res) => { .then(() => {
mutate<IEstimate[]>( mutate(ESTIMATES_LIST(projectId as string));
ESTIMATES_LIST(projectId as string), onClose();
(prevData) => [res, ...(prevData ?? [])],
false
);
handleClose();
}) })
.catch(() => { .catch((err) => {
setToastAlert({ if (err.status === 400)
type: "error", setToastAlert({
title: "Error!", type: "error",
message: "Error: Estimate could not be created", title: "Error!",
}); message: "Estimate with that name already exists. Please try again with another name.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate could not be created. Please try again.",
});
}); });
}; };
const updateEstimate = async (formData: IEstimate) => { const updateEstimate = async (payload: IEstimateFormData) => {
if (!workspaceSlug || !projectId || !data) return; if (!workspaceSlug || !projectId || !data) return;
const payload = {
name: formData.name,
description: formData.description,
};
mutate<IEstimate[]>( mutate<IEstimate[]>(
ESTIMATES_LIST(projectId as string), ESTIMATES_LIST(projectId.toString()),
(prevData) => (prevData) =>
prevData?.map((p) => { prevData?.map((p) => {
if (p.id === data.id) return { ...p, ...payload }; if (p.id === data.id)
return {
...p,
name: payload.estimate.name,
description: payload.estimate.description,
points: p.points.map((point, index) => ({
...point,
value: payload.estimate_points[index].value,
})),
};
return p; return p;
}), }),
@ -95,23 +120,104 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
await estimatesService await estimatesService
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload) .patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload)
.then(() => { .then(() => {
mutate(ESTIMATES_LIST(projectId.toString()));
mutate(ESTIMATE_DETAILS(data.id));
handleClose(); handleClose();
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Error: Estimate could not be updated", message: "Estimate could not be updated. Please try again.",
}); });
}); });
handleClose();
onClose();
};
const onSubmit = async (formData: FormValues) => {
if (!formData.name || formData.name === "") {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate title cannot be empty.",
});
return;
}
if (
formData.value1 === "" ||
formData.value2 === "" ||
formData.value3 === "" ||
formData.value4 === "" ||
formData.value5 === "" ||
formData.value6 === ""
) {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate point cannot be empty.",
});
return;
}
if (
checkDuplicates([
formData.value1,
formData.value2,
formData.value3,
formData.value4,
formData.value5,
formData.value6,
])
) {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate points cannot have duplicate values.",
});
return;
}
const payload: IEstimateFormData = {
estimate: {
name: formData.name,
description: formData.description,
},
estimate_points: [],
};
for (let i = 0; i < 6; i++) {
const point = {
key: i,
value: formData[`value${i + 1}` as keyof FormValues],
};
if (data)
payload.estimate_points.push({
id: data.points[i].id,
...point,
});
else payload.estimate_points.push({ ...point });
}
if (data) await updateEstimate(payload);
else await createEstimate(payload);
}; };
useEffect(() => { useEffect(() => {
reset({ if (data)
...defaultValues, reset({
...data, ...defaultValues,
}); ...data,
value1: data.points[0]?.value,
value2: data.points[1]?.value,
value3: data.points[2]?.value,
value4: data.points[3]?.value,
value5: data.points[4]?.value,
value6: data.points[5]?.value,
});
else reset({ ...defaultValues });
}, [data, reset]); }, [data, reset]);
return ( return (
@ -127,7 +233,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto"> <div className="fixed inset-0 z-20 overflow-y-auto">
@ -141,10 +247,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg bg-white 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-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form <form onSubmit={handleSubmit(onSubmit)}>
onSubmit={data ? handleSubmit(updateEstimate) : handleSubmit(createEstimate)}
>
<div className="space-y-3"> <div className="space-y-3">
<div className="text-lg font-medium leading-6"> <div className="text-lg font-medium leading-6">
{data ? "Update" : "Create"} Estimate {data ? "Update" : "Create"} Estimate
@ -156,17 +260,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
type="name" type="name"
placeholder="Title" placeholder="Title"
autoComplete="off" autoComplete="off"
mode="transparent"
className="resize-none text-xl" className="resize-none text-xl"
error={errors.name}
register={register} register={register}
validations={{
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
},
}}
/> />
</div> </div>
<div> <div>
@ -175,11 +270,107 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
name="description" name="description"
placeholder="Description" placeholder="Description"
className="h-32 resize-none text-sm" className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description}
register={register} register={register}
/> />
</div> </div>
<div className="grid grid-cols-3 gap-3">
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">1</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value1"
type="name"
className="rounded-l-none"
placeholder="Point 1"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">2</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value2"
type="name"
className="rounded-l-none"
placeholder="Point 2"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">3</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value3"
type="name"
className="rounded-l-none"
placeholder="Point 3"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">4</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value4"
type="name"
className="rounded-l-none"
placeholder="Point 4"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">5</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value5"
type="name"
className="rounded-l-none"
placeholder="Point 5"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">6</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value6"
type="name"
className="rounded-l-none"
placeholder="Point 6"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
</div>
</div> </div>
<div className="mt-5 flex justify-end gap-2"> <div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>

View File

@ -46,7 +46,7 @@ export const DeleteEstimateModal: React.FC<Props> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-10 overflow-y-auto">
@ -60,10 +60,10 @@ export const DeleteEstimateModal: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white 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-brand-base bg-brand-base 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 flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6"> <div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-100 p-4"> <span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon <ExclamationTriangleIcon
className="h-6 w-6 text-red-600" className="h-6 w-6 text-red-600"
aria-hidden="true" aria-hidden="true"
@ -74,9 +74,9 @@ export const DeleteEstimateModal: React.FC<Props> = ({
</span> </span>
</div> </div>
<span> <span>
<p className="break-all text-sm leading-7 text-gray-500"> <p className="break-all text-sm leading-7 text-brand-secondary">
Are you sure you want to delete estimate-{" "} Are you sure you want to delete estimate-{" "}
<span className="break-all font-semibold">{data.name}</span> <span className="break-all font-medium text-brand-base">{data.name}</span>
{""}? All of the data related to the estiamte will be permanently removed. {""}? All of the data related to the estiamte will be permanently removed.
This action cannot be undone. This action cannot be undone.
</p> </p>

View File

@ -1,357 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// types
import type { IEstimate, IEstimatePoint } from "types";
import estimatesService from "services/estimates.service";
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
data?: IEstimatePoint[];
estimate: IEstimate | null;
onClose: () => void;
};
interface FormValues {
value1: string;
value2: string;
value3: string;
value4: string;
value5: string;
value6: string;
}
const defaultValues: FormValues = {
value1: "",
value2: "",
value3: "",
value4: "",
value5: "",
value6: "",
};
export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, onClose }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const {
register,
formState: { isSubmitting },
handleSubmit,
reset,
} = useForm<FormValues>({ defaultValues });
const handleClose = () => {
onClose();
reset();
};
const createEstimatePoints = async (formData: FormValues) => {
if (!workspaceSlug || !projectId) return;
const payload = {
estimate_points: [
{
key: 0,
value: formData.value1,
},
{
key: 1,
value: formData.value2,
},
{
key: 2,
value: formData.value3,
},
{
key: 3,
value: formData.value4,
},
{
key: 4,
value: formData.value5,
},
{
key: 5,
value: formData.value6,
},
],
};
await estimatesService
.createEstimatePoints(
workspaceSlug as string,
projectId as string,
estimate?.id as string,
payload
)
.then(() => {
handleClose();
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate points could not be created. Please try again.",
});
});
};
const updateEstimatePoints = async (formData: FormValues) => {
if (!workspaceSlug || !projectId || !data || data.length === 0) return;
const payload = {
estimate_points: data.map((d, index) => ({
id: d.id,
value: (formData as any)[`value${index + 1}`],
})),
};
await estimatesService
.patchEstimatePoints(
workspaceSlug as string,
projectId as string,
estimate?.id as string,
payload
)
.then(() => {
handleClose();
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate points could not be created. Please try again.",
});
});
};
const onSubmit = async (formData: FormValues) => {
if (data && data.length !== 0) await updateEstimatePoints(formData);
else await createEstimatePoints(formData);
if (estimate) mutate(ESTIMATE_POINTS_LIST(estimate.id));
};
useEffect(() => {
if (!data || data.length < 6) return;
reset({
...defaultValues,
value1: data[0].value,
value2: data[1].value,
value3: data[2].value,
value4: data[3].value,
value5: data[4].value,
value6: data[5].value,
});
}, [data, reset]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
<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-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 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-white 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="flex flex-col gap-3">
<h4 className="text-lg font-medium leading-6">
{data ? "Update" : "Create"} Estimate Points
</h4>
<div className="grid grid-cols-3 gap-3">
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">1</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value1"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 10,
message: "Name should be less than 10 characters",
},
}}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">2</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value2"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 10,
message: "Name should be less than 10 characters",
},
}}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">3</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value3"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 10,
message: "Name should be less than 10 characters",
},
}}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">4</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value4"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 10,
message: "Name should be less than 10 characters",
},
}}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">5</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value5"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 10,
message: "Name should be less than 10 characters",
},
}}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">6</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value6"
type="name"
placeholder="Value"
autoComplete="off"
register={register}
validations={{
required: "value is required",
maxLength: {
value: 10,
message: "Name should be less than 10 characters",
},
}}
/>
</span>
</span>
</div>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={() => handleClose()}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{data && data.length > 0
? isSubmitting
? "Updating Points..."
: "Update Points"
: isSubmitting
? "Creating Points..."
: "Create Points"}
</PrimaryButton>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

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