chore: pulled the latest changes

This commit is contained in:
kunal_17 2023-05-02 16:38:11 +05:30
commit f93ad6c886
206 changed files with 3581 additions and 2515 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -79,11 +79,8 @@ from plane.api.views import (
## End Issues ## End Issues
# States # States
StateViewSet, StateViewSet,
StateDeleteIssueCheckEndpoint,
## End States ## End States
# Estimates # Estimates
EstimateViewSet,
EstimatePointViewSet,
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint, BulkEstimatePointEndpoint,
## End Estimates ## End Estimates
@ -134,6 +131,7 @@ from plane.api.views import (
GithubIssueSyncViewSet, GithubIssueSyncViewSet,
GithubCommentSyncViewSet, GithubCommentSyncViewSet,
BulkCreateGithubIssueSyncEndpoint, BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
## End Integrations ## End Integrations
# Importer # Importer
ServiceIssueImportSummaryEndpoint, ServiceIssueImportSummaryEndpoint,
@ -510,73 +508,35 @@ urlpatterns = [
), ),
name="project-state", name="project-state",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateDeleteIssueCheckEndpoint.as_view(),
name="state-delete-check",
),
# End States ## # End States ##
# States # Estimates
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
EstimateViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-estimates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:pk>/",
EstimateViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-estimates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<uuid:pk>/",
EstimatePointViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-estimates",
),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(), ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points", name="project-estimate-points",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/bulk-estimate-points/", "workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(), BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="bulk-create-estimate-points", name="bulk-create-estimate-points",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/bulk-estimate-points/", "workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(), BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="bulk-create-estimate-points", name="bulk-create-estimate-points",
), ),
# End States ## # End Estimates ##
# Shortcuts # Shortcuts
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/", "workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
@ -1251,6 +1211,26 @@ urlpatterns = [
), ),
), ),
## End Github Integrations ## End Github Integrations
# Slack Integration
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
SlackProjectSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
SlackProjectSyncViewSet.as_view(
{
"delete": "destroy",
"get": "retrieve",
}
),
),
## End Slack Integration
## End Integrations ## End Integrations
# Importer # Importer
path( path(

View File

@ -42,7 +42,7 @@ from .workspace import (
UserWorkspaceDashboardEndpoint, UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet, WorkspaceThemeViewSet,
) )
from .state import StateViewSet, StateDeleteIssueCheckEndpoint from .state import StateViewSet
from .shortcut import ShortCutViewSet from .shortcut import ShortCutViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import ( from .cycle import (
@ -106,6 +106,7 @@ from .integration import (
GithubCommentSyncViewSet, GithubCommentSyncViewSet,
GithubRepositoriesEndpoint, GithubRepositoriesEndpoint,
BulkCreateGithubIssueSyncEndpoint, BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
) )
from .importer import ( from .importer import (
@ -133,8 +134,6 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint from .gpt import GPTIntegrationEndpoint
from .estimate import ( from .estimate import (
EstimateViewSet,
EstimatePointViewSet,
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint, BulkEstimatePointEndpoint,
) )

View File

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

View File

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

View File

@ -10,110 +10,11 @@ from sentry_sdk import capture_exception
from .base import BaseViewSet, BaseAPIView from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Project, Estimate, EstimatePoint from plane.db.models import Project, Estimate, EstimatePoint
from plane.api.serializers import EstimateSerializer, EstimatePointSerializer from plane.api.serializers import (
EstimateSerializer,
EstimatePointSerializer,
class EstimateViewSet(BaseViewSet): EstimateReadSerializer,
permission_classes = [ )
ProjectEntityPermission,
]
model = Estimate
serializer_class = EstimateSerializer
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.distinct()
)
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
class EstimatePointViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
model = EstimatePoint
serializer_class = EstimatePointSerializer
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.filter(estimate_id=self.kwargs.get("estimate_id"))
.select_related("project")
.select_related("workspace")
.distinct()
)
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
estimate_id=self.kwargs.get("estimate_id"),
)
def create(self, request, slug, project_id, estimate_id):
try:
serializer = EstimatePointSerializer(data=request.data)
if serializer.is_valid():
serializer.save(estimate_id=estimate_id, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The estimate point is already taken"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, estimate_id, pk):
try:
estimate_point = EstimatePoint.objects.get(
pk=pk,
estimate_id=estimate_id,
project_id=project_id,
workspace__slug=slug,
)
serializer = EstimatePointSerializer(
estimate_point, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save(estimate_id=estimate_id, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except EstimatePoint.DoesNotExist:
return Response(
{"error": "Estimate Point does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The estimate point value is already taken"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectEstimatePointEndpoint(BaseAPIView): class ProjectEstimatePointEndpoint(BaseAPIView):
@ -141,12 +42,28 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
) )
class BulkEstimatePointEndpoint(BaseAPIView): class BulkEstimatePointEndpoint(BaseViewSet):
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]
model = Estimate
serializer_class = EstimateSerializer
def post(self, request, slug, project_id): def list(self, request, slug, project_id):
try:
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: try:
if not request.data.get("estimate", False): if not request.data.get("estimate", False):
return Response( return Response(
@ -215,14 +132,58 @@ class BulkEstimatePointEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
def patch(self, request, slug, project_id, estimate_id): def retrieve(self, request, slug, project_id, estimate_id):
try: try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateReadSerializer(estimate)
return Response(
serializer.data,
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, estimate_id):
try:
if not request.data.get("estimate", False):
return Response(
{"error": "Estimate is required"},
status=status.HTTP_400_BAD_REQUEST,
)
if not len(request.data.get("estimate_points", [])): if not len(request.data.get("estimate_points", [])):
return Response( return Response(
{"error": "Estimate points are required"}, {"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
estimate = Estimate.objects.get(pk=estimate_id)
estimate_serializer = EstimateSerializer(
estimate, data=request.data.get("estimate"), partial=True
)
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
try:
estimate = estimate_serializer.save()
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points_data = request.data.get("estimate_points", []) estimate_points_data = request.data.get("estimate_points", [])
estimate_points = EstimatePoint.objects.filter( estimate_points = EstimatePoint.objects.filter(
@ -242,18 +203,30 @@ class BulkEstimatePointEndpoint(BaseAPIView):
for point in estimate_points_data for point in estimate_points_data
if point.get("id") == str(estimate_point.id) if point.get("id") == str(estimate_point.id)
] ]
print(estimate_point_data)
if len(estimate_point_data): if len(estimate_point_data):
estimate_point.value = estimate_point_data[0].get( estimate_point.value = estimate_point_data[0].get(
"value", estimate_point.value "value", estimate_point.value
) )
updated_estimate_points.append(estimate_point) updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update( try:
updated_estimate_points, ["value"], batch_size=10 EstimatePoint.objects.bulk_update(
updated_estimate_points, ["value"], batch_size=10
)
except IntegrityError as e:
return Response(
{"error": "Values need to be unique for each key"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(
{
"estimate": estimate_serializer.data,
"estimate_points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK,
) )
serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Estimate.DoesNotExist: except Estimate.DoesNotExist:
return Response( return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST {"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
@ -264,3 +237,17 @@ class BulkEstimatePointEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
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, Module,
ModuleLink, ModuleLink,
ModuleIssue, ModuleIssue,
Label,
) )
from plane.api.serializers import ( from plane.api.serializers import (
ImporterSerializer, ImporterSerializer,
@ -104,7 +105,7 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -235,9 +236,20 @@ class ImportServiceEndpoint(BaseAPIView):
def delete(self, request, slug, service, pk): def delete(self, request, slug, service, pk):
try: try:
importer = Importer.objects.filter( importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug pk=pk, service=service, workspace__slug=slug
) )
# Delete all imported Issues
imported_issues = importer.imported_data.get("issues", [])
Issue.objects.filter(id__in=imported_issues).delete()
# Delete all imported Labels
imported_labels = importer.imported_data.get("labels", [])
Label.objects.filter(id__in=imported_labels).delete()
if importer.service == "jira":
imported_modules = importer.imported_data.get("modules", [])
Module.objects.filter(id__in=imported_modules).delete()
importer.delete() importer.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e: except Exception as e:
@ -247,6 +259,27 @@ class ImportServiceEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
def patch(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
serializer = ImporterSerializer(importer, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Importer.DoesNotExist:
return Response(
{"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UpdateServiceImportStatusEndpoint(BaseAPIView): class UpdateServiceImportStatusEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service, importer_id): def post(self, request, slug, project_id, service, importer_id):
@ -487,48 +520,59 @@ class BulkImportModulesEndpoint(BaseAPIView):
ignore_conflicts=True, ignore_conflicts=True,
) )
_ = ModuleLink.objects.bulk_create( modules = Module.objects.filter(id__in=[module.id for module in modules])
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get("url", "https://plane.so"),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
bulk_module_issues = [] if len(modules) == len(modules_data):
for module, module_data in zip(modules, modules_data): _ = ModuleLink.objects.bulk_create(
module_issues_list = module_data.get("module_issues_list", []) [
bulk_module_issues = bulk_module_issues + [ ModuleLink(
ModuleIssue( module=module,
issue_id=issue, url=module_data.get("link", {}).get(
module=module, "url", "https://plane.so"
project_id=project_id, ),
workspace_id=project.workspace_id, title=module_data.get("link", {}).get(
created_by=request.user, "title", "Original Issue"
updated_by=request.user, ),
) project_id=project_id,
for issue in module_issues_list workspace_id=project.workspace_id,
] created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
_ = ModuleIssue.objects.bulk_create( bulk_module_issues = []
bulk_module_issues, batch_size=100, ignore_conflicts=True for module, module_data in zip(modules, modules_data):
) module_issues_list = module_data.get("module_issues_list", [])
bulk_module_issues = bulk_module_issues + [
ModuleIssue(
issue_id=issue,
module=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for issue in module_issues_list
]
serializer = ModuleSerializer(modules, many=True) _ = ModuleIssue.objects.bulk_create(
return Response( bulk_module_issues, batch_size=100, ignore_conflicts=True
{"modules": serializer.data}, status=status.HTTP_201_CREATED )
)
serializer = ModuleSerializer(modules, many=True)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
else:
return Response(
{"message": "Modules created but issues could not be imported"},
status=status.HTTP_200_OK,
)
except Project.DoesNotExist: except Project.DoesNotExist:
return Response( return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -103,22 +103,3 @@ class StateViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except State.DoesNotExist: except State.DoesNotExist:
return Response({"error": "State does not exists"}, status=status.HTTP_404) return Response({"error": "State does not exists"}, status=status.HTTP_404)
class StateDeleteIssueCheckEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, pk):
try:
issue_count = Issue.objects.filter(
state=pk, workspace__slug=slug, project_id=project_id
).count()
return Response({"issue_count": issue_count}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

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

View File

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

View File

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

View File

@ -30,6 +30,5 @@ def magic_link(email, key, token, current_site):
msg.send() msg.send()
return return
except Exception as e: except Exception as e:
print(e)
capture_exception(e) capture_exception(e)
return return

View File

@ -50,6 +50,5 @@ def project_invitation(email, project_id, token, current_site):
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e: except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
return return
except Exception as e: except Exception as e:
print(e)
capture_exception(e) capture_exception(e)
return return

View File

@ -0,0 +1,50 @@
# 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
@shared_task
def send_welcome_email(instance, created, message):
try:
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

@ -0,0 +1,58 @@
# Generated by Django 3.2.18 on 2023-05-01 19:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0028_auto_20230414_1703'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='importer',
name='imported_data',
field=models.JSONField(null=True),
),
migrations.AddField(
model_name='module',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.CreateModel(
name='SlackProjectSync',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('access_token', models.CharField(max_length=300)),
('scopes', models.TextField()),
('bot_user_id', models.CharField(max_length=50)),
('webhook_url', models.URLField(max_length=1000)),
('data', models.JSONField(default=dict)),
('team_id', models.CharField(max_length=30)),
('team_name', models.CharField(max_length=300)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_slackprojectsync', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_slackprojectsync', to='db.workspace')),
('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slack_syncs', to='db.workspaceintegration')),
],
options={
'verbose_name': 'Slack Project Sync',
'verbose_name_plural': 'Slack Project Syncs',
'db_table': 'slack_project_syncs',
'ordering': ('-created_at',),
'unique_together': {('team_id', 'project')},
},
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,7 +80,7 @@ AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
AWS_S3_ADDRESSING_STYLE = "auto" AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL. # The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = "" AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator. # A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = "" AWS_S3_KEY_PREFIX = ""

View File

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

View File

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

View File

@ -94,7 +94,9 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<div className="mt-2 flex items-center justify-between"> <div className="mt-2 flex items-center justify-between">
<div className="ml-auto text-sm"> <div className="ml-auto text-sm">
<Link href={"/forgot-password"}> <Link href={"/forgot-password"}>
<a className="font-medium text-brand-accent hover:text-indigo-500">Forgot your password?</a> <a className="font-medium text-brand-accent hover:text-brand-accent">
Forgot your password?
</a>
</Link> </Link>
</div> </div>
</div> </div>
@ -102,7 +104,8 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<SecondaryButton <SecondaryButton
type="submit" type="submit"
className="w-full text-center" className="w-full text-center"
loading={isSubmitting || (!isValid && isDirty)} disabled={!isValid && isDirty}
loading={isSubmitting}
> >
{isSubmitting ? "Signing in..." : "Sign In"} {isSubmitting ? "Signing in..." : "Sign In"}
</SecondaryButton> </SecondaryButton>

View File

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

View File

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

View File

@ -45,9 +45,9 @@ export const JoinProject: React.FC = () => {
<div className="h-44 w-72"> <div className="h-44 w-72">
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" /> <Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
</div> </div>
<h1 className="text-xl font-medium text-gray-900">You are not a member of this project</h1> <h1 className="text-xl font-medium">You are not a member of this project</h1>
<div className="w-full max-w-md text-base text-gray-500 "> <div className="w-full max-w-md text-base text-brand-secondary">
<p className="mx-auto w-full text-sm md:w-3/4"> <p className="mx-auto w-full text-sm md:w-3/4">
You are not a member of this project, but you can join this project by clicking the button You are not a member of this project, but you can join this project by clicking the button
below. below.

View File

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

View File

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

View File

@ -393,7 +393,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl bg-brand-surface-2 border-brand-base border shadow-2xl transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-surface-2 shadow-2xl transition-all">
<Command <Command
filter={(value, search) => { filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1; if (value.toLowerCase().includes(search.toLowerCase())) return 1;
@ -675,7 +675,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="Page"> <Command.Group heading="Page">
<Command.Item onSelect={createNewPage} className="focus:outline-none"> <Command.Item onSelect={createNewPage} className="focus:outline-none">
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-brand-secondary">
<DocumentTextIcon className="h-4 w-4" color="#6b7280" /> <DocumentTextIcon className="h-4 w-4" color="#6b7280" />
Create new page Create new page
</div> </div>

View File

@ -17,7 +17,7 @@ type Props = {
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };

View File

@ -29,7 +29,7 @@ type Props = {
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -130,7 +130,8 @@ export const SingleBoard: React.FC<Props> = ({
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox} handleTrashBox={handleTrashBox}
removeIssue={() => { removeIssue={() => {
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id); if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
}} }}
isCompleted={isCompleted} isCompleted={isCompleted}
userAuth={userAuth} userAuth={userAuth}

View File

@ -349,7 +349,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
/> />
)} )}
{properties.sub_issue_count && ( {properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm"> <div className="flex flex-shrink-0 items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div> </div>
)} )}

View File

@ -26,7 +26,7 @@ import {
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CustomMenu, Spinner } from "components/ui"; import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
// icon // icon
import { import {
CheckIcon, CheckIcon,
@ -51,6 +51,7 @@ import { IIssue } from "types";
// constant // constant
import { monthOptions, yearOptions } from "constants/calendar"; import { monthOptions, yearOptions } from "constants/calendar";
import modulesService from "services/modules.service"; import modulesService from "services/modules.service";
import { getStateGroupIcon } from "components/icons";
type Props = { type Props = {
addIssueToDate: (date: string) => void; addIssueToDate: (date: string) => void;
@ -62,9 +63,10 @@ interface ICalendarRange {
} }
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => { export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false); const [showWeekEnds, setShowWeekEnds] = useState(false);
const [currentDate, setCurrentDate] = useState<Date>(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true); const [isMonthlyView, setIsMonthlyView] = useState(true);
const [showAllIssues, setShowAllIssues] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -151,15 +153,15 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays; const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
const calendarIssues = cycleId const calendarIssues = cycleId
? cycleCalendarIssues ? (cycleCalendarIssues as IIssue[])
: moduleId : moduleId
? moduleCalendarIssues ? (moduleCalendarIssues as IIssue[])
: projectCalendarIssues; : (projectCalendarIssues as IIssue[]);
const currentViewDaysData = currentViewDays.map((date: Date) => { const currentViewDaysData = currentViewDays.map((date: Date) => {
const filterIssue = const filterIssue =
calendarIssues && calendarIssues.length > 0 calendarIssues && calendarIssues.length > 0
? (calendarIssues as IIssue[]).filter( ? calendarIssues.filter(
(issue) => (issue) =>
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date) issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
) )
@ -324,7 +326,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
<div className="flex w-full items-center justify-end gap-2"> <div className="flex w-full items-center justify-end gap-2">
<button <button
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-4 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none" className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
onClick={() => { onClick={() => {
if (isMonthlyView) { if (isMonthlyView) {
updateDate(new Date()); updateDate(new Date());
@ -337,14 +339,12 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
} }
}} }}
> >
Today{" "} Today
</button> </button>
<CustomMenu <CustomMenu
customButton={ customButton={
<div <div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
className={`group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none `}
>
{isMonthlyView ? "Monthly" : "Weekly"} {isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" /> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div> </div>
@ -390,23 +390,10 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary"> <div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
<h4>Show weekends</h4> <h4>Show weekends</h4>
<button <ToggleSwitch
type="button" value={showWeekEnds}
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${ onChange={() => setShowWeekEnds(!showWeekEnds)}
showWeekEnds ? "bg-green-500" : "bg-brand-surface-2" />
}`}
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-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
showWeekEnds ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
</div> </div>
</CustomMenu> </CustomMenu>
</div> </div>
@ -445,61 +432,87 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
showWeekEnds ? "grid-cols-7" : "grid-cols-5" showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `} } `}
> >
{currentViewDaysData.map((date, index) => ( {currentViewDaysData.map((date, index) => {
<StrictModeDroppable droppableId={date.date}> const totalIssues = date.issues.length;
{(provided, snapshot) => (
<div return (
key={index} <StrictModeDroppable droppableId={date.date}>
ref={provided.innerRef} {(provided) => (
{...provided.droppableProps} <div
className={`group 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 ${ key={index}
showWeekEnds ref={provided.innerRef}
? (index + 1) % 7 === 0 {...provided.droppableProps}
className={`group relative flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? "" ? ""
: "border-r" : "border-r"
: (index + 1) % 5 === 0 }`}
? "" >
: "border-r" {isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
}`} {totalIssues > 0 &&
> date.issues
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>} .slice(0, showAllIssues ? totalIssues : 4)
{date.issues.length > 0 && .map((issue: IIssue, index) => (
date.issues.map((issue: IIssue, index) => ( <Draggable draggableId={issue.id} index={index}>
<Draggable draggableId={issue.id} index={index}> {(provided, snapshot) => (
{(provided, snapshot) => ( <div
<div key={index}
key={index} ref={provided.innerRef}
ref={provided.innerRef} {...provided.draggableProps}
{...provided.draggableProps} {...provided.dragHandleProps}
{...provided.dragHandleProps} className={`w-full cursor-pointer truncate rounded border border-brand-base px-1.5 py-1 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
className={`w-full cursor-pointer truncate rounded bg-brand-surface-2 p-1.5 hover:scale-105 ${ snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
snapshot.isDragging ? "shadow-lg" : "" }`}
}`} >
> <Link
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`} >
className="w-full" <a className="flex w-full items-center gap-2">
> {getStateGroupIcon(
{issue.name} issue.state_detail.group,
</Link> "12",
</div> "12",
)} issue.state_detail.color
</Draggable> )}
))} {issue.name}
<div className="flex items-center justify-center p-1.5 text-sm text-brand-secondary opacity-0 group-hover:opacity-100"> </a>
<button </Link>
className="flex items-center justify-center gap-2 text-center" </div>
onClick={() => addIssueToDate(date.date)} )}
</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`}
> >
<PlusIcon className="h-4 w-4 text-brand-secondary" /> <button
Add new issue className="flex items-center justify-center gap-1 text-center"
</button> onClick={() => addIssueToDate(date.date)}
>
<PlusIcon className="h-3 w-3 text-brand-secondary" />
Add issue
</button>
</div>
{provided.placeholder}
</div> </div>
{provided.placeholder} )}
</div> </StrictModeDroppable>
)} );
</StrictModeDroppable> })}
))}
</div> </div>
</div> </div>
</DragDropContext> </DragDropContext>

View File

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

View File

@ -67,15 +67,19 @@ const activityDetails: {
}, },
name: { name: {
message: "set the name to", message: "set the name to",
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: (
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
),
}, },
description: { description: {
message: "updated the description.", message: "updated the description.",
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />, icon: (
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
),
}, },
estimate_point: { estimate_point: {
message: "set the estimate point to", message: "set the estimate point to",
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />, icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
}, },
target_date: { target_date: {
message: "set the due date to", message: "set the due date to",
@ -91,7 +95,7 @@ const activityDetails: {
}, },
estimate: { estimate: {
message: "updated the estimate", message: "updated the estimate",
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />, icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
}, },
link: { link: {
message: "updated the link", message: "updated the link",
@ -153,11 +157,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
) { ) {
const { workspace_detail, project, issue } = activity; const { workspace_detail, project, issue } = activity;
value = ( value = (
<span className="text-gray-600"> <span className="text-brand-secondary">
created{" "} created{" "}
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}> <Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
<a className="inline-flex items-center hover:underline"> <a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="h-3.5 w-3.5 ml-1" /> this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a> </a>
</Link> </Link>
</span> </span>
@ -198,7 +202,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
if (activity.field === "comment") { if (activity.field === "comment") {
return ( return (
<div key={activity.id}> <div key={activity.id} className="mt-2">
<div className="relative flex items-start space-x-3"> <div className="relative flex items-start space-x-3">
<div className="relative px-1"> <div className="relative px-1">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( {activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
@ -219,7 +223,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px"> <span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
<ChatBubbleLeftEllipsisIcon <ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-gray-400" className="h-3.5 w-3.5 text-brand-secondary"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@ -242,9 +246,8 @@ export const Feeds: React.FC<any> = ({ activities }) => (
: activity.old_value : activity.old_value
} }
editable={false} editable={false}
onBlur={() => ({})}
noBorder noBorder
customClassName="text-xs bg-brand-surface-1" customClassName="text-xs border border-brand-base bg-brand-base"
/> />
</div> </div>
</div> </div>
@ -268,7 +271,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div> <div>
<div className="relative px-1.5"> <div className="relative px-1.5">
<div className="mt-1.5"> <div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-1 ring-white"> <div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-2 ring-white">
{activity.field ? ( {activity.field ? (
activityDetails[activity.field as keyof typeof activityDetails]?.icon activityDetails[activity.field as keyof typeof activityDetails]?.icon
) : activity.actor_detail.avatar && ) : activity.actor_detail.avatar &&

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import { Popover, Transition } from "@headlessui/react";
// components // components
import { SelectFilters } from "components/views"; import { SelectFilters } from "components/views";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu, ToggleSwitch } from "components/ui";
// icons // icons
import { import {
ChevronDownIcon, ChevronDownIcon,
@ -213,23 +213,10 @@ export const IssuesFilterView: React.FC = () => {
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Show empty states</h4> <h4 className="text-brand-secondary">Show empty states</h4>
<button <ToggleSwitch
type="button" value={showEmptyGroups}
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${ onChange={() => setShowEmptyGroups(!showEmptyGroups)}
showEmptyGroups ? "bg-green-500" : "bg-brand-surface-2" />
}`}
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-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
</div> </div>
<div className="relative flex justify-end gap-x-3"> <div className="relative flex justify-end gap-x-3">
<button type="button" onClick={() => resetFilterToDefault()}> <button type="button" onClick={() => resetFilterToDefault()}>

View File

@ -46,7 +46,7 @@ import {
MODULE_DETAILS, MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
STATE_LIST, STATES_LIST,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// image // image
@ -103,7 +103,7 @@ export const IssuesView: React.FC<Props> = ({
} = useIssuesView(); } = useIssuesView();
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string) ? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null : null
@ -314,10 +314,26 @@ export const IssuesView: React.FC<Props> = ({
); );
const removeIssueFromCycle = useCallback( const removeIssueFromCycle = useCallback(
(bridgeId: string) => { (bridgeId: string, issueId: string) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); mutate(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
}
return filteredData;
} else {
const filteredData = prevData.filter((i: any) => i.id !== issueId);
return filteredData;
}
},
false
);
issuesService issuesService
.removeIssueFromCycle( .removeIssueFromCycle(
@ -326,6 +342,13 @@ export const IssuesView: React.FC<Props> = ({
cycleId as string, cycleId as string,
bridgeId bridgeId
) )
.then(() => {
setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
})
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}); });
@ -334,10 +357,26 @@ export const IssuesView: React.FC<Props> = ({
); );
const removeIssueFromModule = useCallback( const removeIssueFromModule = useCallback(
(bridgeId: string) => { (bridgeId: string, issueId: string) => {
if (!workspaceSlug || !projectId || !moduleId) return; if (!workspaceSlug || !projectId || !moduleId) return;
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); mutate(
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
}
return filteredData;
} else {
const filteredData = prevData.filter((item: any) => item.id !== issueId);
return filteredData;
}
},
false
);
modulesService modulesService
.removeIssueFromModule( .removeIssueFromModule(
@ -346,6 +385,13 @@ export const IssuesView: React.FC<Props> = ({
moduleId as string, moduleId as string,
bridgeId bridgeId
) )
.then(() => {
setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
})
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}); });
@ -426,7 +472,7 @@ export const IssuesView: React.FC<Props> = ({
)} )}
</div> </div>
{areFiltersApplied && ( {areFiltersApplied && (
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} /> <div className={`${issueView === "list" ? "mt-4" : "my-4"} border-t border-brand-base`} />
)} )}
</> </>

View File

@ -14,7 +14,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };

View File

@ -216,7 +216,7 @@ export const SingleListIssue: React.FC<Props> = ({
</a> </a>
</ContextMenu> </ContextMenu>
<div <div
className="flex items-center justify-between gap-2 border-b border-brand-base bg-brand-base px-4 py-2.5 last:border-b-0" className="flex flex-wrap items-center justify-between gap-2 border-b border-brand-base bg-brand-base last:border-b-0"
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu(true); setContextMenu(true);
@ -224,26 +224,28 @@ export const SingleListIssue: React.FC<Props> = ({
}} }}
> >
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2"> <div className="flex-grow cursor-pointer px-4 pt-2.5 md:py-2.5">
{properties.key && ( <a className="group relative flex items-center gap-2">
<Tooltip {properties.key && (
tooltipHeading="Issue ID" <Tooltip
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} tooltipHeading="Issue ID"
> tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
<span className="flex-shrink-0 text-xs text-brand-secondary"> >
{issue.project_detail?.identifier}-{issue.sequence_id} <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-[0.825rem] text-brand-base">
{truncateText(issue.name, 50)}
</span> </span>
</Tooltip> </Tooltip>
)} </a>
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> </div>
<span className="text-[0.825rem] text-brand-base">
{truncateText(issue.name, 50)}
</span>
</Tooltip>
</a>
</Link> </Link>
<div className="flex flex-wrap items-center gap-2 text-xs"> <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 && ( {properties.priority && (
<ViewPrioritySelect <ViewPrioritySelect
issue={issue} issue={issue}
@ -268,7 +270,7 @@ export const SingleListIssue: React.FC<Props> = ({
/> />
)} )}
{properties.sub_issue_count && ( {properties.sub_issue_count && (
<div className="flex items-center gap-1 rounded-md border border-brand-base px-3 py-1 text-xs text-brand-secondary shadow-sm"> <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"} {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div> </div>
)} )}

View File

@ -37,7 +37,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; isCompleted?: boolean;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -204,7 +204,7 @@ export const SingleList: React.FC<Props> = ({
makeIssueCopy={() => makeIssueCopy(issue)} makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
removeIssue={() => { removeIssue={() => {
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id); if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
}} }}
isCompleted={isCompleted} isCompleted={isCompleted}
userAuth={userAuth} userAuth={userAuth}

View File

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

View File

@ -78,7 +78,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
return 2; return 2;
default: default:
return 3; return 0;
} }
}; };
return ( return (
@ -94,7 +94,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
return setTab("States"); return setTab("States");
default: default:
return setTab("States"); return setTab("Assignees");
} }
}} }}
> >

View File

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

View File

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

View File

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

View File

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

View File

@ -135,31 +135,31 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<div <div
className={`fixed top-0 ${ className={`fixed top-0 ${
isOpen ? "right-0" : "-right-[24rem]" isOpen ? "right-0" : "-right-[24rem]"
} z-20 h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-surface-2 py-5 duration-300`} } z-20 h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-sidebar py-5 duration-300`}
> >
{cycle ? ( {cycle ? (
<> <>
<div className="flex flex-col items-start justify-center"> <div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm"> <div className="flex gap-2.5 px-5 text-sm">
<div className="flex items-center "> <div className="flex items-center">
<span <span className="flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-center text-xs capitalize">
className={`flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-center text-sm capitalize text-brand-muted-1 `}
>
{capitalizeFirstLetter(cycleStatus)} {capitalizeFirstLetter(cycleStatus)}
</span> </span>
</div> </div>
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-brand-muted-1"> <div className="relative flex h-full w-52 items-center gap-2">
<Popover className="flex h-full items-center justify-center rounded-lg"> <Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} disabled={isCompleted ?? false}
className={`group flex h-full items-center gap-1 rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-brand-muted-1 ${ className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
open ? "bg-brand-surface-1" : "" cycle.start_date ? "" : "text-brand-secondary"
}`} }`}
> >
<CalendarDaysIcon className="h-3 w-3" /> <CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${cycle?.start_date}`))}</span> <span>
{renderShortDate(new Date(`${cycle?.start_date}`), "Start date")}
</span>
</Popover.Button> </Popover.Button>
<Transition <Transition
@ -209,20 +209,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
)} )}
</Popover> </Popover>
<span> <span>
<ArrowLongRightIcon className="h-3 w-3" /> <ArrowLongRightIcon className="h-3 w-3 text-brand-secondary" />
</span> </span>
<Popover className="flex h-full items-center justify-center rounded-lg"> <Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} disabled={isCompleted ?? false}
className={`group flex items-center gap-1 rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-brand-muted-1 ${ className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
open ? "bg-brand-surface-1" : "" cycle.end_date ? "" : "text-brand-secondary"
}`} }`}
> >
<CalendarDaysIcon className="h-3 w-3 " /> <CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${cycle?.end_date}`))}</span> <span>{renderShortDate(new Date(`${cycle?.end_date}`), "End date")}</span>
</Popover.Button> </Popover.Button>
<Transition <Transition
@ -234,7 +234,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden"> <Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker <DatePicker
selected={ selected={
watch("end_date") ? new Date(`${watch("end_date")}`) : new Date() watch("end_date") ? new Date(`${watch("end_date")}`) : new Date()
@ -275,9 +275,9 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div> </div>
</div> </div>
<div className="flex flex-col gap-6 px-6 py-6 w-full"> <div className="flex w-full flex-col gap-6 px-6 py-6">
<div className="flex flex-col items-start justify-start gap-2 w-full"> <div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex items-start justify-between gap-2 w-full"> <div className="flex w-full items-start justify-between gap-2">
<h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4> <h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4>
<CustomMenu width="lg" ellipsis> <CustomMenu width="lg" ellipsis>
{!isCompleted && ( {!isCompleted && (
@ -297,15 +297,15 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</CustomMenu> </CustomMenu>
</div> </div>
<span className="whitespace-normal text-sm leading-5 text-brand-base"> <span className="whitespace-normal text-sm leading-5 text-brand-secondary">
{cycle.description} {cycle.description}
</span> </span>
</div> </div>
<div className="flex flex-col gap-4 text-sm"> <div className="flex flex-col gap-4 text-sm">
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2"> <div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
<UserCircleIcon className="h-5 w-5 text-gray-400" /> <UserCircleIcon className="h-5 w-5" />
<span>Lead</span> <span>Lead</span>
</div> </div>
@ -323,17 +323,17 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
{cycle.owned_by.first_name.charAt(0)} {cycle.owned_by.first_name.charAt(0)}
</span> </span>
)} )}
<span className="text-brand-base">{cycle.owned_by.first_name}</span> <span className="text-brand-secondary">{cycle.owned_by.first_name}</span>
</div> </div>
</div> </div>
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2"> <div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
<ChartPieIcon className="h-5 w-5 text-gray-400" /> <ChartPieIcon className="h-5 w-5" />
<span>Progress</span> <span>Progress</span>
</div> </div>
<div className="flex items-center gap-2.5 text-brand-muted-1"> <div className="flex items-center gap-2.5 text-brand-secondary">
<span className="h-4 w-4"> <span className="h-4 w-4">
<ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} /> <ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} />
</span> </span>

View File

@ -238,7 +238,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
return ( return (
<div> <div>
<div className="flex flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow"> <div className="flex flex-col rounded-[10px] bg-brand-base text-xs shadow">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full"> <a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4"> <div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
@ -269,20 +269,20 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
)} )}
</div> </div>
<div className="flex items-center justify-start gap-5"> <div className="flex items-center justify-start gap-5 text-brand-secondary">
<div className="flex items-start gap-1 "> <div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4 text-brand-base" /> <CalendarDaysIcon className="h-4 w-4" />
<span className="text-brand-secondary">Start :</span> <span>Start :</span>
<span>{renderShortDateWithYearFormat(startDate)}</span> <span>{renderShortDateWithYearFormat(startDate)}</span>
</div> </div>
<div className="flex items-start gap-1 "> <div className="flex items-start gap-1 ">
<TargetIcon className="h-4 w-4 text-brand-base" /> <TargetIcon className="h-4 w-4" />
<span className="text-brand-secondary">End :</span> <span>End :</span>
<span>{renderShortDateWithYearFormat(endDate)}</span> <span>{renderShortDateWithYearFormat(endDate)}</span>
</div> </div>
</div> </div>
<div className="flex items-center justify-between mt-4"> <div className="mt-4 flex items-center justify-between text-brand-secondary">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image <Image
@ -293,11 +293,11 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
alt={cycle.owned_by.first_name} alt={cycle.owned_by.first_name}
/> />
) : ( ) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-brand-base capitalize bg-brand-secondary"> <span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full capitalize">
{cycle.owned_by.first_name.charAt(0)} {cycle.owned_by.first_name.charAt(0)}
</span> </span>
)} )}
<span className="text-brand-base">{cycle.owned_by.first_name}</span> <span>{cycle.owned_by.first_name}</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
{!isCompleted && ( {!isCompleted && (
@ -350,7 +350,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
<Disclosure> <Disclosure>
{({ open }) => ( {({ open }) => (
<div <div
className={`flex h-full w-full flex-col border-t border-brand-base bg-brand-surface-1 ${ className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-brand-base bg-brand-surface-2 text-brand-secondary ${
open ? "" : "flex-row" open ? "" : "flex-row"
}`} }`}
> >

View File

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

View File

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

View File

@ -1,329 +0,0 @@
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { mutate } from "swr";
// services
import estimatesService from "services/estimates.service";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// types
import type { IEstimate, IEstimatePoint } from "types";
// fetch-keys
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) => {
let c = 0;
Object.keys(formData).map((key) => {
if (formData[key as keyof FormValues] === "") c++;
});
if (c !== 0) {
setToastAlert({
type: "error",
title: "Error!",
message: "Please fill all the fields.",
});
return;
}
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 && data.length > 0 ? "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="Point 1"
autoComplete="off"
register={register}
/>
</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="Point 2"
autoComplete="off"
register={register}
/>
</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="Point 3"
autoComplete="off"
register={register}
/>
</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="Point 4"
autoComplete="off"
register={register}
/>
</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="Point 5"
autoComplete="off"
register={register}
/>
</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="Point 6"
autoComplete="off"
register={register}
/>
</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>
);
};

View File

@ -1,4 +1,3 @@
export * from "./create-update-estimate-modal"; export * from "./create-update-estimate-modal";
export * from "./single-estimate"; export * from "./single-estimate";
export * from "./estimate-points-modal" export * from "./delete-estimate-modal";
export * from "./delete-estimate-modal"

View File

@ -2,31 +2,21 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
// services // services
import estimatesService from "services/estimates.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
// components // components
import { EstimatePointsModal, DeleteEstimateModal } from "components/estimates"; import { DeleteEstimateModal } from "components/estimates";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu, SecondaryButton } from "components/ui";
//icons //icons
import { import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
PencilIcon,
TrashIcon,
SquaresPlusIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline";
// helpers // helpers
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IEstimate } from "types"; import { IEstimate } from "types";
// fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
estimate: IEstimate; estimate: IEstimate;
@ -39,7 +29,6 @@ export const SingleEstimate: React.FC<Props> = ({
editEstimate, editEstimate,
handleEstimateDelete, handleEstimateDelete,
}) => { }) => {
const [isEstimatePointsModalOpen, setIsEstimatePointsModalOpen] = useState(false);
const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false); const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
@ -49,18 +38,6 @@ export const SingleEstimate: React.FC<Props> = ({
const { projectDetails, mutateProjectDetails } = useProjectDetails(); const { projectDetails, mutateProjectDetails } = useProjectDetails();
const { data: estimatePoints } = useSWR(
workspaceSlug && projectId ? ESTIMATE_POINTS_LIST(estimate.id) : null,
workspaceSlug && projectId
? () =>
estimatesService.getEstimatesPointsList(
workspaceSlug as string,
projectId as string,
estimate.id
)
: null
);
const handleUseEstimate = async () => { const handleUseEstimate = async () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -87,78 +64,61 @@ export const SingleEstimate: React.FC<Props> = ({
return ( return (
<> <>
<EstimatePointsModal
isOpen={isEstimatePointsModalOpen}
estimate={estimate}
onClose={() => setIsEstimatePointsModalOpen(false)}
data={estimatePoints ? orderArrayBy(estimatePoints, "key") : undefined}
/>
<div className="gap-2 py-3"> <div className="gap-2 py-3">
<div className="flex justify-between items-center"> <div className="flex items-center justify-between">
<div> <div>
<h6 className="flex items-center gap-2 font-medium text-base w-[40vw] truncate"> <h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
{estimate.name} {estimate.name}
{projectDetails?.estimate && projectDetails?.estimate === estimate.id && ( {projectDetails?.estimate && projectDetails?.estimate === estimate.id && (
<span className="capitalize px-2 py-0.5 text-xs rounded bg-green-100 text-green-500"> <span className="rounded bg-green-500/20 px-2 py-0.5 text-xs capitalize text-green-500">
In use In use
</span> </span>
)} )}
</h6> </h6>
<p className="font-sm text-gray-400 font-normal text-[14px] w-[40vw] truncate"> <p className="font-sm w-[40vw] truncate text-[14px] font-normal text-brand-secondary">
{estimate.description} {estimate.description}
</p> </p>
</div> </div>
<CustomMenu ellipsis> <div className="flex items-center gap-2">
{projectDetails?.estimate !== estimate.id && {projectDetails?.estimate !== estimate.id && estimate.points.length > 0 && (
estimatePoints && <SecondaryButton onClick={handleUseEstimate} className="py-1">
estimatePoints.length > 0 && ( Use
<CustomMenu.MenuItem onClick={handleUseEstimate}> </SecondaryButton>
<div className="flex items-center justify-start gap-2"> )}
<SquaresPlusIcon className="h-3.5 w-3.5" /> <CustomMenu ellipsis>
<span>Use estimate</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => setIsEstimatePointsModalOpen(true)}>
<div className="flex items-center justify-start gap-2">
<ListBulletIcon className="h-3.5 w-3.5" />
<span>
{estimatePoints && estimatePoints?.length > 0 ? "Edit points" : "Create points"}
</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
editEstimate(estimate);
}}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-3.5 w-3.5" />
<span>Edit estimate</span>
</div>
</CustomMenu.MenuItem>
{projectDetails?.estimate !== estimate.id && (
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
setIsDeleteEstimateModalOpen(true); editEstimate(estimate);
}} }}
> >
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<TrashIcon className="h-3.5 w-3.5" /> <PencilIcon className="h-3.5 w-3.5" />
<span>Delete estimate</span> <span>Edit estimate</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} {projectDetails?.estimate !== estimate.id && (
</CustomMenu> <CustomMenu.MenuItem
onClick={() => {
setIsDeleteEstimateModalOpen(true);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-3.5 w-3.5" />
<span>Delete estimate</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
</div> </div>
{estimatePoints && estimatePoints.length > 0 ? ( {estimate.points.length > 0 ? (
<div className="flex text-sm text-gray-400"> <div className="flex text-xs text-brand-secondary">
Estimate points ( Estimate points (
<span className="flex gap-1"> <span className="flex gap-1">
{estimatePoints.map((point, index) => ( {orderArrayBy(estimate.points, "key").map((point, index) => (
<h6 key={point.id}> <h6 key={point.id} className="text-brand-secondary">
{point.value} {point.value}
{index !== estimatePoints.length - 1 && ","}{" "} {index !== estimate.points.length - 1 && ","}{" "}
</h6> </h6>
))} ))}
</span> </span>
@ -166,7 +126,7 @@ export const SingleEstimate: React.FC<Props> = ({
</div> </div>
) : ( ) : (
<div> <div>
<p className="text-sm text-gray-400">No estimate points</p> <p className="text-xs text-brand-secondary">No estimate points</p>
</div> </div>
)} )}
</div> </div>

View File

@ -6,29 +6,29 @@ export const CyclesIcon: React.FC<Props> = ({
width = "24", width = "24",
height = "24", height = "24",
className, className,
color = "black", color = "#858E96",
}) => ( }) => (
<svg <svg
width={width} width={width}
height={height} height={height}
className={className} className={className}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M6.5 17.5H3.5V20.5M20.5 20.5H17.5V17.5M17.5 6.5H20.5V3.5M3.5 3.5H6.5V6.5" d="M6.5 17.5H3.5V20.5M20.5 20.5H17.5V17.5M17.5 6.5H20.5V3.5M3.5 3.5H6.5V6.5"
stroke={color} stroke={color}
strokeWidth="1" strokeWidth="1"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M6.5 3.647C3.789 5.4355 2 8.509 2 12C2 12.51 2.038 13.0105 2.1115 13.5M13.5 21.888C13.0035 21.9626 12.5021 22.0001 12 22C8.509 22 5.4355 20.211 3.647 17.5M21.888 10.5C21.962 10.9895 22 11.49 22 12C22 15.491 20.211 18.5645 17.5 20.353M10.5 2.1115C10.9965 2.03703 11.4979 1.99976 12 2C15.491 2 18.5645 3.789 20.353 6.5" d="M6.5 3.647C3.789 5.4355 2 8.509 2 12C2 12.51 2.038 13.0105 2.1115 13.5M13.5 21.888C13.0035 21.9626 12.5021 22.0001 12 22C8.509 22 5.4355 20.211 3.647 17.5M21.888 10.5C21.962 10.9895 22 11.49 22 12C22 15.491 20.211 18.5645 17.5 20.353M10.5 2.1115C10.9965 2.03703 11.4979 1.99976 12 2C15.491 2 18.5645 3.789 20.353 6.5"
stroke={color} stroke={color}
strokeWidth="1" strokeWidth="1"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
); );

View File

@ -6,15 +6,21 @@ export const TargetIcon: React.FC<Props> = ({
width = "24", width = "24",
height = "24", height = "24",
className, className,
color = "black", color = "#858E96",
}) => ( }) => (
<svg width={width} height={height} className={className} color={color} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xlinkHref="http://www.w3.org/1999/xlink"> <svg
<rect width="16" height="16" fill="url(#pattern0)"/> width={width}
<defs> height={height}
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1"> className={className}
<use xlinkHref="#image0_2094_50417" transform="scale(0.01)"/> viewBox="0 0 18 18"
</pattern> fill="none"
<image id="image0_2094_50417" width="100" height="100" xlinkHref="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMyElEQVR4nO1de3AdZRX/3TQpBdJH2gZR+zCJUHDUOoiKSO2DWhkZRjttaauAPEpQRKWog9Q+RYGqbcdhUOlgKeXhCNVIq/KHTxCfQSwtIAVtG8ACaRraatskTe51DnOusx7O3nu+b3fv3Zu5v5mdySQ5u99+5/vO+zsLVFFFFVVUUcXgxTAAkwFcCGApgHsB/BHAdgD/BNANoBdAP//8IoB/APgdgE0AVgG4FMC7ANSW+2UqEXUAzgGwHMBvAPQAyMV0/QfAIwBuAfBBAEPK/bJpRQ2AcwHcBeBQjAwodu0DsBHA+TEzZyUqFOMB3ATg+RIyIRdyPc8TSWOKgpV8v4rCWwHcwbLfMlm7AfwUwLcAXAlgKoC3A2gC0MBibgj//GYApwKYyf97M4CfAHjJ+KxjAO4E0BKBGRXDkCZWyv1FJuVfrJBJGU+I8flvAfBxAPcAeNXImAkezEg9Q45jC+lIgQn4N8vzGaxTkkYd76KNRcZ1GMANAIY6MCPVDKGX3lnghelvlwM4sYxjJFF3LYDnCozzGbbMLMxIJUNqebADIQMmP+KSlJmeNQDmAXg2ZMxZAN8O7JYwZqSOIRMB/ClkoC8DuBhABulFHYDPFNAz5JiuLaKDUoPz2GOWAyRFfhuAUagcnMwGQNwmdsn8lIsA9CkD2APgLFQuzgfQWWnM+GyIvngQwGhUPk4C8FBEZqwo1WC/GiKiFqdcV/go/dTvjMXKwykgOCeBFTqHV9kPAPwVwC7WV318dbN4/AuA+3ihfBTA2BjHkXqdkRUPPwhgekz3P5OtmR3Kc1wuon2K42YUyo+C1DLjPEWBH+RJjILhAK7jCcwldD0J4CoAJ3iML5XMmKiYtj0Rd0YDv8D+BBkhry4ASwAcbxzj/DQyo05x+voj6AxS+pcZzMp+fi5FfBcBmCKivbX880QOcVA4Zh3rk2NF7v0Ci99izDiWNmaAJ0QOgBS7D1o41Rr2Yr0A2pjZIyKMeQRHeduKhPwpxP+mSmLGTEW5tnmatnMBHCggSpbFbB3l8UYAN3K2UHs2icyPFGHGMf592UPoO5XEEYkJFxDzvlkg570EQD2Sx0i2urTQOzm41wNYEJEZIzn0QnMXO5YqMv39jvcgWX93CDMeZPlfajRzMYVFHLkyI69rae5iRZOykihQ6MqMLcpLHgVwNcqLDOvBYwkwI5/kinWx3aeE0Ec5vvDdIanaM5AeTA2JVLvqjDHKfSgdHFtBgsyBUz7DBZrOeJZ3XpowP0YF/nnlPqfEMcg7xI23OVpVc0OY8QakC/NjtqbqlMwjFW5EwnjFbqeSThc/44AiptK2M+YpzCCp8ImI971M3LOPy5S8cZO44U6HHHhGcfqOxqAzWtgIIL32OPssfaw4d3N6dSN73pTxi3NnNPMqzzjskl1x5URqlIpCCkf4ro5cBGtqCICFAH5vNE/zFzmxvwAwKyZmdPD/0LtZca0iIbyKvs9V6qaspToNSmyK/AwfzOISnFzEi/In7/FkRlOAGXS94uC8NvDuDT4nbIEUxF3iJiQGrJDlMT52+PEA1sfACDnhN7KV6KLAT1FSDVSdb8VGQft9x7l4re5IVqFTRaE1nyFD6BQOcUEjgPYiq50mZBqA03jnnsBln1P5b486MqqYNXWroHnVYZd8WNB2F6mEfB3OUeSetbzzOkHb5RibagTw9xB98ACASQ73mswOaaFso9W0beQEXJCWCjusyl0uUq0KMhTLI9jPMtNHUVsXMdWuTNquiGVE00Oq3139jK8J+qcdaDcJWjrZZYYMtlEVugVnCrpexxD6emXSHo4hDB+X09es7LZ3G2kvFXQkUs1n+uQxMmtJ/lolV2LFrBBmOMnaEnjgcrFSWMiCiYKuh0WZSe4GCcnZsmKHoJ3j4Gc8o4iptOyMIC5RisateFHQvsNCdKEg2upQN5UVL05haAsWKgr8LESDq59hLdAYLd4zy1lIC34mxlMsj/8algkiyqFbMEfQ/Rl2SA/8AUSDjwf+S4f7SyuQivEsuFnQUWiqKGS1N53Ts2CFoFvjEJuSK24S/OEbDsk6BP42eBY2yHASmeTOq5UcLZ8kFpXqWHC14vSVSmf83HPxXSno7vc0XMhAKIrtgohOu1rwmKCjuikfRi6HW3pgM0cVuj0U+CrPrN4UQUfpWgveJugoX1IUuwWRNQa1R9DRiVcLHhd00xyYUajK0WJNTRc0tKgsaFIK7Sw4WdCRs1oUXYKI8sOlpDvVSLc5IjPy+kvWClgwUtBREs5asBeko1BMUcgModUxi4tuuJGuUAsOEl8W1CsJNAvqBF1fknSDgSEHjPcYrnjPFhxXSrpKEVltBRjyQ+M9Jgk6SqpZMFbQ7UuSrtxKfbrDZGo1VMTgccZ7zIhJqe9Kkq7UZu+9Ec3e+1k5HuSdYWWGltk0OWrsmwXp6MiDBe8UdE9YiB4tsWP46RgdQ1f8TTy71UjXKugoumGBNLOpKsc5dNLqmdRa45ljyEYMnVhxuhhv1kE8rxO01mLqT/kk/pZ5TmyU4KLclZuRPH4snkmt/6yQWc3Znoz8ik/4nU4VwZhzzorKP2v4fYGyWs9GcviAYgxQ9aK1rKdfnCWxWpQydkZlts6KxyVBtd3ngZygelp5LjE5bjQqFuEOhyIOefCTrEQrOnwMpmHssfqYvmsEHbXWczkul0sghRvEUBZNcjda42dQzrisdtCVQbojLierfi2IraWTZ0QscvhuCFPi2CmNCjNyXG8Fh6yoLJizNiK4QtC5JMRep9it9rmWV3fxK4axMZBTHKizI+oMKabo+oPj+b9VPn5EiL+1JIrS2+sgYxdHLJQbq+iTvGj5EZurVpyuWFPBbg5WZZyP1MrIAB3KserIlwWtU81AnRK8IxlvQb0Sn3I99Di2QEe6HDt0Kzn0cRo/s55/nsF/k06f3BkuzNC8+k6H4vNZSvDTuQL+TnETKr72FXmHHWJbQfH1nQKT6nNlWWe4HlNuUQwd6lRqhXS26VSaM2ZEOI4wikv2ffwZbRxxNKLZ4WhN5ZFRGpftdUgT1PP5e59w1P+hRrGbyVLwLShzKU7WxjKfYz8ubZqybFnNi9ALWB64yXGbDnjGvXZHaer2dSUp73Kk7WElIRO1fVMTt1a6hyPMXWxe93J+4TG2Cls9xKTEe5UE2q8c6Gv50xlBejqf4o1xyoAozGHFBKUQodMhCVVONCsV892OTL5YWZCRDn1qFenbHbfcbEXMPJfCY9GyOkR2uaZ3uAB21CjmOzm+kdGi1Dt90vEesoQyzxRahWlDiyJmfETNIkHfF4MI/R9kW4xXHLsAZZRzdnlrJapOiRPvUxy4/JlAF6kwRmn/FFtrDXBw8XDE7VfLwcacIld9ra+4kGFrSmtu1ubhxK1XXAaX1LIJN4iHDHjEl2pDdkreT2lCeURUWIPkDR7MmKI0lP5SEgMfqpThd3h0rc6wTtH8iSMcjLQ6XVEwgsMhWgOzLOuMjEfI5wUlZmY6KeXrNcuJ3OLp6HwspIwnx6byioQSVCdx1LbQs12sqTwyyoGcAdfTtj5YrbzEFz3vRVbHb0MmJseWyRZOK0f5qkIDe/pbQz4WEHT6fD+x9GXlfuRYJ4465QzJgENOWltZFxk+2tXPxQXr2AufymbzaBanQ/nnZo5ZtfL/thu+ebXXMRwisUDRG4+U8kOWmgfew/1RfDGSw/Rh3UKTuDo5URRFZ81UrLN9SVhVxfAhZSCHRIMXH9SzCSoLJuK8nuDk0okxxLpk3qjXoQ1J7FiobNVDEXdKEPT92m9wsinsW1aWa4CrQ1bH0Iw/uDMOKc8pez/fzykT0OPYec5qUs4OfKC4nT9OvD8Q7d3Pv2vnaPBSpnHNDlp0huZEXoOUQPta2QA7RIPpgy4Ztqa03erS06UkuCZkoFsTWKHlwBiOJsj3I7/sC0gpFoZs5Q6uZKlUTFE88LwCL7vOsFhfXSEi7HsV9oGwMRwo1Hb+vnJaU64YX6CjWydXQpbie7e+qOF8hraw8k5fyf2MpD+9+iQXQpTMm3X49OpTRT69mliwsBSYFlKJGMwcLirRJyrCUM/tMrRMYXABJR4oLBXq2DMudJT5KJ8XvKBEu6aGe0vezgmksHEd5p0eZxV+ajCBKyCLfT7oJXbsLo8zD80JsCvYsdRStcGrj9OuFacrfCfmdqWVYC7k6uDTR2u5JmsaHywKi/ZO5gOWVzHNQ0rRX9jVw+npOBdCxWAcd/q0TlYuwWsPZwkj100NBtTwqt+g9MZN8jrAFSVTB1mIJ1YM4dar13MjfS3n7XsdYf/oFo7WDkpFnTSGsr6Yy8eIN/GkbisQ7d3G/7OJE1BzuQNolQFVVFFFFVVgsOK/CQ2naFV2iSEAAAAASUVORK5CYII="/> xmlns="http://www.w3.org/2000/svg"
</defs> >
<circle cx="9" cy="9" r="5.4375" stroke={color} strokeLinecap="round" />
<path
fill={color}
stroke-width="0.5"
d="M17.6033 7.51926C18.1597 9.22867 18.1241 11.0757 17.5021 12.7624C16.8802 14.4491 15.7083 15.8771 14.1753 16.8161C12.6424 17.7551 10.8378 18.1504 9.05269 17.9382C7.26757 17.7259 5.60599 16.9185 4.33594 15.6463C3.0659 14.374 2.26145 12.711 2.05235 10.9255C1.84325 9.14002 2.24169 7.33614 3.18341 5.80485C4.12512 4.27355 5.5552 3.10411 7.24298 2.48516C8.93076 1.86621 10.7778 1.83384 12.4863 2.39326L11.4383 3.44026C11.3803 3.49726 11.3283 3.56026 11.2803 3.62526C9.91235 3.34916 8.49189 3.52063 7.22898 4.11431C5.96606 4.70799 4.92774 5.69235 4.26757 6.92182C3.6074 8.15128 3.36045 9.56057 3.56322 10.9413C3.76599 12.3219 4.40773 13.6007 5.39353 14.5884C6.37933 15.5762 7.65683 16.2204 9.03712 16.4259C10.4174 16.6314 11.8272 16.3872 13.0579 15.7295C14.2887 15.0717 15.2751 14.0353 15.8713 12.7736C16.4674 11.5118 16.6417 10.0917 16.3683 8.72326C16.4367 8.67472 16.5009 8.62053 16.5603 8.56126L17.6023 7.51926H17.6033ZM14.8983 9.00026C15.1129 10.0534 14.9826 11.1477 14.5264 12.1209C14.0703 13.0942 13.3127 13.8945 12.3659 14.4033C11.4191 14.9121 10.3336 15.1022 9.27028 14.9456C8.20695 14.7889 7.22239 14.2938 6.46258 13.5336C5.70276 12.7734 5.20814 11.7886 5.05203 10.7252C4.89593 9.66176 5.08665 8.57635 5.59593 7.62984C6.10521 6.68334 6.90593 5.92615 7.87938 5.4705C8.85284 5.01486 9.94721 4.88503 11.0003 5.10026V6.64626C10.2539 6.42346 9.45458 6.45598 8.7288 6.73866C8.00302 7.02135 7.39227 7.53806 6.99325 8.20697C6.59423 8.87589 6.42973 9.65879 6.52581 10.4317C6.62188 11.2047 6.97304 11.9235 7.52368 12.4744C8.07432 13.0252 8.79298 13.3767 9.56588 13.4731C10.3388 13.5695 11.1218 13.4053 11.7908 13.0066C12.4599 12.6079 12.9769 11.9973 13.2599 11.2717C13.5429 10.546 13.5757 9.7467 13.3533 9.00026H14.8983ZM9.99826 11.5003C10.2283 11.5004 10.4553 11.4476 10.6617 11.346C10.868 11.2443 11.0483 11.0966 11.1884 10.9142C11.3286 10.7318 11.4249 10.5196 11.47 10.294C11.515 10.0684 11.5076 9.83551 11.4483 9.61326L13.0303 8.03026L13.0603 8.00026H15.5003C15.566 8.00038 15.631 7.98754 15.6918 7.96249C15.7525 7.93744 15.8077 7.90066 15.8543 7.85426L17.8543 5.85426C17.9244 5.78433 17.9721 5.69516 17.9915 5.59805C18.0109 5.50094 18.001 5.40027 17.963 5.3088C17.9251 5.21732 17.8609 5.13917 17.7785 5.08424C17.6961 5.02931 17.5993 5.00008 17.5003 5.00026H15.0003V2.50026C15.0002 2.40144 14.9709 2.30485 14.9161 2.22268C14.8612 2.1405 14.7832 2.07643 14.6919 2.03855C14.6006 2.00068 14.5002 1.99069 14.4033 2.00986C14.3063 2.02903 14.2172 2.0765 14.1473 2.14626L12.1473 4.14626C12.1007 4.1927 12.0637 4.24787 12.0385 4.30861C12.0133 4.36936 12.0003 4.43448 12.0003 4.50026V6.94026C11.99 6.94998 11.98 6.95998 11.9703 6.97026L10.3883 8.55026C10.1658 8.49057 9.93264 8.48287 9.70675 8.52775C9.48086 8.57263 9.26833 8.66889 9.0856 8.80908C8.90288 8.94926 8.75486 9.12961 8.65302 9.33617C8.55117 9.54273 8.49821 9.76996 8.49826 10.0003C8.49826 10.3981 8.65629 10.7796 8.9376 11.0609C9.2189 11.3422 9.60043 11.5003 9.99826 11.5003Z"
/>
</svg> </svg>
); );

View File

@ -73,7 +73,7 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto"> <div className="fixed inset-0 z-20 overflow-y-auto">
@ -87,12 +87,12 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6"> <div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-100 p-4"> <span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon <ExclamationTriangleIcon
className="h-6 w-6 text-red-600" className="h-6 w-6 text-red-500"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@ -101,16 +101,19 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
</span> </span>
</div> </div>
<span> <span>
<p className="text-sm leading-7 text-gray-500"> <p className="text-sm leading-7 text-brand-secondary">
Are you sure you want to delete project{" "} Are you sure you want to delete import from{" "}
<span className="break-all font-semibold">{data?.service}</span>? All of the <span className="break-all font-semibold capitalize text-brand-base">
data related to the project will be permanently removed. This action cannot be {data?.service}
undone </span>
? All of the data related to the import will be permanently removed. This
action cannot be undone.
</p> </p>
</span> </span>
<div className="text-gray-600"> <div>
<p className="text-sm"> <p className="text-sm text-brand-secondary">
To confirm, type <span className="font-medium">delete import</span> below: To confirm, type{" "}
<span className="font-medium text-brand-base">delete import</span> below:
</p> </p>
<Input <Input
type="text" type="text"

View File

@ -36,7 +36,7 @@ export const GithubImportConfigure: React.FC<Props> = ({
<div className="flex items-center gap-2 py-5"> <div className="flex items-center gap-2 py-5">
<div className="w-full"> <div className="w-full">
<div className="font-medium">Configure</div> <div className="font-medium">Configure</div>
<div className="text-sm text-gray-600">Set up your GitHub import.</div> <div className="text-sm text-brand-secondary">Set up your GitHub import.</div>
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<GithubAuth workspaceIntegration={workspaceIntegration} provider={provider} /> <GithubAuth workspaceIntegration={workspaceIntegration} provider={provider} />

View File

@ -14,7 +14,7 @@ type Props = {
export const GithubImportConfirm: FC<Props> = ({ handleStepChange, watch }) => ( export const GithubImportConfirm: FC<Props> = ({ handleStepChange, watch }) => (
<div className="mt-6"> <div className="mt-6">
<h4 className="font-medium"> <h4 className="font-medium text-brand-secondary">
You are about to import issues from {watch("github").full_name}. Click on {'"'}Confirm & You are about to import issues from {watch("github").full_name}. Click on {'"'}Confirm &
Import{'" '} Import{'" '}
to complete the process. to complete the process.

View File

@ -1,4 +1,4 @@
import { FC, useState } from "react"; import { FC } from "react";
// react-hook-form // react-hook-form
import { Control, Controller, UseFormWatch } from "react-hook-form"; import { Control, Controller, UseFormWatch } from "react-hook-form";
@ -7,7 +7,7 @@ import useProjects from "hooks/use-projects";
// components // components
import { SelectRepository, TFormValues, TIntegrationSteps } from "components/integration"; import { SelectRepository, TFormValues, TIntegrationSteps } from "components/integration";
// ui // ui
import { CustomSearchSelect, PrimaryButton, SecondaryButton } from "components/ui"; import { CustomSearchSelect, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
@ -36,7 +36,7 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8"> <div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Repository</h4> <h4 className="font-semibold">Select Repository</h4>
<p className="text-gray-500 text-xs"> <p className="text-xs text-brand-secondary">
Select the repository that you want the issues to be imported from. Select the repository that you want the issues to be imported from.
</p> </p>
</div> </div>
@ -49,7 +49,13 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<SelectRepository <SelectRepository
integration={integration} integration={integration}
value={value ? value.id : null} value={value ? value.id : null}
label={value ? `${value.full_name}` : "Select Repository"} label={
value ? (
`${value.full_name}`
) : (
<span className="text-brand-secondary">Select Repository</span>
)
}
onChange={onChange} onChange={onChange}
characterLimit={50} characterLimit={50}
/> />
@ -61,7 +67,9 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8"> <div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Project</h4> <h4 className="font-semibold">Select Project</h4>
<p className="text-gray-500 text-xs">Select the project to import the issues to.</p> <p className="text-xs text-brand-secondary">
Select the project to import the issues to.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-4"> <div className="col-span-12 sm:col-span-4">
{projects && ( {projects && (
@ -71,7 +79,13 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomSearchSelect <CustomSearchSelect
value={value} value={value}
label={value ? projects.find((p) => p.id === value)?.name : "Select Project"} label={
value ? (
projects.find((p) => p.id === value)?.name
) : (
<span className="text-brand-secondary">Select Project</span>
)
}
onChange={onChange} onChange={onChange}
options={options} options={options}
optionsClassName="w-full" optionsClassName="w-full"
@ -84,30 +98,16 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8"> <div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Sync Issues</h4> <h4 className="font-semibold">Sync Issues</h4>
<p className="text-gray-500 text-xs">Set whether you want to sync the issues or not.</p> <p className="text-xs text-brand-secondary">
Set whether you want to sync the issues or not.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-4"> <div className="col-span-12 sm:col-span-4">
<Controller <Controller
control={control} control={control}
name="sync" name="sync"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<button <ToggleSwitch value={value} onChange={() => onChange(!value)} />
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 ${
value ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked={value ? true : false}
onClick={() => onChange(!value)}
>
<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 ${
value ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
)} )}
/> />
</div> </div>

View File

@ -25,9 +25,9 @@ export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers
return ( return (
<div className="mt-6"> <div className="mt-6">
<div> <div>
<div className="grid grid-cols-3 gap-2 text-sm mb-2 font-medium"> <div className="mb-2 grid grid-cols-3 gap-2 text-sm font-medium">
<div>Name</div> <div className="text-brand-secondary">Name</div>
<div>Import as...</div> <div className="text-brand-secondary">Import as...</div>
<div className="text-right"> <div className="text-right">
{users.filter((u) => u.import !== false).length} users selected {users.filter((u) => u.import !== false).length} users selected
</div> </div>

View File

@ -64,20 +64,20 @@ export const GithubRepoDetails: FC<Props> = ({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div> <div>
<div className="font-medium">Repository Details</div> <div className="font-medium">Repository Details</div>
<div className="text-sm text-gray-600">Import completed. We have found:</div> <div className="text-sm text-brand-secondary">Import completed. We have found:</div>
</div> </div>
<div className="flex gap-16 mt-4"> <div className="mt-4 flex gap-16">
<div className="text-center flex-shrink-0"> <div className="flex-shrink-0 text-center">
<p className="text-3xl font-bold">{repoInfo.issue_count}</p> <p className="text-3xl font-bold">{repoInfo.issue_count}</p>
<h6 className="text-sm text-gray-500">Issues</h6> <h6 className="text-sm text-brand-secondary">Issues</h6>
</div> </div>
<div className="text-center flex-shrink-0"> <div className="flex-shrink-0 text-center">
<p className="text-3xl font-bold">{repoInfo.labels}</p> <p className="text-3xl font-bold">{repoInfo.labels}</p>
<h6 className="text-sm text-gray-500">Labels</h6> <h6 className="text-sm text-brand-secondary">Labels</h6>
</div> </div>
<div className="text-center flex-shrink-0"> <div className="flex-shrink-0 text-center">
<p className="text-3xl font-bold">{repoInfo.collaborators.length}</p> <p className="text-3xl font-bold">{repoInfo.collaborators.length}</p>
<h6 className="text-sm text-gray-500">Users</h6> <h6 className="text-sm text-brand-secondary">Users</h6>
</div> </div>
</div> </div>
</div> </div>

View File

@ -175,15 +175,13 @@ export const GithubImporterRoot = () => {
<form onSubmit={handleSubmit(createGithubImporterService)}> <form onSubmit={handleSubmit(createGithubImporterService)}>
<div className="space-y-2"> <div className="space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}> <Link href={`/${workspaceSlug}/settings/import-export`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900"> <div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-brand-secondary hover:text-brand-base">
<div> <ArrowLeftIcon className="h-3 w-3" />
<ArrowLeftIcon className="h-3 w-3" />
</div>
<div>Cancel import & go back</div> <div>Cancel import & go back</div>
</div> </div>
</Link> </Link>
<div className="space-y-4 rounded-[10px] border border-gray-200 bg-white p-4"> <div className="space-y-4 rounded-[10px] border border-brand-base bg-brand-base p-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-10 w-10 flex-shrink-0"> <div className="h-10 w-10 flex-shrink-0">
<Image src={GithubLogo} alt="GithubLogo" /> <Image src={GithubLogo} alt="GithubLogo" />
@ -194,12 +192,12 @@ export const GithubImporterRoot = () => {
<div <div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${ className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${
index <= activeIntegrationState() index <= activeIntegrationState()
? `border-[#3F76FF] bg-[#3F76FF] text-white ${ ? `border-brand-accent bg-brand-accent ${
index === activeIntegrationState() index === activeIntegrationState()
? "border-opacity-100 bg-opacity-100" ? "border-opacity-100 bg-opacity-100"
: "border-opacity-80 bg-opacity-80" : "border-opacity-80 bg-opacity-80"
}` }`
: "border-gray-300" : "border-brand-base"
}`} }`}
> >
<integration.icon <integration.icon
@ -213,8 +211,8 @@ export const GithubImporterRoot = () => {
key={index} key={index}
className={`border-b px-7 ${ className={`border-b px-7 ${
index <= activeIntegrationState() - 1 index <= activeIntegrationState() - 1
? `border-[#3F76FF]` ? `border-brand-accent`
: `border-gray-300` : `border-brand-base`
}`} }`}
> >
{" "} {" "}

View File

@ -16,7 +16,7 @@ import { IWorkspaceIntegration } from "types";
type Props = { type Props = {
integration: IWorkspaceIntegration; integration: IWorkspaceIntegration;
value: any; value: any;
label: string; label: string | JSX.Element;
onChange: (repo: any) => void; onChange: (repo: any) => void;
characterLimit?: number; characterLimit?: number;
}; };

View File

@ -64,9 +64,9 @@ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users,
})) ?? []; })) ?? [];
return ( return (
<div className="bg-gray-50 px-2 py-3 rounded-md grid grid-cols-3 items-center gap-2"> <div className="grid grid-cols-3 items-center gap-2 rounded-md bg-brand-surface-2 px-2 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative h-8 w-8 rounded flex-shrink-0"> <div className="relative h-8 w-8 flex-shrink-0 rounded">
<Image <Image
src={collaborator.avatar_url} src={collaborator.avatar_url}
layout="fill" layout="fill"
@ -112,7 +112,7 @@ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users,
setUsers(newUsers); setUsers(newUsers);
}} }}
placeholder="Enter email of the user" placeholder="Enter email of the user"
className="py-1 border-gray-200 text-xs" className="py-1 text-xs"
/> />
)} )}
{users[index].import === "map" && members && ( {users[index].import === "map" && members && (

View File

@ -52,10 +52,10 @@ const IntegrationGuide = () => {
handleClose={() => setDeleteImportModal(false)} handleClose={() => setDeleteImportModal(false)}
data={importToDelete} data={importToDelete}
/> />
<div className="space-y-2 h-full"> <div className="h-full space-y-2">
{!provider && ( {!provider && (
<> <>
<div className="flex items-center gap-2 mb-5"> <div className="mb-5 flex items-center gap-2">
<div className="h-full w-full space-y-1"> <div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div> <div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm"> <div className="text-sm">
@ -72,7 +72,10 @@ const IntegrationGuide = () => {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{IMPORTERS_EXPORTERS_LIST.map((service) => ( {IMPORTERS_EXPORTERS_LIST.map((service) => (
<div key={service.provider} className="rounded-[10px] border bg-white p-4"> <div
key={service.provider}
className="rounded-[10px] border border-brand-base bg-brand-base p-4"
>
<div className="flex items-center gap-4 whitespace-nowrap"> <div className="flex items-center gap-4 whitespace-nowrap">
<div className="relative h-10 w-10 flex-shrink-0"> <div className="relative h-10 w-10 flex-shrink-0">
<Image <Image
@ -84,7 +87,7 @@ const IntegrationGuide = () => {
</div> </div>
<div className="w-full"> <div className="w-full">
<h3>{service.title}</h3> <h3>{service.title}</h3>
<p className="text-sm text-gray-500">{service.description}</p> <p className="text-sm text-brand-secondary">{service.description}</p>
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Link <Link
@ -101,12 +104,12 @@ const IntegrationGuide = () => {
</div> </div>
))} ))}
</div> </div>
<div className="rounded-[10px] border bg-white p-4"> <div className="rounded-[10px] border border-brand-base bg-brand-base p-4">
<h3 className="mb-2 font-medium text-lg flex gap-2"> <h3 className="mb-2 flex gap-2 text-lg font-medium">
Previous Imports Previous Imports
<button <button
type="button" type="button"
className="flex-shrink-0 flex items-center gap-1 outline-none text-xs py-1 px-1.5 bg-gray-100 rounded" className="flex flex-shrink-0 items-center gap-1 rounded bg-brand-surface-2 py-1 px-1.5 text-xs outline-none"
onClick={() => { onClick={() => {
setRefreshing(true); setRefreshing(true);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() => mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
@ -133,10 +136,12 @@ const IntegrationGuide = () => {
</div> </div>
</div> </div>
) : ( ) : (
<div className="py-2 text-sm text-gray-800">No previous imports available.</div> <p className="py-2 text-sm text-brand-secondary">
No previous imports available.
</p>
) )
) : ( ) : (
<Loader className="grid grid-cols-1 gap-3 mt-6"> <Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" /> <Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" /> <Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" /> <Loader.Item height="40px" width="100%" />

View File

@ -17,30 +17,30 @@ export const JiraConfirmImport: React.FC = () => {
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<p className="text-sm text-gray-500">Migrating</p> <p className="text-sm text-brand-secondary">Migrating</p>
</div> </div>
<div className="col-span-1 flex items-center justify-between"> <div className="col-span-1 flex items-center justify-between">
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_issues")}</h4> <h4 className="mb-2 text-lg font-semibold">{watch("data.total_issues")}</h4>
<p className="text-sm text-gray-500">Issues</p> <p className="text-sm text-brand-secondary">Issues</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_states")}</h4> <h4 className="mb-2 text-lg font-semibold">{watch("data.total_states")}</h4>
<p className="text-sm text-gray-500">States</p> <p className="text-sm text-brand-secondary">States</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_modules")}</h4> <h4 className="mb-2 text-lg font-semibold">{watch("data.total_modules")}</h4>
<p className="text-sm text-gray-500">Modules</p> <p className="text-sm text-brand-secondary">Modules</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_labels")}</h4> <h4 className="mb-2 text-lg font-semibold">{watch("data.total_labels")}</h4>
<p className="text-sm text-gray-500">Labels</p> <p className="text-sm text-brand-secondary">Labels</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold"> <h4 className="mb-2 text-lg font-semibold">
{watch("data.users").filter((user) => user.import).length} {watch("data.users").filter((user) => user.import).length}
</h4> </h4>
<p className="text-sm text-gray-500">User</p> <p className="text-sm text-brand-secondary">User</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -30,15 +30,11 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="h-full w-full space-y-8 overflow-y-auto"> <div className="h-full w-full space-y-8 overflow-y-auto">
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Personal Access Token</h3> <h3 className="font-semibold">Jira Personal Access Token</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-brand-secondary">
Get to know your access token by navigating to{" "} Get to know your access token by navigating to{" "}
<Link href="https://id.atlassian.com/manage-profile/security/api-tokens"> <Link href="https://id.atlassian.com/manage-profile/security/api-tokens">
<a <a className="text-brand-accent underline" target="_blank" rel="noreferrer">
className="font-medium text-gray-600 hover:text-gray-900"
target="_blank"
rel="noreferrer"
>
Atlassian Settings Atlassian Settings
</a> </a>
</Link> </Link>
@ -61,8 +57,8 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Project Key</h3> <h3 className="font-semibold">Jira Project Key</h3>
<p className="text-sm text-gray-500">If XXX-123 is your issue, then enter XXX</p> <p className="text-sm text-brand-secondary">If XXX-123 is your issue, then enter XXX</p>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Input <Input
@ -80,8 +76,8 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Email Address</h3> <h3 className="font-semibold">Jira Email Address</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-brand-secondary">
Enter the Gmail account that you use in Jira account Enter the Gmail account that you use in Jira account
</p> </p>
</div> </div>
@ -102,8 +98,8 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Installation or Cloud Host Name</h3> <h3 className="font-semibold">Jira Installation or Cloud Host Name</h3>
<p className="text-sm text-gray-500">Enter your companies cloud host name</p> <p className="text-sm text-brand-secondary">Enter your companies cloud host name</p>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Input <Input
@ -122,8 +118,10 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Import to project</h3> <h3 className="font-semibold">Import to project</h3>
<p className="text-sm text-gray-500">Select which project you want to import to.</p> <p className="text-sm text-brand-secondary">
Select which project you want to import to.
</p>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Controller <Controller
@ -138,9 +136,11 @@ export const JiraGetImportDetail: React.FC = () => {
onChange={onChange} onChange={onChange}
label={ label={
<span> <span>
{value && value !== "" {value && value !== "" ? (
? projects.find((p) => p.id === value)?.name projects.find((p) => p.id === value)?.name
: "Select Project"} ) : (
<span className="text-brand-secondary">Select a project</span>
)}
</span> </span>
} }
> >
@ -151,7 +151,7 @@ export const JiraGetImportDetail: React.FC = () => {
</CustomSelect.Option> </CustomSelect.Option>
)) ))
) : ( ) : (
<div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500"> <div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-brand-secondary">
<p>You don{"'"}t have any project. Please create a project first.</p> <p>You don{"'"}t have any project. Please create a project first.</p>
</div> </div>
)} )}
@ -162,7 +162,7 @@ export const JiraGetImportDetail: React.FC = () => {
const event = new KeyboardEvent("keydown", { key: "p" }); const event = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(event); document.dispatchEvent(event);
}} }}
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500" className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-brand-secondary"
> >
<PlusIcon className="h-4 w-4 text-gray-500" /> <PlusIcon className="h-4 w-4 text-gray-500" />
<span>Create new project</span> <span>Create new project</span>

View File

@ -52,11 +52,13 @@ export const JiraImportUsers: FC = () => {
})) ?? []; })) ?? [];
return ( return (
<div className="h-full w-full space-y-10 divide-y-2 overflow-y-auto"> <div className="h-full w-full space-y-10 divide-y-2 divide-brand-base overflow-y-auto">
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Users</h3> <h3 className="font-semibold">Users</h3>
<p className="text-sm text-gray-500">Update, invite or choose not to invite assignee</p> <p className="text-sm text-brand-secondary">
Update, invite or choose not to invite assignee
</p>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Controller <Controller
@ -72,8 +74,8 @@ export const JiraImportUsers: FC = () => {
{watch("data.invite_users") && ( {watch("data.invite_users") && (
<div className="pt-6"> <div className="pt-6">
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="col-span-1 text-gray-500">Name</div> <div className="col-span-1 text-sm text-brand-secondary">Name</div>
<div className="col-span-1 text-gray-500">Import as</div> <div className="col-span-1 text-sm text-brand-secondary">Import as</div>
</div> </div>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">

View File

@ -102,12 +102,12 @@ export const JiraProjectDetail: React.FC<Props> = (props) => {
if (error) { if (error) {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<p className="text-sm text-gray-500"> <p className="text-sm text-brand-secondary">
Something went wrong. Please{" "} Something went wrong. Please{" "}
<button <button
onClick={() => setCurrentStep({ state: "import-configure" })} onClick={() => setCurrentStep({ state: "import-configure" })}
type="button" type="button"
className="inline text-blue-500 underline" className="inline text-brand-accent underline"
> >
go back go back
</button>{" "} </button>{" "}
@ -121,37 +121,37 @@ export const JiraProjectDetail: React.FC<Props> = (props) => {
<div className="h-full w-full space-y-10 overflow-y-auto"> <div className="h-full w-full space-y-10 overflow-y-auto">
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Import Data</h3> <h3 className="font-semibold">Import Data</h3>
<p className="text-sm text-gray-500">Import Completed. We have found:</p> <p className="text-sm text-brand-secondary">Import Completed. We have found:</p>
</div> </div>
<div className="col-span-1 flex items-center justify-between"> <div className="col-span-1 flex items-center justify-between">
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.issues}</h4> <h4 className="mb-2 text-lg font-semibold">{projectInfo?.issues}</h4>
<p className="text-sm text-gray-500">Issues</p> <p className="text-sm text-brand-secondary">Issues</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.states}</h4> <h4 className="mb-2 text-lg font-semibold">{projectInfo?.states}</h4>
<p className="text-sm text-gray-500">States</p> <p className="text-sm text-brand-secondary">States</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.modules}</h4> <h4 className="mb-2 text-lg font-semibold">{projectInfo?.modules}</h4>
<p className="text-sm text-gray-500">Modules</p> <p className="text-sm text-brand-secondary">Modules</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.labels}</h4> <h4 className="mb-2 text-lg font-semibold">{projectInfo?.labels}</h4>
<p className="text-sm text-gray-500">Labels</p> <p className="text-sm text-brand-secondary">Labels</p>
</div> </div>
<div> <div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.users?.length}</h4> <h4 className="mb-2 text-lg font-semibold">{projectInfo?.users?.length}</h4>
<p className="text-sm text-gray-500">Users</p> <p className="text-sm text-brand-secondary">Users</p>
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="text-lg font-semibold">Import Epics</h3> <h3 className="font-semibold">Import Epics</h3>
<p className="text-sm text-gray-500">Import epics as modules</p> <p className="text-sm text-brand-secondary">Import epics as modules</p>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Controller <Controller

View File

@ -106,7 +106,7 @@ export const JiraImporterRoot = () => {
return ( return (
<div className="flex h-full flex-col space-y-2"> <div className="flex h-full flex-col space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}> <Link href={`/${workspaceSlug}/settings/import-export`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900"> <div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-brand-secondary hover:text-brand-base">
<div> <div>
<ArrowLeftIcon className="h-3 w-3" /> <ArrowLeftIcon className="h-3 w-3" />
</div> </div>
@ -114,7 +114,7 @@ export const JiraImporterRoot = () => {
</div> </div>
</Link> </Link>
<div className="flex h-full flex-col space-y-4 rounded-[10px] border border-gray-200 bg-white p-4"> <div className="flex h-full flex-col space-y-4 rounded-[10px] border border-brand-base bg-brand-base p-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-10 w-10 flex-shrink-0"> <div className="h-10 w-10 flex-shrink-0">
<Image src={JiraLogo} alt="jira logo" /> <Image src={JiraLogo} alt="jira logo" />
@ -131,14 +131,14 @@ export const JiraImporterRoot = () => {
index > activeIntegrationState() + 1 || index > activeIntegrationState() + 1 ||
Boolean(index === activeIntegrationState() + 1 && disableTopBarAfter) Boolean(index === activeIntegrationState() + 1 && disableTopBarAfter)
} }
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${ className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border border-brand-base ${
index <= activeIntegrationState() index <= activeIntegrationState()
? `border-[#3F76FF] bg-[#3F76FF] text-white ${ ? `border-brand-accent bg-brand-accent ${
index === activeIntegrationState() index === activeIntegrationState()
? "border-opacity-100 bg-opacity-100" ? "border-opacity-100 bg-opacity-100"
: "border-opacity-80 bg-opacity-80" : "border-opacity-80 bg-opacity-80"
}` }`
: "border-gray-300" : "border-brand-base"
}`} }`}
> >
<integration.icon <integration.icon
@ -151,7 +151,9 @@ export const JiraImporterRoot = () => {
<div <div
key={index} key={index}
className={`border-b px-7 ${ className={`border-b px-7 ${
index <= activeIntegrationState() - 1 ? `border-[#3F76FF]` : `border-gray-300` index <= activeIntegrationState() - 1
? `border-brand-accent`
: `border-brand-base`
}`} }`}
> >
{" "} {" "}
@ -177,7 +179,7 @@ export const JiraImporterRoot = () => {
{currentStep?.state === "import-confirmation" && <JiraConfirmImport />} {currentStep?.state === "import-confirmation" && <JiraConfirmImport />}
</div> </div>
<div className="-mx-4 mt-4 flex justify-end gap-4 border-t p-4 pb-0"> <div className="-mx-4 mt-4 flex justify-end gap-4 border-t border-brand-base p-4 pb-0">
{currentStep?.state !== "import-configure" && ( {currentStep?.state !== "import-configure" && (
<SecondaryButton <SecondaryButton
onClick={() => { onClick={() => {

View File

@ -18,28 +18,28 @@ const importersList: { [key: string]: string } = {
}; };
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => ( export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
<div className="py-3 flex justify-between items-center gap-2"> <div className="flex items-center justify-between gap-2 py-3">
<div> <div>
<h4 className="text-sm flex items-center gap-2"> <h4 className="flex items-center gap-2 text-sm">
<span> <span>
Import from <span className="font-medium">{importersList[service.service]}</span> to{" "} Import from <span className="font-medium">{importersList[service.service]}</span> to{" "}
<span className="font-medium">{service.project_detail.name}</span> <span className="font-medium">{service.project_detail.name}</span>
</span> </span>
<span <span
className={`capitalize px-2 py-0.5 text-xs rounded ${ className={`rounded px-2 py-0.5 text-xs capitalize ${
service.status === "completed" service.status === "completed"
? "bg-green-100 text-green-500" ? "bg-green-500/20 text-green-500"
: service.status === "processing" : service.status === "processing"
? "bg-yellow-100 text-yellow-500" ? "bg-yellow-500/20 text-yellow-500"
: service.status === "failed" : service.status === "failed"
? "bg-red-100 text-red-500" ? "bg-red-500/20 text-red-500"
: "" : ""
}`} }`}
> >
{refreshing ? "Refreshing..." : service.status} {refreshing ? "Refreshing..." : service.status}
</span> </span>
</h4> </h4>
<div className="text-gray-500 text-xs mt-2 flex items-center gap-2"> <div className="mt-2 flex items-center gap-2 text-xs text-brand-secondary">
<span>{renderShortDateWithYearFormat(service.created_at)}</span>| <span>{renderShortDateWithYearFormat(service.created_at)}</span>|
<span> <span>
Imported by{" "} Imported by{" "}

View File

@ -99,7 +99,7 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
); );
return ( return (
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-brand-base bg-brand-surface-2 p-5"> <div className="flex items-center justify-between gap-2 rounded-[10px] border border-brand-base bg-brand-base p-5">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="h-12 w-12 flex-shrink-0"> <div className="h-12 w-12 flex-shrink-0">
<Image <Image

View File

@ -5,14 +5,13 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// services // services
import appinstallationsService from "services/app-installations.service"; import appinstallationsService from "services/app-installations.service";
// ui // ui
import { Loader } from "components/ui"; import { Loader } from "components/ui";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useIntegrationPopup from "hooks/use-integration-popup"; import useIntegrationPopup from "hooks/use-integration-popup";
// types // types
import { IWorkspaceIntegration } from "types"; import { IWorkspaceIntegration, ISlackIntegration } from "types";
// fetch-keys // fetch-keys
import { SLACK_CHANNEL_INFO } from "constants/fetch-keys"; import { SLACK_CHANNEL_INFO } from "constants/fetch-keys";
@ -21,7 +20,9 @@ type Props = {
}; };
export const SelectChannel: React.FC<Props> = ({ integration }) => { export const SelectChannel: React.FC<Props> = ({ integration }) => {
const [deletingProjectSync, setDeletingProjectSync] = useState(false); const [slackChannelAvailabilityToggle, setSlackChannelAvailabilityToggle] =
useState<boolean>(false);
const [slackChannel, setSlackChannel] = useState<ISlackIntegration | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -43,33 +44,38 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
); );
useEffect(() => { useEffect(() => {
if (projectIntegration?.length > 0) { if (projectId && projectIntegration && projectIntegration.length > 0) {
setDeletingProjectSync(true); const projectSlackIntegrationCheck: ISlackIntegration | undefined = projectIntegration.find(
(_slack: ISlackIntegration) => _slack.project === projectId
);
if (projectSlackIntegrationCheck) {
setSlackChannel(() => projectSlackIntegrationCheck);
setSlackChannelAvailabilityToggle(true);
}
} }
if (projectIntegration?.length === 0) { }, [projectIntegration, projectId]);
setDeletingProjectSync(false);
}
}, [projectIntegration]);
const handleDelete = async () => { const handleDelete = async () => {
if (projectIntegration.length === 0) return; if (projectIntegration.length === 0) return;
mutate(SLACK_CHANNEL_INFO, (prevData: any) => { mutate(SLACK_CHANNEL_INFO, (prevData: any) => {
if (!prevData) return; if (!prevData) return;
return prevData.id !== integration.id; return prevData.id !== integration.id;
}).then(() => setDeletingProjectSync(false)); }).then(() => {
setSlackChannelAvailabilityToggle(false);
setSlackChannel(null);
});
appinstallationsService appinstallationsService
.removeSlackChannel( .removeSlackChannel(
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
integration.id as string, integration.id as string,
projectIntegration?.[0]?.id slackChannel?.id
) )
.catch((err) => console.log(err)); .catch((err) => console.log(err));
}; };
const handleAuth = async () => { const handleAuth = async () => {
await startAuth(); await startAuth();
setDeletingProjectSync(true);
}; };
return ( return (
@ -78,20 +84,18 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
<button <button
type="button" type="button"
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${ className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
projectIntegration.length > 0 && deletingProjectSync ? "bg-green-500" : "bg-gray-200" slackChannelAvailabilityToggle ? "bg-green-500" : "bg-gray-200"
}`} }`}
role="switch" role="switch"
aria-checked aria-checked
onClick={() => { onClick={() => {
deletingProjectSync ? handleDelete() : handleAuth(); slackChannelAvailabilityToggle ? handleDelete() : handleAuth();
}} }}
> >
<span <span
aria-hidden="true" aria-hidden="true"
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${ className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
projectIntegration.length > 0 && deletingProjectSync slackChannelAvailabilityToggle ? "translate-x-5" : "translate-x-0"
? "translate-x-5"
: "translate-x-0"
}`} }`}
/> />
</button> </button>

View File

@ -110,9 +110,7 @@ const activityDetails: {
}, },
}; };
type Props = {}; export const IssueActivitySection: React.FC = () => {
export const IssueActivitySection: React.FC<Props> = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;

View File

@ -68,7 +68,10 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
)} )}
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px"> <span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
<ChatBubbleLeftEllipsisIcon className="h-3.5 w-3.5 text-gray-400" aria-hidden="true" /> <ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-brand-secondary"
aria-hidden="true"
/>
</span> </span>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@ -77,7 +80,9 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
{comment.actor_detail.first_name} {comment.actor_detail.first_name}
{comment.actor_detail.is_bot ? "Bot" : " " + comment.actor_detail.last_name} {comment.actor_detail.is_bot ? "Bot" : " " + comment.actor_detail.last_name}
</div> </div>
<p className="mt-0.5 text-xs text-brand-secondary">Commented {timeAgo(comment.created_at)}</p> <p className="mt-0.5 text-xs text-brand-secondary">
Commented {timeAgo(comment.created_at)}
</p>
</div> </div>
<div className="issue-comments-section p-0"> <div className="issue-comments-section p-0">
{isEditing ? ( {isEditing ? (
@ -94,13 +99,13 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="group rounded border border-green-500 bg-green-100 p-2 shadow-md duration-300 hover:bg-green-500" className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
> >
<CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" /> <CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
</button> </button>
<button <button
type="button" type="button"
className="group rounded border border-red-500 bg-red-100 p-2 shadow-md duration-300 hover:bg-red-500" className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
onClick={() => setIsEditing(false)} onClick={() => setIsEditing(false)}
> >
<XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" /> <XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
@ -108,16 +113,11 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
</div> </div>
</form> </form>
) : ( ) : (
// <div
// className="mt-2 mb-6 text-sm text-gray-700"
// dangerouslySetInnerHTML={{ __html: comment.comment_html }}
// />
<RemirrorRichTextEditor <RemirrorRichTextEditor
value={comment.comment_html} value={comment.comment_html}
editable={false} editable={false}
onBlur={() => ({})}
noBorder noBorder
customClassName="text-xs bg-brand-surface-1" customClassName="text-xs border border-brand-base bg-brand-base"
/> />
)} )}
</div> </div>

View File

@ -88,7 +88,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-10 overflow-y-auto">
@ -102,10 +102,10 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6"> <div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-100 p-4"> <span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon <ExclamationTriangleIcon
className="h-6 w-6 text-red-600" className="h-6 w-6 text-red-600"
aria-hidden="true" aria-hidden="true"
@ -116,9 +116,9 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
</span> </span>
</div> </div>
<span> <span>
<p className="break-all text-sm leading-7 text-brand-secondary"> <p className="text-sm text-brand-secondary">
Are you sure you want to delete issue{" "} Are you sure you want to delete issue{" "}
<span className="break-all font-semibold"> <span className="break-all font-medium text-brand-base">
{data?.project_detail.identifier}-{data?.sequence_id} {data?.project_detail.identifier}-{data?.sequence_id}
</span> </span>
{""}? All of the data related to the issue will be permanently removed. This {""}? All of the data related to the issue will be permanently removed. This

View File

@ -26,7 +26,14 @@ import { CreateStateModal } from "components/states";
import { CreateUpdateCycleModal } from "components/cycles"; import { CreateUpdateCycleModal } from "components/cycles";
import { CreateLabelModal } from "components/labels"; import { CreateLabelModal } from "components/labels";
// ui // ui
import { CustomMenu, Input, Loader, PrimaryButton, SecondaryButton } from "components/ui"; import {
CustomMenu,
Input,
Loader,
PrimaryButton,
SecondaryButton,
ToggleSwitch,
} from "components/ui";
// icons // icons
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// helpers // helpers
@ -221,7 +228,7 @@ export const IssueForm: FC<IssueFormProps> = ({
</h3> </h3>
</div> </div>
{watch("parent") && watch("parent") !== "" ? ( {watch("parent") && watch("parent") !== "" ? (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-brand-surface-1 p-2 text-xs"> <div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-brand-surface-2 p-2 text-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className="block h-1.5 w-1.5 rounded-full" className="block h-1.5 w-1.5 rounded-full"
@ -230,7 +237,7 @@ export const IssueForm: FC<IssueFormProps> = ({
.color, .color,
}} }}
/> />
<span className="flex-shrink-0 text-gray-600"> <span className="flex-shrink-0 text-brand-secondary">
{/* {projects?.find((p) => p.id === projectId)?.identifier}- */} {/* {projects?.find((p) => p.id === projectId)?.identifier}- */}
{issues.find((i) => i.id === watch("parent"))?.sequence_id} {issues.find((i) => i.id === watch("parent"))?.sequence_id}
</span> </span>
@ -253,7 +260,6 @@ export const IssueForm: FC<IssueFormProps> = ({
onChange={handleTitleChange} onChange={handleTitleChange}
className="resize-none text-xl" className="resize-none text-xl"
placeholder="Title" placeholder="Title"
mode="transparent"
autoComplete="off" autoComplete="off"
error={errors.name} error={errors.name}
register={register} register={register}
@ -294,7 +300,7 @@ export const IssueForm: FC<IssueFormProps> = ({
)} )}
</div> </div>
<div className="relative"> <div className="relative">
<div className="flex justify-end -mb-2"> <div className="-mb-2 flex justify-end">
{issueName && issueName !== "" && ( {issueName && issueName !== "" && (
<button <button
type="button" type="button"
@ -450,22 +456,7 @@ export const IssueForm: FC<IssueFormProps> = ({
onClick={() => setCreateMore((prevData) => !prevData)} onClick={() => setCreateMore((prevData) => !prevData)}
> >
<span className="text-xs">Create more</span> <span className="text-xs">Create more</span>
<button <ToggleSwitch value={createMore} onChange={() => {}} size="md" />
type="button"
className={`pointer-events-none relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent ${
createMore ? "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">Create more</span>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-3 w-3 ${
createMore ? "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 className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton> <SecondaryButton onClick={handleClose}>Discard</SecondaryButton>

View File

@ -219,11 +219,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20"> <div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child <Transition.Child
as={React.Fragment} as={React.Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -233,7 +233,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg bg-brand-surface-1 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl"> <Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm <IssueForm
issues={issues ?? []} issues={issues ?? []}
handleFormSubmit={handleFormSubmit} handleFormSubmit={handleFormSubmit}

View File

@ -58,7 +58,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" /> <div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20"> <div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
@ -71,7 +71,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-brand-surface-2 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">
{multiple ? ( {multiple ? (
<> <>
<Combobox value={value} onChange={() => ({})} multiple> <Combobox value={value} onChange={() => ({})} multiple>
@ -95,19 +95,17 @@ export const ParentIssuesListModal: React.FC<Props> = ({
{filteredIssues.length > 0 && ( {filteredIssues.length > 0 && (
<li className="p-2"> <li className="p-2">
{query === "" && ( {query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base"> <h2 className="mt-4 mb-2 px-3 text-xs font-medium">{title}</h2>
{title}
</h2>
)} )}
<ul className="text-sm text-gray-700"> <ul className="text-sm">
{filteredIssues.map((issue) => ( {filteredIssues.map((issue) => (
<Combobox.Option <Combobox.Option
key={issue.id} key={issue.id}
value={issue.id} value={issue.id}
className={({ active }) => className={({ active, selected }) =>
`flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${ `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
active ? "bg-gray-900 bg-opacity-5 text-brand-base" : "" active ? "bg-brand-surface-2 text-brand-base" : ""
}` } ${selected ? "text-brand-base" : ""}`
} }
> >
{({ selected }) => ( {({ selected }) => (
@ -119,7 +117,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
backgroundColor: issue.state_detail.color, backgroundColor: issue.state_detail.color,
}} }}
/> />
<span className="flex-shrink-0 text-xs text-brand-secondary"> <span className="flex-shrink-0 text-xs">
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span>{" "} </span>{" "}
{issue.id} {issue.id}
@ -164,28 +162,23 @@ export const ParentIssuesListModal: React.FC<Props> = ({
/> />
</div> </div>
{customDisplay && <div className="p-3">{customDisplay}</div>} {customDisplay && <div className="p-3">{customDisplay}</div>}
<Combobox.Options <Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{filteredIssues.length > 0 ? ( {filteredIssues.length > 0 ? (
<li className="p-2"> <li className="p-2">
{query === "" && ( {query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base"> <h2 className="mt-4 mb-2 px-3 text-xs font-medium">{title}</h2>
{title}
</h2>
)} )}
<ul className="text-sm text-gray-700"> <ul className="text-sm">
{filteredIssues.map((issue) => ( {filteredIssues.map((issue) => (
<Combobox.Option <Combobox.Option
key={issue.id} key={issue.id}
value={issue.id} value={issue.id}
className={({ active }) => className={({ active, selected }) =>
`flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${ `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
active ? "bg-gray-900 bg-opacity-5 text-brand-base" : "" active ? "bg-brand-surface-2 text-brand-base" : ""
}` } ${selected ? "text-brand-base" : ""}`
} }
onClick={() => handleClose()} onClick={handleClose}
> >
<> <>
<span <span
@ -194,7 +187,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
backgroundColor: issue.state_detail.color, backgroundColor: issue.state_detail.color,
}} }}
/> />
<span className="flex-shrink-0 text-xs text-brand-secondary"> <span className="flex-shrink-0 text-xs">
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span>{" "} </span>{" "}
{issue.name} {issue.name}
@ -205,10 +198,13 @@ export const ParentIssuesListModal: React.FC<Props> = ({
</li> </li>
) : ( ) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center"> <div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" /> <LayerDiagonalIcon height="52" width="52" />
<h3 className="text-brand-secondary"> <h3 className="text-brand-secondary">
No issues found. Create a new issue with{" "} No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>. <pre className="inline rounded bg-brand-surface-2 px-2 py-1 text-sm">
C
</pre>
.
</h3> </h3>
</div> </div>
)} )}

View File

@ -16,28 +16,19 @@ export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
<Popover className="relative flex items-center justify-center rounded-lg"> <Popover className="relative flex items-center justify-center rounded-lg">
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button className="flex cursor-pointer items-center rounded-md border border-brand-base text-xs shadow-sm duration-200">
className={({ open }) => <span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-brand-secondary">
`flex cursor-pointer items-center rounded-md border border-brand-base text-xs shadow-sm duration-200
${
open
? "border-brand-accent bg-brand-accent/5 outline-none ring-1 ring-brand-accent "
: "hover:bg-brand-accent/5 "
}`
}
>
<span className="flex items-center justify-center gap-2 px-3 py-1.5 text-xs">
{value ? ( {value ? (
<> <>
<span className="text-gray-600">{value}</span> <span className="text-brand-base">{value}</span>
<button onClick={() => onChange(null)}> <button onClick={() => onChange(null)}>
<XMarkIcon className="h-3 w-3 text-gray-600" /> <XMarkIcon className="h-3 w-3" />
</button> </button>
</> </>
) : ( ) : (
<> <>
<CalendarDaysIcon className="h-4 w-4 text-gray-500 flex-shrink-0 " /> <CalendarDaysIcon className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-gray-500">Due Date</span> <span>Due Date</span>
</> </>
)} )}
</span> </span>

View File

@ -22,8 +22,8 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
value={value} value={value}
label={ label={
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<PlayIcon className="h-4 w-4 text-gray-500 -rotate-90" /> <PlayIcon className={`h-3.5 w-3.5 -rotate-90 ${value !== null ? "text-brand-base" : "text-brand-secondary"}`} />
<span className={`${value ? "text-gray-600" : "text-gray-500"}`}> <span className={value !== null ? "text-brand-base" : "text-brand-secondary"}>
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"} {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
</span> </span>
</div> </div>

View File

@ -58,16 +58,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
> >
{({ open }: any) => ( {({ open }: any) => (
<> <>
<Combobox.Button <Combobox.Button className="flex cursor-pointer items-center rounded-md border border-brand-base text-xs shadow-sm duration-200 hover:bg-brand-surface-2">
className={({ open }) =>
`flex cursor-pointer items-center rounded-md border border-brand-base text-xs shadow-sm duration-200
${
open
? "border-brand-accent bg-brand-accent/5 outline-none ring-1 ring-brand-accent "
: "hover:bg-brand-accent/5 "
}`
}
>
{value && value.length > 0 ? ( {value && value.length > 0 ? (
<span className="flex items-center justify-center gap-2 px-3 py-1 text-xs"> <span className="flex items-center justify-center gap-2 px-3 py-1 text-xs">
<IssueLabelsList <IssueLabelsList
@ -77,8 +68,8 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
/> />
</span> </span>
) : ( ) : (
<span className="flex items-center justify-center gap-2 px-3 py-1.5 text-xs"> <span className="flex items-center justify-center gap-2 px-2.5 py-1 text-xs">
<TagIcon className="h-3 w-3 text-brand-secondary" /> <TagIcon className="h-3.5 w-3.5 text-brand-secondary" />
<span className=" text-brand-secondary">Label</span> <span className=" text-brand-secondary">Label</span>
</span> </span>
)} )}
@ -96,12 +87,12 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
> >
<Combobox.Options <Combobox.Options
className={`absolute z-10 mt-1 max-h-52 min-w-[8rem] overflow-auto rounded-md border-none className={`absolute z-10 mt-1 max-h-52 min-w-[8rem] overflow-auto rounded-md border-none
bg-brand-surface-2 px-2 py-2 text-xs shadow-md focus:outline-none`} bg-brand-surface-1 px-2 py-2 text-xs shadow-md focus:outline-none`}
> >
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] bg-brand-surface-1 px-2"> <div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-brand-base bg-brand-surface-1 px-2">
<MagnifyingGlassIcon className="h-3 w-3 text-brand-secondary" /> <MagnifyingGlassIcon className="h-3 w-3 text-brand-secondary" />
<Combobox.Input <Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none" className="w-full bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none"
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
placeholder="Search for label..." placeholder="Search for label..."
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
@ -121,7 +112,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
className={({ active }) => className={({ active }) =>
`${ `${
active ? "bg-brand-surface-2" : "" active ? "bg-brand-surface-2" : ""
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-brand-secondary`
} }
value={label.id} value={label.id}
> >
@ -129,10 +120,9 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
<div className="flex w-full justify-between gap-2 rounded"> <div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<span <span
className="h-3 w-3 flex-shrink-0 rounded-full" className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: backgroundColor: label.color,
label.color && label.color !== "" ? label.color : "#000",
}} }}
/> />
<span>{label.name}</span> <span>{label.name}</span>
@ -150,8 +140,8 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
); );
} else } else
return ( return (
<div className="border-y border-brand-base bg-brand-surface-2"> <div className="border-y border-brand-base">
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-brand-base"> <div className="flex select-none items-center gap-2 truncate p-2 text-brand-base">
<RectangleGroupIcon className="h-3 w-3" /> {label.name} <RectangleGroupIcon className="h-3 w-3" /> {label.name}
</div> </div>
<div> <div>
@ -161,7 +151,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
className={({ active }) => className={({ active }) =>
`${ `${
active ? "bg-brand-surface-2" : "" active ? "bg-brand-surface-2" : ""
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-brand-secondary`
} }
value={child.id} value={child.id}
> >
@ -169,9 +159,9 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
<div className="flex w-full justify-between gap-2 rounded"> <div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<span <span
className="h-3 w-3 flex-shrink-0 rounded-full" className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: child?.color ?? "black", backgroundColor: child?.color,
}} }}
/> />
<span>{child.name}</span> <span>{child.name}</span>
@ -202,9 +192,9 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-brand-surface-2" className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-brand-surface-2"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
> >
<span className="flex items-center justify-start gap-1"> <span className="flex items-center justify-start gap-1 text-brand-secondary">
<PlusIcon className="h-4 w-4 text-gray-600" aria-hidden="true" /> <PlusIcon className="h-4 w-4" aria-hidden="true" />
<span className="text-gray-600">Create New Label</span> <span>Create New Label</span>
</span> </span>
</button> </button>
</div> </div>

View File

@ -20,7 +20,7 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
<span className="flex items-center"> <span className="flex items-center">
{getPriorityIcon(value, `text-xs ${value ? "" : "text-brand-secondary"}`)} {getPriorityIcon(value, `text-xs ${value ? "" : "text-brand-secondary"}`)}
</span> </span>
<span className={`${value ? "text-gray-600" : "text-brand-secondary"} capitalize`}> <span className={`${value ? "" : "text-brand-secondary"} capitalize`}>
{value ?? "Priority"} {value ?? "Priority"}
</span> </span>
</div> </div>

View File

@ -14,7 +14,7 @@ import { getStateGroupIcon } from "components/icons";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// fetch keys // fetch keys
import { STATE_LIST } from "constants/fetch-keys"; import { STATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -29,7 +29,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId) : null, workspaceSlug && projectId ? STATES_LIST(projectId) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId) ? () => stateService.getStates(workspaceSlug as string, projectId)
: null : null
@ -56,15 +56,17 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
onChange={onChange} onChange={onChange}
options={options} options={options}
label={ label={
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2">
{selectedOption ? ( {selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color) getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
) : currentDefaultState ? ( ) : currentDefaultState ? (
getStateGroupIcon(currentDefaultState.group, "16", "16", currentDefaultState.color) getStateGroupIcon(currentDefaultState.group, "16", "16", currentDefaultState.color)
) : ( ) : (
<Squares2X2Icon className="h-4 w-4" /> <Squares2X2Icon className="h-3.5 w-3.5 text-brand-secondary" />
)} )}
{selectedOption?.name ? selectedOption.name : currentDefaultState?.name ?? "State"} {selectedOption?.name
? selectedOption.name
: currentDefaultState?.name ?? <span className="text-brand-secondary">State</span>}
</div> </div>
} }
footerOption={ footerOption={

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