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
# Cache
REDIS_URL=redis://redis:6379/
# SMPT
# SMTP
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT="587"
EMAIL_USE_TLS="1"
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
# AWS
AWS_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_S3_BUCKET_NAME=""
AWS_S3_ENDPOINT_URL=""
# FE
WEB_URL="localhost/"
# OAUTH

View File

@ -3,7 +3,15 @@ import uuid
import random
from django.contrib.auth.hashers import make_password
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
@ -174,3 +182,29 @@ def update_label_color():
except Exception as e:
print(e)
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,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
SlackProjectSyncSerializer,
)
from .importer import ImporterSerializer
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",
"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,
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,
## End States
# Estimates
EstimateViewSet,
EstimatePointViewSet,
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
## End Estimates
@ -133,6 +131,7 @@ from plane.api.views import (
GithubIssueSyncViewSet,
GithubCommentSyncViewSet,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
## End Integrations
# Importer
ServiceIssueImportSummaryEndpoint,
@ -146,6 +145,9 @@ from plane.api.views import (
# Gpt
GPTIntegrationEndpoint,
## End Gpt
# Release Notes
ReleaseNotesEndpoint,
## End Release Notes
)
@ -507,62 +509,34 @@ urlpatterns = [
name="project-state",
),
# End States ##
# States
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",
),
# Estimates
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/bulk-estimate-points/",
BulkEstimatePointEndpoint.as_view(),
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
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
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
@ -1237,6 +1211,26 @@ urlpatterns = [
),
),
## 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
# Importer
path(
@ -1284,4 +1278,11 @@ urlpatterns = [
name="importer",
),
## 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,
GithubRepositoriesEndpoint,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
)
from .importer import (
@ -133,8 +134,9 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint
from .estimate import (
EstimateViewSet,
EstimatePointViewSet,
ProjectEstimatePointEndpoint,
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.permissions import IsAuthenticated
from rest_framework.exceptions import NotFound
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend
# Module imports
@ -39,7 +39,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
try:
return self.model.objects.all()
except Exception as e:
print(e)
capture_exception(e)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
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
)
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):
subquery = CycleFavorite.objects.filter(
user=self.request.user,
@ -181,6 +203,22 @@ class CycleIssueViewSet(BaseViewSet):
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):
return self.filter_queryset(
super()
@ -286,9 +324,9 @@ class CycleIssueViewSet(BaseViewSet):
# Get all CycleIssues already created
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
records_to_update = []
update_cycle_issue_activity = []
record_to_create = []
records_to_update = []
for issue in issues:
cycle_issue = [
@ -333,7 +371,7 @@ class CycleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
type="issue.activity.updated",
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": issues}),
actor_id=str(self.request.user.id),
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 plane.api.permissions import ProjectEntityPermission
from plane.db.models import Project, Estimate, EstimatePoint
from plane.api.serializers import EstimateSerializer, EstimatePointSerializer
class EstimateViewSet(BaseViewSet):
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,
)
from plane.api.serializers import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
)
class ProjectEstimatePointEndpoint(BaseAPIView):
@ -141,17 +42,35 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
)
class BulkEstimatePointEndpoint(BaseAPIView):
class BulkEstimatePointEndpoint(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
model = Estimate
serializer_class = EstimateSerializer
def post(self, request, slug, project_id, estimate_id):
def list(self, request, slug, project_id):
try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project=project_id
estimates = Estimate.objects.filter(
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", [])
if not len(estimate_points) or len(estimate_points) > 8:
@ -160,6 +79,18 @@ class BulkEstimatePointEndpoint(BaseAPIView):
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(
[
EstimatePoint(
@ -178,9 +109,17 @@ class BulkEstimatePointEndpoint(BaseAPIView):
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:
return Response(
{"error": "Estimate does not exist"},
@ -193,14 +132,58 @@ class BulkEstimatePointEndpoint(BaseAPIView):
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:
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", [])):
return Response(
{"error": "Estimate points are required"},
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 = EstimatePoint.objects.filter(
@ -212,7 +195,6 @@ class BulkEstimatePointEndpoint(BaseAPIView):
estimate_id=estimate_id,
)
print(estimate_points)
updated_estimate_points = []
for estimate_point in estimate_points:
# Find the data for that estimate point
@ -221,24 +203,50 @@ class BulkEstimatePointEndpoint(BaseAPIView):
for point in estimate_points_data
if point.get("id") == str(estimate_point.id)
]
print(estimate_point_data)
if len(estimate_point_data):
estimate_point.value = estimate_point_data[0].get(
"value", estimate_point.value
)
updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update(
updated_estimate_points, ["value"], batch_size=10
try:
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:
return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
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(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,

View File

@ -28,6 +28,7 @@ from plane.db.models import (
Module,
ModuleLink,
ModuleIssue,
Label,
)
from plane.api.serializers import (
ImporterSerializer,
@ -104,7 +105,7 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
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,
@ -235,9 +236,20 @@ class ImportServiceEndpoint(BaseAPIView):
def delete(self, request, slug, service, pk):
try:
importer = Importer.objects.filter(
importer = Importer.objects.get(
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()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
@ -247,6 +259,27 @@ class ImportServiceEndpoint(BaseAPIView):
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):
def post(self, request, slug, project_id, service, importer_id):
@ -487,48 +520,59 @@ class BulkImportModulesEndpoint(BaseAPIView):
ignore_conflicts=True,
)
_ = ModuleLink.objects.bulk_create(
[
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,
)
modules = Module.objects.filter(id__in=[module.id for module in modules])
bulk_module_issues = []
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
]
if len(modules) == len(modules_data):
_ = ModuleLink.objects.bulk_create(
[
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,
)
_ = ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=100, ignore_conflicts=True
)
bulk_module_issues = []
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)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
_ = ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=100, ignore_conflicts=True
)
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:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND

View File

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

View File

@ -27,6 +27,7 @@ from plane.utils.integrations.github import (
)
from plane.api.permissions import WorkSpaceAdminPermission
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
model = Integration
@ -101,7 +102,6 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
WorkSpaceAdminPermission,
]
def get_queryset(self):
return (
super()
@ -112,21 +112,30 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
def create(self, request, slug, provider):
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)
integration = Integration.objects.get(provider=provider)
config = {}
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)
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
bot_user = User.objects.create(
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
import json
import random
from itertools import groupby, chain
from itertools import chain
# 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.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db.models.functions import Coalesce
# Third Party imports
from rest_framework.response import Response
@ -46,6 +47,7 @@ from plane.db.models import (
Label,
IssueLink,
IssueAttachment,
State,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@ -590,8 +592,31 @@ class SubIssuesEndpoint(BaseAPIView):
.prefetch_related("labels")
)
serializer = IssueLiteSerializer(sub_issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
state_distribution = (
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:
capture_exception(e)
return Response(

View File

@ -109,6 +109,28 @@ class ModuleViewSet(BaseViewSet):
.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):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id)
@ -158,6 +180,22 @@ class ModuleIssueViewSet(BaseViewSet):
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):
return self.filter_queryset(
super()
@ -302,7 +340,7 @@ class ModuleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
type="issue.activity.updated",
type="module.activity.created",
requested_data=json.dumps({"modules_list": issues}),
actor_id=str(self.request.user.id),
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_simplejwt.tokens import RefreshToken
from rest_framework import status
from sentry_sdk import capture_exception
# sso authentication
from google.oauth2 import id_token
from google.auth.transport import requests as google_auth_request
@ -48,7 +48,7 @@ def validate_google_token(token, client_id):
}
return data
except Exception as e:
print(e)
capture_exception(e)
raise exceptions.AuthenticationFailed("Error with Google connection.")
@ -305,8 +305,7 @@ class OauthEndpoint(BaseAPIView):
)
return Response(data, status=status.HTTP_201_CREATED)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{
"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,
)
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):
serializer_class = PageBlockSerializer
@ -344,7 +374,7 @@ class RecentPagesEndpoint(BaseAPIView):
status=status.HTTP_200_OK,
)
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,

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)
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,

View File

@ -1,6 +1,9 @@
# Python imports
from itertools import groupby
# Django imports
from django.db import IntegrityError
# Third party imports
from rest_framework.response import Response
from rest_framework import status
@ -8,10 +11,10 @@ from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import StateSerializer
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import State
from plane.db.models import State, Issue
class StateViewSet(BaseViewSet):
@ -36,6 +39,25 @@ class StateViewSet(BaseViewSet):
.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):
try:
state_dict = dict()
@ -66,6 +88,17 @@ class StateViewSet(BaseViewSet):
{"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()
return Response(status=status.HTTP_204_NO_CONTENT)
except State.DoesNotExist:

View File

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

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
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)
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!"

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
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}/"
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!"

View File

@ -27,6 +27,7 @@ from plane.db.models import (
User,
)
from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_email
@shared_task
@ -40,7 +41,7 @@ def service_importer(service, importer_id):
# Check if we need to import users as well
if len(users):
# For all invited users create the uers
# For all invited users create the users
new_users = User.objects.bulk_create(
[
User(
@ -56,6 +57,15 @@ def service_importer(service, importer_id):
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(
email__in=[
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(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
@ -683,8 +570,6 @@ def update_issue_activity(
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"cycles_list": track_cycles,
"modules_list": track_modules,
"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(
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.updated": update_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.updated": update_link_activity,
"link.activity.deleted": delete_link_activity,
@ -947,6 +1007,5 @@ def issue_activity(
)
return
except Exception as e:
print(e)
capture_exception(e)
return

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
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}"
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"
@ -29,6 +30,5 @@ def magic_link(email, key, token, current_site):
msg.send()
return
except Exception as e:
print(e)
capture_exception(e)
return

View File

@ -2,6 +2,7 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
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}"
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"
@ -49,6 +50,5 @@ def project_invitation(email, project_id, token, current_site):
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
return
except Exception as e:
print(e)
capture_exception(e)
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
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"

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,
GithubIssueSync,
GithubCommentSync,
SlackProjectSync,
)
from .importer import Importer

View File

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

View File

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

View File

@ -1,2 +1,3 @@
from .base import Integration, WorkspaceIntegration
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_fields=("module", "member"),
)
view_props = models.JSONField(default=dict)
class Meta:
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:
first_name = instance.first_name.capitalize()
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 ✈️!"

View File

@ -174,11 +174,12 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Host for sending e-mail.
EMAIL_HOST = os.environ.get("EMAIL_HOST")
# Port for sending e-mail.
EMAIL_PORT = 587
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
# Optional SMTP authentication information for EMAIL_HOST.
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
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 = {

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_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"
# 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.
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)
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_BROKER_URL = broker_url
if DOCKERIZED:
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"
# 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.
AWS_S3_KEY_PREFIX = ""
@ -204,3 +204,5 @@ broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={cert
CELERY_RESULT_BACKEND = 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 cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.backends import default_backend
from django.conf import settings
def get_jwt_token():
@ -128,3 +129,24 @@ def get_github_repo_details(access_tokens_url, owner, repo):
).json()
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
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):
if method == "GET":
priorties = params.get("priority").split(",")
@ -192,6 +203,7 @@ def issue_filters(query_params, method):
ISSUE_FILTER = {
"state": filter_state,
"estimate_point": filter_estimate_point,
"priority": filter_priority,
"parent": filter_parent,
"labels": filter_labels,

View File

@ -1,5 +1,6 @@
# Replace with your instance Public IP
# NEXT_PUBLIC_API_BASE_URL = "http://localhost"
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
NEXT_PUBLIC_GOOGLE_CLIENTID=""
NEXT_PUBLIC_GITHUB_APP_NAME=""
NEXT_PUBLIC_GITHUB_ID=""
@ -8,3 +9,4 @@ NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_SENTRY=0
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=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">
{(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-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 className="ml-3">
<p className="text-sm font-medium text-green-800">
<p className="text-sm font-medium text-green-500">
{codeResent
? "Please check your mail for new code."
: "Please check your mail for code."}
@ -141,7 +141,9 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
<button
type="button"
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={() => {
setIsCodeResending(true);
@ -174,7 +176,8 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
className="w-full text-center"
size="md"
onClick={handleSubmit(handleSignin)}
loading={isSubmitting || (!isValid && isDirty)}
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? "Signing in..." : "Sign in"}
</PrimaryButton>

View File

@ -50,7 +50,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
if (!error?.response?.data) return;
Object.keys(error.response.data).forEach((key) => {
const err = error.response.data[key];
console.log("err", err);
console.log(err);
setError(key as keyof EmailPasswordFormValues, {
type: "manual",
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="ml-auto text-sm">
<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>
</div>
</div>
@ -102,7 +104,8 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<SecondaryButton
type="submit"
className="w-full text-center"
loading={isSubmitting || (!isValid && isDirty)}
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? "Signing in..." : "Sign In"}
</SecondaryButton>

View File

@ -33,11 +33,11 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
}, []);
return (
<div className="px-1 w-full">
<div className="w-full px-1">
<Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
>
<button className="flex w-full items-center justify-center gap-3 rounded-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" />
<span>Sign In with Github</span>
</button>

View File

@ -47,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
return (
<>
<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"
/>
</div>
<h1 className="text-xl font-medium text-gray-900">
Oops! You are not authorized to view this page
</h1>
<h1 className="text-xl font-medium">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 ? (
<p>
You have signed in as {user.email}. <br />
<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>{" "}
with different account that has access to this page.
</p>
@ -53,7 +51,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
<p>
You need to{" "}
<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>{" "}
with an account that has access to this page.
</p>

View File

@ -31,7 +31,7 @@ export const JoinProject: React.FC = () => {
project_ids: [projectId as string],
})
.then(async () => {
await mutate(USER_PROJECT_VIEW(workspaceSlug.toString()));
await mutate(USER_PROJECT_VIEW(projectId.toString()));
setIsJoiningProject(false);
})
.catch((err) => {
@ -45,9 +45,9 @@ export const JoinProject: React.FC = () => {
<div className="h-44 w-72">
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
</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">
You are not a member of this project, but you can join this project by clicking the button
below.

View File

@ -20,12 +20,12 @@ export const NotAWorkspaceMember = () => {
<div className="space-y-8 text-center">
<div className="space-y-2">
<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
an invitation or check your pending invitations.
</p>
</div>
<div className="flex items-center gap-2 justify-center">
<div className="flex items-center justify-center gap-2">
<Link href="/invitations">
<a>
<SecondaryButton>Check pending invites</SecondaryButton>

View File

@ -16,7 +16,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
<div className="flex items-center">
<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()}
>
<ArrowLeftIcon className="h-3 w-3" />
@ -37,7 +37,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
<>
{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" : ""}`}>
{icon ?? null}
{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
import { IIssue } from "types";
// 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
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 { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -44,6 +44,7 @@ import {
ChangeIssueState,
ChangeIssuePriority,
ChangeIssueAssignee,
ChangeInterfaceTheme,
} from "components/command-palette";
import { BulkDeleteIssuesModal } from "components/core";
import { CreateUpdateCycleModal } from "components/cycles";
@ -379,7 +380,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100"
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>
<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"
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
filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
@ -415,7 +416,7 @@ export const CommandPalette: React.FC = () => {
>
{issueId && issueDetails && (
<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?.name}
</p>
@ -423,11 +424,11 @@ export const CommandPalette: React.FC = () => {
)}
<div className="relative">
<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"
/>
<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}
value={searchTerm}
onValueChange={(e) => {
@ -441,7 +442,9 @@ export const CommandPalette: React.FC = () => {
resultsCount === 0 &&
searchTerm !== "" &&
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) && (
@ -502,8 +505,11 @@ export const CommandPalette: React.FC = () => {
value={value}
className="focus:outline-none"
>
<div className="flex items-center gap-2 overflow-hidden text-gray-700">
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" />
<div className="flex items-center gap-2 overflow-hidden text-brand-secondary">
<Icon
className="h-4 w-4 text-brand-secondary"
color="#6b7280"
/>
<p className="block flex-1 truncate">{item.name}</p>
</div>
</Command.Item>
@ -528,8 +534,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<Squares2X2Icon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
Change state...
</div>
</Command.Item>
@ -541,8 +547,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<ChartBarIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
Change priority...
</div>
</Command.Item>
@ -554,8 +560,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<UsersIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<UsersIcon className="h-4 w-4 text-brand-secondary" />
Assign to...
</div>
</Command.Item>
@ -566,15 +572,15 @@ export const CommandPalette: React.FC = () => {
}}
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) ? (
<>
<UserMinusIcon className="h-4 w-4 text-gray-500" />
<UserMinusIcon className="h-4 w-4 text-brand-secondary" />
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
</>
)}
@ -582,8 +588,8 @@ export const CommandPalette: React.FC = () => {
</Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-gray-700">
<TrashIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<TrashIcon className="h-4 w-4 text-brand-secondary" />
Delete issue
</div>
</Command.Item>
@ -594,16 +600,19 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<LinkIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<LinkIcon className="h-4 w-4 text-brand-secondary" />
Copy issue URL to clipboard
</div>
</Command.Item>
</>
)}
<Command.Group heading="Issue">
<Command.Item onSelect={createNewIssue} className="focus:bg-gray-200">
<div className="flex items-center gap-2 text-gray-700">
<Command.Item
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" />
Create new issue
</div>
@ -617,7 +626,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewProject}
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" />
Create new project
</div>
@ -633,7 +642,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewCycle}
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" />
Create new cycle
</div>
@ -646,7 +655,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewModule}
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" />
Create new module
</div>
@ -656,7 +665,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="View">
<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" />
Create new view
</div>
@ -666,7 +675,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="Page">
<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" />
Create new page
</div>
@ -685,7 +694,7 @@ export const CommandPalette: React.FC = () => {
}}
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" />
Search settings...
</div>
@ -696,11 +705,24 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewWorkspace}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<FolderPlusIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<FolderPlusIcon className="h-4 w-4 text-brand-secondary" />
Create new workspace
</div>
</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 heading="Help">
<Command.Item
@ -713,8 +735,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<RocketLaunchIcon className="h-4 w-4 text-brand-secondary" />
Open keyboard shortcuts
</div>
</Command.Item>
@ -725,8 +747,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<DocumentIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<DocumentIcon className="h-4 w-4 text-brand-secondary" />
Open Plane documentation
</div>
</Command.Item>
@ -737,7 +759,7 @@ export const CommandPalette: React.FC = () => {
}}
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" />
Join our Discord
</div>
@ -752,7 +774,7 @@ export const CommandPalette: React.FC = () => {
}}
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" />
Report a bug
</div>
@ -764,8 +786,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" />
Chat with us
</div>
</Command.Item>
@ -779,8 +801,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings()}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
General
</div>
</Command.Item>
@ -788,8 +810,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("members")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Members
</div>
</Command.Item>
@ -797,8 +819,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("billing")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Billings and Plans
</div>
</Command.Item>
@ -806,8 +828,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("integrations")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Integrations
</div>
</Command.Item>
@ -815,8 +837,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("import-export")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Import/Export
</div>
</Command.Item>
@ -842,6 +864,9 @@ export const CommandPalette: React.FC = () => {
setIsPaletteOpen={setIsPaletteOpen}
/>
)}
{page === "change-interface-theme" && (
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
)}
</Command.List>
</Command>
</Dialog.Panel>

View File

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

View File

@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { MacCommandIcon } from "components/icons";
import { CommandIcon } from "components/icons";
// ui
import { Input } from "components/ui";
@ -71,7 +71,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100"
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>
<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"
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">
<div className="bg-white p-5">
<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-brand-surface-2 p-5">
<div className="sm:flex sm:items-start">
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
<Dialog.Title
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>
<button type="button" onClick={() => setIsOpen(false)}>
<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"
/>
</button>
</span>
</Dialog.Title>
<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">
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-gray-500" />
<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-brand-secondary" />
<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"
name="search"
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 className="flex flex-col gap-y-3">
<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">
{shortcut.keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
<MacCommandIcon />
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</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">
{key === "Ctrl" ? <MacCommandIcon /> : key}
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key}
</kbd>
)}
</span>
@ -145,7 +151,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
))
) : (
<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{" "}
<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">
{shortcuts.map(({ keys, description }, index) => (
<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">
{keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
<MacCommandIcon />
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</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">
{key === "Ctrl" ? <MacCommandIcon /> : key}
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key}
</kbd>
)}
</span>

View File

@ -17,7 +17,7 @@ type Props = {
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@ -81,7 +81,7 @@ export const AllBoards: React.FC<Props> = ({
return (
<div
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">
{currentState &&
@ -92,7 +92,7 @@ export const AllBoards: React.FC<Props> = ({
: addSpaceIfCamelCase(singleGroup)}
</h4>
</div>
<span className="text-xs text-gray-500">0</span>
<span className="text-xs text-brand-secondary">0</span>
</div>
);
})}

View File

@ -96,17 +96,17 @@ export const BoardHeader: React.FC<Props> = ({
switch (selectedGroup) {
case "state":
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor);
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center");
icon = getPriorityIcon(groupTitle, "text-lg");
break;
case "labels":
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = (
<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 }}
/>
);
@ -123,8 +123,8 @@ export const BoardHeader: React.FC<Props> = ({
return (
<div
className={`flex justify-between items-center px-1 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
className={`flex items-center justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : ""
}`}
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
@ -143,7 +143,9 @@ export const BoardHeader: React.FC<Props> = ({
{getGroupTitle()}
</h2>
<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}
</span>
@ -153,7 +155,7 @@ export const BoardHeader: React.FC<Props> = ({
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<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={() => {
setIsCollapsed((prevData) => !prevData);
}}
@ -167,7 +169,7 @@ export const BoardHeader: React.FC<Props> = ({
{!isCompleted && (
<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}
>
<PlusIcon className="h-4 w-4" />

View File

@ -29,7 +29,7 @@ type Props = {
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@ -66,7 +66,7 @@ export const SingleBoard: React.FC<Props> = ({
}, [currentState]);
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"}`}>
<BoardHeader
addIssueToState={addIssueToState}
@ -81,7 +81,7 @@ export const SingleBoard: React.FC<Props> = ({
{(provided, snapshot) => (
<div
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"}`}
ref={provided.innerRef}
{...provided.droppableProps}
@ -91,15 +91,17 @@ export const SingleBoard: React.FC<Props> = ({
<div
className={`absolute ${
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
className={`absolute ${
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{" "}
{replaceUnderscoreIfSnakeCase(orderBy ?? "created_at")}
{replaceUnderscoreIfSnakeCase(
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
)}
</div>
</>
)}
@ -128,7 +130,8 @@ export const SingleBoard: React.FC<Props> = ({
handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox}
removeIssue={() => {
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id);
if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
}}
isCompleted={isCompleted}
userAuth={userAuth}
@ -146,7 +149,7 @@ export const SingleBoard: React.FC<Props> = ({
{type === "issue" ? (
<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}
>
<PlusIcon className="h-4 w-4" />
@ -158,7 +161,7 @@ export const SingleBoard: React.FC<Props> = ({
customButton={
<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" />
Add Issue

View File

@ -261,8 +261,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
</a>
</ContextMenu>
<div
className={`mb-3 rounded bg-white shadow ${
snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
className={`mb-3 rounded bg-brand-base shadow ${
snapshot.isDragging ? "border-2 border-brand-accent shadow-lg" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
@ -312,12 +312,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a>
{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}
</div>
)}
<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 }}
>
{truncateText(issue.name, 100)}
@ -349,7 +349,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
/>
)}
{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"}
</div>
)}
@ -358,7 +358,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{issue.label_details.map((label) => (
<div
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
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
@ -389,20 +389,20 @@ export const SingleBoardIssue: React.FC<Props> = ({
/>
)}
{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}`}>
<div className="flex items-center gap-1 text-gray-500">
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
<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 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}`}>
<div className="flex items-center gap-1 text-gray-500">
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
<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>

View File

@ -121,7 +121,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
leaveFrom="opacity-100 scale-100"
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>
<Combobox
onChange={(val: string) => {
@ -136,12 +136,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
>
<div className="relative m-1">
<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"
/>
<input
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..."
onChange={(event) => setQuery(event.target.value)}
/>
@ -154,7 +154,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
{filteredIssues.length > 0 ? (
<li className="p-2">
{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
</h2>
)}
@ -166,7 +166,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
value={issue.id}
className={({ active }) =>
`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,
}}
/>
<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}
</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">
<LayerDiagonalIcon height="56" width="56" />
<h3 className="text-gray-500">
<h3 className="text-brand-secondary">
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>
</div>
)}

View File

@ -26,14 +26,17 @@ import {
import { Popover, Transition } from "@headlessui/react";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CustomMenu } from "components/ui";
import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
// icon
import {
CheckIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
// hooks
import useIssuesView from "hooks/use-issues-view";
// services
import issuesService from "services/issues.service";
import cyclesService from "services/cycles.service";
@ -48,20 +51,28 @@ import { IIssue } from "types";
// constant
import { monthOptions, yearOptions } from "constants/calendar";
import modulesService from "services/modules.service";
import { getStateGroupIcon } from "components/icons";
type Props = {
addIssueToDate: (date: string) => void;
};
interface ICalendarRange {
startDate: Date;
endDate: Date;
}
export const CalendarView = () => {
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false);
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true);
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
const [showWeekEnds, setShowWeekEnds] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const [isMonthlyView, setIsMonthlyView] = useState(true);
const [showAllIssues, setShowAllIssues] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { params } = useIssuesView();
const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({
startDate: startOfWeek(currentDate),
endDate: lastDayOfWeek(currentDate),
@ -77,11 +88,13 @@ export const CalendarView = () => {
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
workspaceSlug && projectId
? () =>
issuesService.getIssuesWithParams(
workspaceSlug as string,
projectId as string,
targetDateFilter
)
issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, {
...params,
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
calendarDateRange.endDate
)};before`,
group_by: null,
})
: null
);
@ -95,7 +108,13 @@ export const CalendarView = () => {
workspaceSlug as string,
projectId as string,
cycleId as string,
targetDateFilter
{
...params,
target_date: `${renderDateFormat(
calendarDateRange.startDate
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
group_by: null,
}
)
: null
);
@ -110,7 +129,13 @@ export const CalendarView = () => {
workspaceSlug as string,
projectId as string,
moduleId as string,
targetDateFilter
{
...params,
target_date: `${renderDateFormat(
calendarDateRange.startDate
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
group_by: null,
}
)
: null
);
@ -127,12 +152,16 @@ export const CalendarView = () => {
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 filterIssue =
calendarIssues && calendarIssues.length > 0
? (calendarIssues as IIssue[]).filter(
? calendarIssues.filter(
(issue) =>
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
)
@ -198,17 +227,17 @@ export const CalendarView = () => {
});
};
return (
return calendarIssues ? (
<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="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">
{({ 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">
<span className="text-black">{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "yyyy")}</span>
</div>
</Popover.Button>
@ -222,30 +251,30 @@ export const CalendarView = () => {
leaveFrom="opacity-100 translate-y-0"
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]">
<div className="flex justify-center items-center text-sm gap-5 px-2 py-2">
<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 items-center justify-center gap-5 px-2 py-2 text-sm">
{yearOptions.map((year) => (
<button
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
className={` ${
isSameYear(year.value, currentDate)
? "text-sm font-medium text-gray-800"
: "text-xs text-gray-400 "
} hover:text-sm hover:text-gray-800 hover:font-medium `}
? "text-sm font-medium text-brand-base"
: "text-xs text-brand-secondary "
} hover:text-sm hover:font-medium hover:text-brand-base`}
>
{year.label}
</button>
))}
</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) => (
<button
onClick={() =>
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)
? "font-medium text-gray-800"
? "font-medium text-brand-base"
: ""
}`}
>
@ -295,9 +324,9 @@ export const CalendarView = () => {
</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
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={() => {
if (isMonthlyView) {
updateDate(new Date());
@ -310,13 +339,12 @@ export const CalendarView = () => {
}
}}
>
Today{" "}
Today
</button>
<CustomMenu
customButton={
<div
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 `}
>
<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 ">
{isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
@ -330,7 +358,7 @@ export const CalendarView = () => {
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">
<span className="flex items-center gap-2">Monthly View</span>
@ -349,7 +377,7 @@ export const CalendarView = () => {
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">
<span className="flex items-center gap-2">Weekly View</span>
@ -360,25 +388,12 @@ export const CalendarView = () => {
/>
</div>
</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>
<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 ${
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>
<ToggleSwitch
value={showWeekEnds}
onChange={() => setShowWeekEnds(!showWeekEnds)}
/>
</div>
</CustomMenu>
</div>
@ -392,7 +407,7 @@ export const CalendarView = () => {
{weeks.map((date, index) => (
<div
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
? showWeekEnds
? (index + 1) % 7 === 0
@ -417,54 +432,93 @@ export const CalendarView = () => {
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `}
>
{currentViewDaysData.map((date, index) => (
<StrictModeDroppable droppableId={date.date}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.droppableProps}
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 ${
showWeekEnds
? (index + 1) % 7 === 0
{currentViewDaysData.map((date, index) => {
const totalIssues = date.issues.length;
return (
<StrictModeDroppable droppableId={date.date}>
{(provided) => (
<div
key={index}
ref={provided.innerRef}
{...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"
: (index + 1) % 5 === 0
? ""
: "border-r"
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{date.issues.length > 0 &&
date.issues.map((issue: IIssue, index) => (
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full cursor-pointer truncate rounded bg-white p-1.5 hover:scale-105 ${
snapshot.isDragging ? "shadow-lg" : ""
}`}
>
<Link
href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}
className="w-full"
>
{issue.name}
</Link>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
))}
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{totalIssues > 0 &&
date.issues
.slice(0, showAllIssues ? totalIssues : 4)
.map((issue: IIssue, index) => (
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...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 ${
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
}`}
>
<Link
href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
>
<a className="flex w-full items-center gap-2">
{getStateGroupIcon(
issue.state_detail.group,
"12",
"12",
issue.state_detail.color
)}
{issue.name}
</a>
</Link>
</div>
)}
</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>
</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"
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>
<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"
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>
<Controller
control={control}
@ -139,11 +139,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
<Combobox as="div" {...field} multiple>
<div className="relative m-1">
<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"
/>
<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..."
onChange={(e) => setQuery(e.target.value)}
/>
@ -151,26 +151,26 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
<Combobox.Options
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 ? (
<li className="p-2">
{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
</h2>
)}
<ul className="text-sm text-gray-700">
<ul className="text-sm text-brand-base">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue.id}
className={({ active }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
}`
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
active ? "bg-brand-surface-2 text-brand-base" : ""
} ${selected ? "text-brand-base" : ""}`
}
>
{({ selected }) => (
@ -182,7 +182,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
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}
</span>
{issue.name}
@ -194,10 +194,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
</li>
) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" />
<h3 className="text-gray-500">
<LayerDiagonalIcon height="52" width="52" />
<h3 className="text-sm text-brand-secondary">
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>
</div>
)}

View File

@ -55,43 +55,47 @@ const activityDetails: {
},
modules: {
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: {
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: {
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: {
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: {
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: {
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: {
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: {
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: {
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: {
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: {
message: "updated the link",
@ -153,11 +157,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
) {
const { workspace_detail, project, issue } = activity;
value = (
<span className="text-gray-600">
<span className="text-brand-secondary">
created{" "}
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
<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>
</Link>
</span>
@ -198,7 +202,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
if (activity.field === "comment") {
return (
<div key={activity.id}>
<div key={activity.id} className="mt-2">
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
@ -217,9 +221,9 @@ export const Feeds: React.FC<any> = ({ activities }) => (
</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
className="h-3.5 w-3.5 text-gray-400"
className="h-3.5 w-3.5 text-brand-secondary"
aria-hidden="true"
/>
</span>
@ -230,7 +234,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
{activity.actor_detail.first_name}
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
</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)}
</p>
</div>
@ -242,9 +246,8 @@ export const Feeds: React.FC<any> = ({ activities }) => (
: activity.old_value
}
editable={false}
onBlur={() => ({})}
noBorder
customClassName="text-xs bg-gray-100"
customClassName="text-xs border border-brand-base bg-brand-base"
/>
</div>
</div>
@ -259,7 +262,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div className="relative pb-1">
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
<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"
/>
) : null}
@ -268,7 +271,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div>
<div className="relative px-1.5">
<div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-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 ? (
activityDetails[activity.field as keyof typeof activityDetails]?.icon
) : activity.actor_detail.avatar &&
@ -292,7 +295,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
</div>
</div>
<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">
{activity.actor_detail.first_name}
{activity.actor_detail.is_bot
@ -300,7 +303,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
: " " + activity.actor_detail.last_name}
</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>
</div>
</div>

View File

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

View File

@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC<Props> = ({
return (
<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"
}`}
>

View File

@ -65,7 +65,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
return (
<Popover className="relative z-[2]" ref={ref}>
<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)}
>
{label}
@ -79,16 +79,16 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-white 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]">
<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 border-brand-base bg-brand-surface-2 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
<Tab.Group>
<Tab.List as="span" className="inline-block rounded bg-gray-200 p-1">
<Tab.List as="span" className="inline-block rounded bg-brand-surface-2 p-1">
{tabOptions.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`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"
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>
<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"
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">
<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
</Dialog.Title>
<div className="space-y-3">
@ -135,7 +135,7 @@ export const ImageUploadModal: React.FC<Props> = ({
{...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 ${
(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
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
</button>
@ -157,7 +157,7 @@ export const ImageUploadModal: React.FC<Props> = ({
) : (
<>
<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
? "Drop image here to upload"
: "Drag & drop image here"}

View File

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

View File

@ -10,10 +10,14 @@ import { Popover, Transition } from "@headlessui/react";
// components
import { SelectFilters } from "components/views";
// ui
import { CustomMenu } from "components/ui";
import { CustomMenu, ToggleSwitch } from "components/ui";
// icons
import { ChevronDownIcon, ListBulletIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
import { Squares2X2Icon } from "@heroicons/react/20/solid";
import {
ChevronDownIcon,
ListBulletIcon,
Squares2X2Icon,
CalendarDaysIcon,
} from "@heroicons/react/24/outline";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
@ -53,30 +57,30 @@ export const IssuesFilterView: React.FC = () => {
<div className="flex items-center gap-x-1">
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
issueView === "list" ? "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-brand-surface-2" : ""
}`}
onClick={() => setIssueView("list")}
>
<ListBulletIcon className="h-4 w-4" />
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
issueView === "kanban" ? "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-brand-surface-2" : ""
}`}
onClick={() => setIssueView("kanban")}
>
<Squares2X2Icon className="h-4 w-4" />
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
issueView === "calendar" ? "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-brand-surface-2" : ""
}`}
onClick={() => setIssueView("calendar")}
>
<CalendarDaysIcon className="h-4 w-4" />
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
</button>
</div>
<SelectFilters
@ -113,8 +117,8 @@ export const IssuesFilterView: React.FC = () => {
{({ open }) => (
<>
<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 ${
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
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-brand-surface-1 text-brand-base" : "text-brand-secondary"
}`}
>
View
@ -130,55 +134,59 @@ export const IssuesFilterView: React.FC = () => {
leaveFrom="opacity-100 translate-y-0"
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">
<div className="relative divide-y-2">
<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 divide-brand-base">
<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">
<h4 className="text-gray-600">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-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>
<h4 className="text-brand-secondary">Issue type</h4>
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
@ -200,62 +208,56 @@ export const IssuesFilterView: React.FC = () => {
))}
</CustomMenu>
</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 (
<button
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-theme bg-theme text-white"
: "border-gray-300"
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
{issueView !== "calendar" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Show empty states</h4>
<ToggleSwitch
value={showEmptyGroups}
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
/>
</div>
<div className="relative flex justify-end gap-x-3">
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
</button>
);
})}
</div>
<button
type="button"
className="font-medium text-brand-accent"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</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>
</Popover.Panel>
</Transition>

View File

@ -46,7 +46,7 @@ import {
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATE_LIST,
STATES_LIST,
} from "constants/fetch-keys";
// image
@ -103,7 +103,7 @@ export const IssuesView: React.FC<Props> = ({
} = useIssuesView();
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
@ -280,6 +280,17 @@ export const IssuesView: React.FC<Props> = ({
[setCreateIssueModal, setPreloadedData, selectedGroup]
);
const addIssueToDate = useCallback(
(date: string) => {
setCreateIssueModal(true);
setPreloadedData({
target_date: date,
actionType: "createIssue",
});
},
[setCreateIssueModal, setPreloadedData]
);
const makeIssueCopy = useCallback(
(issue: IIssue) => {
setCreateIssueModal(true);
@ -303,10 +314,26 @@ export const IssuesView: React.FC<Props> = ({
);
const removeIssueFromCycle = useCallback(
(bridgeId: string) => {
(bridgeId: string, issueId: string) => {
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
.removeIssueFromCycle(
@ -315,8 +342,12 @@ export const IssuesView: React.FC<Props> = ({
cycleId as string,
bridgeId
)
.then((res) => {
console.log(res);
.then(() => {
setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
})
.catch((e) => {
console.log(e);
@ -326,10 +357,26 @@ export const IssuesView: React.FC<Props> = ({
);
const removeIssueFromModule = useCallback(
(bridgeId: string) => {
(bridgeId: string, issueId: string) => {
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
.removeIssueFromModule(
@ -338,8 +385,12 @@ export const IssuesView: React.FC<Props> = ({
moduleId as string,
bridgeId
)
.then((res) => {
console.log(res);
.then(() => {
setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
})
.catch((e) => {
console.log(e);
@ -391,49 +442,48 @@ export const IssuesView: React.FC<Props> = ({
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
{issueView !== "calendar" && (
<>
<div
className={`flex items-center justify-between gap-2 ${
issueView === "list" && areFiltersApplied ? "px-8 mt-6" : "-mt-2"
}`}
>
<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>
<>
<div
className={`flex items-center justify-between gap-2 ${
issueView === "list" ? (areFiltersApplied ? "mt-6 px-8" : "") : "-mt-2"
}`}
>
<FilterList filters={filters} setFilters={setFilters} />
{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}>
<StrictModeDroppable droppableId="trashBox">
{(provided, snapshot) => (
<div
className={`${
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 ${
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
} 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/100 text-white" : ""
} duration-200`}
ref={provided.innerRef}
{...provided.droppableProps}
@ -487,7 +537,7 @@ export const IssuesView: React.FC<Props> = ({
userAuth={memberRole}
/>
) : (
<CalendarView />
<CalendarView addIssueToDate={addIssueToDate} />
)}
</>
) : type === "issue" ? (
@ -508,8 +558,8 @@ export const IssuesView: React.FC<Props> = ({
title="Create a new issue"
description={
<span>
Use <pre className="inline rounded bg-gray-200 px-2 py-1">C</pre> shortcut to
create a new issue
Use <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>{" "}
shortcut to create a new issue
</span>
}
Icon={PlusIcon}

View File

@ -56,7 +56,7 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child>
<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"
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)}>
<div>
<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
</Dialog.Title>
<div className="mt-2 space-y-3">

View File

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

View File

@ -44,7 +44,6 @@ import {
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
import { DIVIDER } from "@blueprintjs/core/lib/esm/common/classes";
type Props = {
type?: string;
@ -216,149 +215,151 @@ export const SingleListIssue: React.FC<Props> = ({
</ContextMenu.Item>
</a>
</ContextMenu>
<div className="border-b mx-6 border-gray-300 last:border-b-0">
<div
className="flex items-center justify-between gap-2 py-3"
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
}}
>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<div
className="flex flex-wrap items-center justify-between gap-2 border-b border-brand-base bg-brand-base last:border-b-0"
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
}}
>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<div className="flex-grow cursor-pointer px-4 pt-2.5 md:py-2.5">
<a className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
tooltipHeading="Issue 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}
</span>
</Tooltip>
)}
<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>
</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>
</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>
</>

View File

@ -37,7 +37,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string) => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@ -104,17 +104,17 @@ export const SingleList: React.FC<Props> = ({
switch (selectedGroup) {
case "state":
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor);
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center");
icon = getPriorityIcon(groupTitle, "text-lg");
break;
case "labels":
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = (
<span
className="h-[18px] w-[18px] flex-shrink-0 rounded-full"
className="h-3 w-3 flex-shrink-0 rounded-full"
style={{ backgroundColor: labelColor }}
/>
);
@ -130,27 +130,23 @@ export const SingleList: React.FC<Props> = ({
};
return (
<Disclosure key={groupTitle} as="div" defaultOpen>
<Disclosure as="div" defaultOpen>
{({ open }) => (
<div className="bg-white">
<div
className={`flex items-center justify-between bg-gray-100 px-5 py-3 ${
open ? "" : "rounded-[10px]"
}`}
>
<div>
<div className="flex items-center justify-between px-4 py-2.5">
<Disclosure.Button>
<div className="flex items-center gap-x-3">
{selectedGroup !== null && (
<span className="flex items-center">{getGroupIcon()}</span>
<div className="flex items-center">{getGroupIcon()}</div>
)}
{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()}
</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}
</span>
</div>
@ -158,7 +154,7 @@ export const SingleList: React.FC<Props> = ({
{type === "issue" ? (
<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}
>
<PlusIcon className="h-4 w-4" />
@ -168,7 +164,7 @@ export const SingleList: React.FC<Props> = ({
) : (
<CustomMenu
customButton={
<div className="flex items-center cursor-pointer">
<div className="flex cursor-pointer items-center">
<PlusIcon className="h-4 w-4" />
</div>
}
@ -208,14 +204,16 @@ export const SingleList: React.FC<Props> = ({
makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue}
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}
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>

View File

@ -1,8 +1,7 @@
import Link from "next/link";
// icons
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
import { ExternalLinkIcon } from "components/icons";
import { ArrowTopRightOnSquareIcon, LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { timeAgo } from "helpers/date-time.helper";
// 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">
<Link href={link.url}>
<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"
>
<ExternalLinkIcon width="14" height="14" />
<ArrowTopRightOnSquareIcon className="h-4 w-4 text-brand-secondary" />
</a>
</Link>
<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)}
>
<TrashIcon className="h-4 w-4" />
@ -49,13 +48,13 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
</div>
)}
<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">
<LinkIcon className="h-3.5 w-3.5" />
</div>
<div>
<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)}
<br />
by{" "}

View File

@ -40,7 +40,7 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
const CustomTooltip = ({ active, payload }: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) {
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>
</div>
);
@ -68,20 +68,8 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
<stop offset="100%" stopColor="#3F76FF" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="currentDate"
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}
/>
<XAxis dataKey="currentDate" tickSize={10} minTickGap={10} />
<YAxis tickSize={10} minTickGap={10} allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"

View File

@ -78,7 +78,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
return 2;
default:
return 3;
return 0;
}
};
return (
@ -94,19 +94,19 @@ export const SidebarProgressStats: React.FC<Props> = ({
return setTab("States");
default:
return setTab("States");
return setTab("Assignees");
}
}}
>
<Tab.List
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"} `}
>
<Tab
className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
`w-full rounded px-3 py-1 text-brand-base ${
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
}`
}
>
@ -114,8 +114,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
</Tab>
<Tab
className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
`w-full rounded px-3 py-1 text-brand-base ${
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
}`
}
>
@ -123,8 +123,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
</Tab>
<Tab
className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
`w-full rounded px-3 py-1 text-brand-base ${
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
}`
}
>
@ -166,7 +166,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
<SingleProgressStats
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
src={User}
height="100%"

View File

@ -19,8 +19,8 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
}) => (
<div
className={`flex w-full items-center justify-between rounded p-2 text-xs ${
onClick ? "cursor-pointer hover:bg-gray-100" : ""
} ${selected ? "bg-gray-100" : ""}`}
onClick ? "cursor-pointer hover:bg-brand-surface-1" : ""
} ${selected ? "bg-brand-surface-1" : ""}`}
onClick={onClick}
>
<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.completed_cycles.length > 0 ? (
<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} />
<span>Completed cycles are not editable.</span>
</div>

View File

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

View File

@ -139,7 +139,7 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
leaveFrom="opacity-100"
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>
<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"
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]">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<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="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-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
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<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
</Dialog.Title>
<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-{" "}
<span className="font-bold">{data?.name}</span>? All of the data related
to the cycle will be permanently removed. This action cannot be undone.
<span className="break-all font-medium text-brand-base">
{data?.name}
</span>
? All of the data related to the cycle will be permanently removed. This
action cannot be undone.
</p>
</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>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}

View File

@ -37,30 +37,30 @@ export const EmptyCycle = () => {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-5 ">
<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">
<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">
<span className="h-2 w-20 rounded-full bg-gray-200" />
<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-brand-surface-2" />
</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} />
</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">
<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">
<span className="h-2 w-20 rounded-full bg-gray-200" />
<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-brand-surface-2" />
</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} />
</div>
</div>
@ -68,7 +68,7 @@ export const EmptyCycle = () => {
<div className="flex flex-col items-center justify-center gap-4 text-center ">
<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
time. Create new cycle now.
</p>

View File

@ -11,7 +11,11 @@ import useToast from "hooks/use-toast";
// ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import { getDateRangeStatus, isDateRangeValid } from "helpers/date-time.helper";
import {
getDateRangeStatus,
isDateGreaterThanToday,
isDateRangeValid,
} from "helpers/date-time.helper";
// 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) : "";
const dateChecker = async (payload: any) => {
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
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);
if (isDateGreaterThanToday(payload.end_date)) {
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
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);
});
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
});
}
};
const checkEmptyDate =
@ -94,13 +107,12 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
return (
<form onSubmit={handleSubmit(handleCreateUpdateCycle)}>
<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
</h3>
<div className="space-y-3">
<div>
<Input
mode="transparent"
autoComplete="off"
id="name"
name="name"
@ -124,7 +136,6 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
name="description"
placeholder="Description"
className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description}
register={register}
/>
@ -153,7 +164,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
setToastAlert({
type: "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({
type: "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 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>
<PrimaryButton
type="submit"
@ -207,7 +220,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
? "cursor-pointer"
: "cursor-not-allowed"
}
loading={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true}
disabled={checkEmptyDate ? false : isDateValid ? false : true}
loading={isSubmitting}
>
{status
? isSubmitting

View File

@ -151,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100"
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>
<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">
@ -164,7 +164,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg 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
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}

View File

@ -59,9 +59,9 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
{({ open }) => (
<>
<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">
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
</div>
@ -75,7 +75,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
leaveTo="opacity-0"
>
<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">
{options ? (
@ -93,7 +93,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
: ""
} ${
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}
>
@ -103,14 +103,14 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
</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
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}
>
<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
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
.then((res) => {
console.log(res);
mutate(CYCLE_DETAILS(cycleId as string));
})
.catch((e) => {
console.log(e);
});
.then(() => mutate(CYCLE_DETAILS(cycleId as string)))
.catch((e) => console.log(e));
};
const handleCopyText = () => {
@ -140,31 +135,31 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<div
className={`fixed top-0 ${
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 ? (
<>
<div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm">
<div className="flex items-center ">
<span
className={`flex items-center rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-center text-sm capitalize text-gray-800 `}
>
<div className="flex items-center">
<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">
{capitalizeFirstLetter(cycleStatus)}
</span>
</div>
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-gray-800">
<Popover className="flex h-full items-center justify-center rounded-lg">
<div className="relative flex h-full w-52 items-center gap-2">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
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 ${
open ? "bg-gray-100" : ""
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 ${
cycle.start_date ? "" : "text-brand-secondary"
}`}
>
<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>
<Transition
@ -214,20 +209,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
)}
</Popover>
<span>
<ArrowLongRightIcon className="h-3 w-3" />
<ArrowLongRightIcon className="h-3 w-3 text-brand-secondary" />
</span>
<Popover className="flex h-full items-center justify-center rounded-lg">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
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 ${
open ? "bg-gray-100" : ""
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 ${
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>
<Transition
@ -239,7 +234,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0"
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
selected={
watch("end_date") ? new Date(`${watch("end_date")}`) : new Date()
@ -280,10 +275,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
</div>
<div className="flex flex-col gap-6 px-6 py-6 w-full">
<div className="flex flex-col items-start justify-start gap-2 w-full">
<div className="flex items-start justify-between gap-2 w-full">
<h4 className="text-xl font-semibold text-gray-900">{cycle.name}</h4>
<div className="flex w-full flex-col gap-6 px-6 py-6">
<div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2">
<h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4>
<CustomMenu width="lg" ellipsis>
{!isCompleted && (
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
@ -302,15 +297,15 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</CustomMenu>
</div>
<span className="whitespace-normal text-sm leading-5 text-black">
<span className="whitespace-normal text-sm leading-5 text-brand-secondary">
{cycle.description}
</span>
</div>
<div className="flex flex-col gap-4 text-sm">
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2">
<UserCircleIcon className="h-5 w-5 text-gray-400" />
<div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
<UserCircleIcon className="h-5 w-5" />
<span>Lead</span>
</div>
@ -328,17 +323,17 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
{cycle.owned_by.first_name.charAt(0)}
</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 className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2">
<ChartPieIcon className="h-5 w-5 text-gray-400" />
<div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
<ChartPieIcon className="h-5 w-5" />
<span>Progress</span>
</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">
<ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} />
</span>
@ -349,7 +344,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</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>
{({ open }) => (
<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 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 ? (
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
{progressPercentage ? `${progressPercentage}%` : ""}
@ -376,7 +371,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
) : (
<div className="flex items-center gap-1">
<ExclamationIcon height={14} width={14} />
<span className="text-xs italic text-gray-500">
<span className="text-xs italic text-brand-secondary">
{cycleStatus === "upcoming"
? "Cycle is yet to start."
: "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-center gap-1">
<span>
<DocumentIcon className="h-3 w-3 text-gray-500" />
<DocumentIcon className="h-3 w-3 text-brand-secondary" />
</span>
<span>
Pending Issues -{" "}
@ -400,7 +395,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</span>
</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">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
@ -429,7 +424,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</Disclosure>
</div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-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>
{({ open }) => (
<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 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>
{cycle.total_issues > 0 ? (
@ -450,7 +445,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
) : (
<div className="flex items-center gap-1">
<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.
</span>
</div>

View File

@ -238,7 +238,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
return (
<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}`}>
<a className="w-full">
<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 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 ">
<CalendarDaysIcon className="h-4 w-4 text-gray-900" />
<span className="text-gray-400">Start :</span>
<CalendarDaysIcon className="h-4 w-4" />
<span>Start :</span>
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<div className="flex items-start gap-1 ">
<TargetIcon className="h-4 w-4 text-gray-900" />
<span className="text-gray-400">End :</span>
<TargetIcon className="h-4 w-4" />
<span>End :</span>
<span>{renderShortDateWithYearFormat(endDate)}</span>
</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">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
@ -293,11 +293,11 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
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)}
</span>
)}
<span className="text-gray-900">{cycle.owned_by.first_name}</span>
<span>{cycle.owned_by.first_name}</span>
</div>
<div className="flex items-center">
{!isCompleted && (
@ -306,7 +306,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
e.preventDefault();
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>
<PencilIcon className="h-4 w-4" />
@ -350,7 +350,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
<Disclosure>
{({ open }) => (
<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"
}`}
>
@ -368,7 +368,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
</div>
<Transition show={open}>
<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="space-y-3 text-xs">
{stateGroups.map((group) => (
@ -388,7 +388,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
<div>
<span>
{cycle[group.key as keyof ICycle] as number}{" "}
<span className="text-gray-500">
<span className="text-brand-secondary">
-{" "}
{cycle.total_issues > 0
? `${Math.round(

View File

@ -89,7 +89,7 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
leaveFrom="opacity-100"
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>
<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"
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 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" />
<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>
<button onClick={handleClose}>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
<div className="flex items-center gap-2 pb-3 mt-2 px-5 border-b border-gray-200">
<MagnifyingGlassIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 border-b border-brand-base px-5 pb-3">
<MagnifyingGlassIcon className="h-4 w-4 text-brand-secondary" />
<input
className="outline-none"
className="bg-brand-surface-1 outline-none"
placeholder="Search for a cycle..."
onChange={(e) => setQuery(e.target.value)}
value={query}
/>
</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.length > 0 ? (
filteredOptions.map((option: ICycle) => (
<button
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={() => {
transferIssue({
new_cycle_id: option?.id,
@ -138,25 +138,25 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
}}
>
<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 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)}
</span>
</div>
</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} />
<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
issues.
</span>
</div>
)
) : (
<p className="text-center text-gray-500">Loading...</p>
<p className="text-center text-brand-secondary">Loading...</p>
)}
</div>
</div>

View File

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

View File

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

View File

@ -6,19 +6,20 @@ import { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import estimatesService from "services/estimates.service";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import { checkDuplicates } from "helpers/array.helper";
// types
import { IEstimate } from "types";
import { IEstimate, IEstimateFormData } from "types";
// fetch-keys
import { ESTIMATES_LIST } from "constants/fetch-keys";
import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
@ -26,66 +27,90 @@ type Props = {
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: "",
description: "",
value1: "",
value2: "",
value3: "",
value4: "",
value5: "",
value6: "",
};
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
const {
register,
formState: { errors, isSubmitting },
formState: { isSubmitting },
handleSubmit,
reset,
} = useForm<IEstimate>({
} = useForm<FormValues>({
defaultValues,
});
const onClose = () => {
handleClose();
reset();
};
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const createEstimate = async (formData: IEstimate) => {
const createEstimate = async (payload: IEstimateFormData) => {
if (!workspaceSlug || !projectId) return;
const payload = {
name: formData.name,
description: formData.description,
};
await estimatesService
.createEstimate(workspaceSlug as string, projectId as string, payload)
.then((res) => {
mutate<IEstimate[]>(
ESTIMATES_LIST(projectId as string),
(prevData) => [res, ...(prevData ?? [])],
false
);
handleClose();
.then(() => {
mutate(ESTIMATES_LIST(projectId as string));
onClose();
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Error: Estimate could not be created",
});
.catch((err) => {
if (err.status === 400)
setToastAlert({
type: "error",
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;
const payload = {
name: formData.name,
description: formData.description,
};
mutate<IEstimate[]>(
ESTIMATES_LIST(projectId as string),
ESTIMATES_LIST(projectId.toString()),
(prevData) =>
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;
}),
@ -95,23 +120,104 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
await estimatesService
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload)
.then(() => {
mutate(ESTIMATES_LIST(projectId.toString()));
mutate(ESTIMATE_DETAILS(data.id));
handleClose();
})
.catch(() => {
setToastAlert({
type: "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(() => {
reset({
...defaultValues,
...data,
});
if (data)
reset({
...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]);
return (
@ -127,7 +233,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
leaveFrom="opacity-100"
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>
<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"
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={data ? handleSubmit(updateEstimate) : handleSubmit(createEstimate)}
>
<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 onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-3">
<div className="text-lg font-medium leading-6">
{data ? "Update" : "Create"} Estimate
@ -156,17 +260,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
type="name"
placeholder="Title"
autoComplete="off"
mode="transparent"
className="resize-none text-xl"
error={errors.name}
register={register}
validations={{
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
},
}}
/>
</div>
<div>
@ -175,11 +270,107 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
name="description"
placeholder="Description"
className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description}
register={register}
/>
</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 className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>

View File

@ -46,7 +46,7 @@ export const DeleteEstimateModal: React.FC<Props> = ({
leaveFrom="opacity-100"
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>
<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"
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 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
className="h-6 w-6 text-red-600"
aria-hidden="true"
@ -74,9 +74,9 @@ export const DeleteEstimateModal: React.FC<Props> = ({
</span>
</div>
<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-{" "}
<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.
This action cannot be undone.
</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