mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge pull request #993 from makeplane/stage-release
promote: stage-release to prod
This commit is contained in:
commit
563bb12b9b
@ -3,15 +3,19 @@ DJANGO_SETTINGS_MODULE="plane.settings.production"
|
|||||||
DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane
|
DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane
|
||||||
# Cache
|
# Cache
|
||||||
REDIS_URL=redis://redis:6379/
|
REDIS_URL=redis://redis:6379/
|
||||||
# SMPT
|
# SMTP
|
||||||
EMAIL_HOST=""
|
EMAIL_HOST=""
|
||||||
EMAIL_HOST_USER=""
|
EMAIL_HOST_USER=""
|
||||||
EMAIL_HOST_PASSWORD=""
|
EMAIL_HOST_PASSWORD=""
|
||||||
|
EMAIL_PORT="587"
|
||||||
|
EMAIL_USE_TLS="1"
|
||||||
|
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||||
# AWS
|
# AWS
|
||||||
AWS_REGION=""
|
AWS_REGION=""
|
||||||
AWS_ACCESS_KEY_ID=""
|
AWS_ACCESS_KEY_ID=""
|
||||||
AWS_SECRET_ACCESS_KEY=""
|
AWS_SECRET_ACCESS_KEY=""
|
||||||
AWS_S3_BUCKET_NAME=""
|
AWS_S3_BUCKET_NAME=""
|
||||||
|
AWS_S3_ENDPOINT_URL=""
|
||||||
# FE
|
# FE
|
||||||
WEB_URL="localhost/"
|
WEB_URL="localhost/"
|
||||||
# OAUTH
|
# OAUTH
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
]
|
||||||
|
@ -5,3 +5,4 @@ from .github import (
|
|||||||
GithubIssueSyncSerializer,
|
GithubIssueSyncSerializer,
|
||||||
GithubCommentSyncSerializer,
|
GithubCommentSyncSerializer,
|
||||||
)
|
)
|
||||||
|
from .slack import SlackProjectSyncSerializer
|
14
apiserver/plane/api/serializers/integration/slack.py
Normal file
14
apiserver/plane/api/serializers/integration/slack.py
Normal 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",
|
||||||
|
]
|
@ -81,8 +81,6 @@ from plane.api.views import (
|
|||||||
StateViewSet,
|
StateViewSet,
|
||||||
## End States
|
## End States
|
||||||
# Estimates
|
# Estimates
|
||||||
EstimateViewSet,
|
|
||||||
EstimatePointViewSet,
|
|
||||||
ProjectEstimatePointEndpoint,
|
ProjectEstimatePointEndpoint,
|
||||||
BulkEstimatePointEndpoint,
|
BulkEstimatePointEndpoint,
|
||||||
## End Estimates
|
## End Estimates
|
||||||
@ -133,6 +131,7 @@ from plane.api.views import (
|
|||||||
GithubIssueSyncViewSet,
|
GithubIssueSyncViewSet,
|
||||||
GithubCommentSyncViewSet,
|
GithubCommentSyncViewSet,
|
||||||
BulkCreateGithubIssueSyncEndpoint,
|
BulkCreateGithubIssueSyncEndpoint,
|
||||||
|
SlackProjectSyncViewSet,
|
||||||
## End Integrations
|
## End Integrations
|
||||||
# Importer
|
# Importer
|
||||||
ServiceIssueImportSummaryEndpoint,
|
ServiceIssueImportSummaryEndpoint,
|
||||||
@ -146,6 +145,9 @@ from plane.api.views import (
|
|||||||
# Gpt
|
# Gpt
|
||||||
GPTIntegrationEndpoint,
|
GPTIntegrationEndpoint,
|
||||||
## End Gpt
|
## End Gpt
|
||||||
|
# Release Notes
|
||||||
|
ReleaseNotesEndpoint,
|
||||||
|
## End Release Notes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -507,62 +509,34 @@ urlpatterns = [
|
|||||||
name="project-state",
|
name="project-state",
|
||||||
),
|
),
|
||||||
# End States ##
|
# End States ##
|
||||||
# States
|
# Estimates
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
|
||||||
EstimateViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "list",
|
|
||||||
"post": "create",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="project-estimates",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:pk>/",
|
|
||||||
EstimateViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve",
|
|
||||||
"put": "update",
|
|
||||||
"patch": "partial_update",
|
|
||||||
"delete": "destroy",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="project-estimates",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
|
|
||||||
EstimatePointViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "list",
|
|
||||||
"post": "create",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="project-estimate-points",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<uuid:pk>/",
|
|
||||||
EstimatePointViewSet.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve",
|
|
||||||
"put": "update",
|
|
||||||
"patch": "partial_update",
|
|
||||||
"delete": "destroy",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="project-estimates",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
|
||||||
ProjectEstimatePointEndpoint.as_view(),
|
ProjectEstimatePointEndpoint.as_view(),
|
||||||
name="project-estimate-points",
|
name="project-estimate-points",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/bulk-estimate-points/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||||
BulkEstimatePointEndpoint.as_view(),
|
BulkEstimatePointEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
name="bulk-create-estimate-points",
|
name="bulk-create-estimate-points",
|
||||||
),
|
),
|
||||||
# End States ##
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
|
||||||
|
BulkEstimatePointEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="bulk-create-estimate-points",
|
||||||
|
),
|
||||||
|
# End Estimates ##
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
|
||||||
@ -1237,6 +1211,26 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
## End Github Integrations
|
## End Github Integrations
|
||||||
|
# Slack Integration
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
|
||||||
|
SlackProjectSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
|
||||||
|
SlackProjectSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
"get": "retrieve",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
## End Slack Integration
|
||||||
## End Integrations
|
## End Integrations
|
||||||
# Importer
|
# Importer
|
||||||
path(
|
path(
|
||||||
@ -1284,4 +1278,11 @@ urlpatterns = [
|
|||||||
name="importer",
|
name="importer",
|
||||||
),
|
),
|
||||||
## End Gpt
|
## End Gpt
|
||||||
|
# Release Notes
|
||||||
|
path(
|
||||||
|
"release-notes/",
|
||||||
|
ReleaseNotesEndpoint.as_view(),
|
||||||
|
name="release-notes",
|
||||||
|
),
|
||||||
|
## End Release Notes
|
||||||
]
|
]
|
||||||
|
@ -106,6 +106,7 @@ from .integration import (
|
|||||||
GithubCommentSyncViewSet,
|
GithubCommentSyncViewSet,
|
||||||
GithubRepositoriesEndpoint,
|
GithubRepositoriesEndpoint,
|
||||||
BulkCreateGithubIssueSyncEndpoint,
|
BulkCreateGithubIssueSyncEndpoint,
|
||||||
|
SlackProjectSyncViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .importer import (
|
from .importer import (
|
||||||
@ -133,8 +134,9 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
|||||||
from .gpt import GPTIntegrationEndpoint
|
from .gpt import GPTIntegrationEndpoint
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
EstimateViewSet,
|
|
||||||
EstimatePointViewSet,
|
|
||||||
ProjectEstimatePointEndpoint,
|
ProjectEstimatePointEndpoint,
|
||||||
BulkEstimatePointEndpoint,
|
BulkEstimatePointEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from .release import ReleaseNotesEndpoint
|
||||||
|
@ -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):
|
||||||
|
@ -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)),
|
||||||
|
@ -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,15 +42,33 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BulkEstimatePointEndpoint(BaseAPIView):
|
class BulkEstimatePointEndpoint(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
model = Estimate
|
||||||
|
serializer_class = EstimateSerializer
|
||||||
|
|
||||||
def post(self, request, slug, project_id, estimate_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
estimate = Estimate.objects.get(
|
estimates = Estimate.objects.filter(
|
||||||
pk=estimate_id, workspace__slug=slug, project=project_id
|
workspace__slug=slug, project_id=project_id
|
||||||
|
).prefetch_related("points")
|
||||||
|
serializer = EstimateReadSerializer(estimates, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
if not request.data.get("estimate", False):
|
||||||
|
return Response(
|
||||||
|
{"error": "Estimate is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
estimate_points = request.data.get("estimate_points", [])
|
estimate_points = request.data.get("estimate_points", [])
|
||||||
@ -160,6 +79,18 @@ class BulkEstimatePointEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
|
||||||
|
if not estimate_serializer.is_valid():
|
||||||
|
return Response(
|
||||||
|
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
estimate = estimate_serializer.save(project_id=project_id)
|
||||||
|
except IntegrityError:
|
||||||
|
return Response(
|
||||||
|
{"errror": "Estimate with the name already exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
estimate_points = EstimatePoint.objects.bulk_create(
|
estimate_points = EstimatePoint.objects.bulk_create(
|
||||||
[
|
[
|
||||||
EstimatePoint(
|
EstimatePoint(
|
||||||
@ -178,9 +109,17 @@ class BulkEstimatePointEndpoint(BaseAPIView):
|
|||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = EstimatePointSerializer(estimate_points, many=True)
|
estimate_point_serializer = EstimatePointSerializer(
|
||||||
|
estimate_points, many=True
|
||||||
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{
|
||||||
|
"estimate": estimate_serializer.data,
|
||||||
|
"estimate_points": estimate_point_serializer.data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
except Estimate.DoesNotExist:
|
except Estimate.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Estimate does not exist"},
|
{"error": "Estimate does not exist"},
|
||||||
@ -193,14 +132,58 @@ class BulkEstimatePointEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, estimate_id):
|
def retrieve(self, request, slug, project_id, estimate_id):
|
||||||
try:
|
try:
|
||||||
|
estimate = Estimate.objects.get(
|
||||||
|
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
serializer = EstimateReadSerializer(estimate)
|
||||||
|
return Response(
|
||||||
|
serializer.data,
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
except Estimate.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, estimate_id):
|
||||||
|
try:
|
||||||
|
if not request.data.get("estimate", False):
|
||||||
|
return Response(
|
||||||
|
{"error": "Estimate is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if not len(request.data.get("estimate_points", [])):
|
if not len(request.data.get("estimate_points", [])):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Estimate points are required"},
|
{"error": "Estimate points are required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
estimate = Estimate.objects.get(pk=estimate_id)
|
||||||
|
|
||||||
|
estimate_serializer = EstimateSerializer(
|
||||||
|
estimate, data=request.data.get("estimate"), partial=True
|
||||||
|
)
|
||||||
|
if not estimate_serializer.is_valid():
|
||||||
|
return Response(
|
||||||
|
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
estimate = estimate_serializer.save()
|
||||||
|
except IntegrityError:
|
||||||
|
return Response(
|
||||||
|
{"errror": "Estimate with the name already exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
estimate_points_data = request.data.get("estimate_points", [])
|
estimate_points_data = request.data.get("estimate_points", [])
|
||||||
|
|
||||||
estimate_points = EstimatePoint.objects.filter(
|
estimate_points = EstimatePoint.objects.filter(
|
||||||
@ -212,7 +195,6 @@ class BulkEstimatePointEndpoint(BaseAPIView):
|
|||||||
estimate_id=estimate_id,
|
estimate_id=estimate_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(estimate_points)
|
|
||||||
updated_estimate_points = []
|
updated_estimate_points = []
|
||||||
for estimate_point in estimate_points:
|
for estimate_point in estimate_points:
|
||||||
# Find the data for that estimate point
|
# Find the data for that estimate point
|
||||||
@ -221,24 +203,50 @@ class BulkEstimatePointEndpoint(BaseAPIView):
|
|||||||
for point in estimate_points_data
|
for point in estimate_points_data
|
||||||
if point.get("id") == str(estimate_point.id)
|
if point.get("id") == str(estimate_point.id)
|
||||||
]
|
]
|
||||||
print(estimate_point_data)
|
|
||||||
if len(estimate_point_data):
|
if len(estimate_point_data):
|
||||||
estimate_point.value = estimate_point_data[0].get(
|
estimate_point.value = estimate_point_data[0].get(
|
||||||
"value", estimate_point.value
|
"value", estimate_point.value
|
||||||
)
|
)
|
||||||
updated_estimate_points.append(estimate_point)
|
updated_estimate_points.append(estimate_point)
|
||||||
|
|
||||||
|
try:
|
||||||
EstimatePoint.objects.bulk_update(
|
EstimatePoint.objects.bulk_update(
|
||||||
updated_estimate_points, ["value"], batch_size=10
|
updated_estimate_points, ["value"], batch_size=10
|
||||||
)
|
)
|
||||||
serializer = EstimatePointSerializer(estimate_points, many=True)
|
except IntegrityError as e:
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
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,
|
||||||
|
)
|
||||||
except Estimate.DoesNotExist:
|
except Estimate.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, estimate_id):
|
||||||
|
try:
|
||||||
|
estimate = Estimate.objects.get(
|
||||||
|
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
estimate.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
@ -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,11 +520,16 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
|||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
modules = Module.objects.filter(id__in=[module.id for module in modules])
|
||||||
|
|
||||||
|
if len(modules) == len(modules_data):
|
||||||
_ = ModuleLink.objects.bulk_create(
|
_ = ModuleLink.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ModuleLink(
|
ModuleLink(
|
||||||
module=module,
|
module=module,
|
||||||
url=module_data.get("link", {}).get("url", "https://plane.so"),
|
url=module_data.get("link", {}).get(
|
||||||
|
"url", "https://plane.so"
|
||||||
|
),
|
||||||
title=module_data.get("link", {}).get(
|
title=module_data.get("link", {}).get(
|
||||||
"title", "Original Issue"
|
"title", "Original Issue"
|
||||||
),
|
),
|
||||||
@ -529,6 +567,12 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
|||||||
return Response(
|
return Response(
|
||||||
{"modules": serializer.data}, status=status.HTTP_201_CREATED
|
{"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
|
||||||
|
@ -6,3 +6,4 @@ from .github import (
|
|||||||
GithubCommentSyncViewSet,
|
GithubCommentSyncViewSet,
|
||||||
GithubRepositoriesEndpoint,
|
GithubRepositoriesEndpoint,
|
||||||
)
|
)
|
||||||
|
from .slack import SlackProjectSyncViewSet
|
||||||
|
@ -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:
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
integration = Integration.objects.get(provider=provider)
|
||||||
|
config = {}
|
||||||
|
if provider == "github":
|
||||||
installation_id = request.data.get("installation_id", None)
|
installation_id = request.data.get("installation_id", None)
|
||||||
|
|
||||||
if not installation_id:
|
if not installation_id:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Installation ID is required"},
|
{"error": "Installation ID is required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
|
||||||
integration = Integration.objects.get(provider=provider)
|
|
||||||
config = {}
|
|
||||||
if provider == "github":
|
|
||||||
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",
|
||||||
|
59
apiserver/plane/api/views/integration/slack.py
Normal file
59
apiserver/plane/api/views/integration/slack.py
Normal 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,
|
||||||
|
)
|
@ -1,13 +1,14 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from itertools import groupby, chain
|
from itertools import chain
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db.models import Prefetch, OuterRef, Func, F, Q
|
from django.db.models import Prefetch, OuterRef, Func, F, Q, Count
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -46,6 +47,7 @@ from plane.db.models import (
|
|||||||
Label,
|
Label,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
State,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
@ -590,8 +592,31 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
.prefetch_related("labels")
|
.prefetch_related("labels")
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = IssueLiteSerializer(sub_issues, many=True)
|
state_distribution = (
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
State.objects.filter(workspace__slug=slug, project_id=project_id)
|
||||||
|
.annotate(
|
||||||
|
state_count=Count(
|
||||||
|
"state_issue",
|
||||||
|
filter=Q(state_issue__parent_id=issue_id),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("group")
|
||||||
|
.values("group", "state_count")
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {item["group"]: item["state_count"] for item in state_distribution}
|
||||||
|
|
||||||
|
serializer = IssueLiteSerializer(
|
||||||
|
sub_issues,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"sub_issues": serializer.data,
|
||||||
|
"state_distribution": result,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -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)),
|
||||||
|
@ -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."
|
||||||
|
@ -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,
|
||||||
|
21
apiserver/plane/api/views/release.py
Normal file
21
apiserver/plane/api/views/release.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Third party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import BaseAPIView
|
||||||
|
from plane.utils.integrations.github import get_release_notes
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseNotesEndpoint(BaseAPIView):
|
||||||
|
def get(self, request):
|
||||||
|
try:
|
||||||
|
release_notes = get_release_notes()
|
||||||
|
return Response(release_notes, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
@ -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,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -8,10 +11,10 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet
|
from . import BaseViewSet, BaseAPIView
|
||||||
from plane.api.serializers import StateSerializer
|
from plane.api.serializers import StateSerializer
|
||||||
from plane.api.permissions import ProjectEntityPermission
|
from plane.api.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import State
|
from plane.db.models import State, Issue
|
||||||
|
|
||||||
|
|
||||||
class StateViewSet(BaseViewSet):
|
class StateViewSet(BaseViewSet):
|
||||||
@ -36,6 +39,25 @@ class StateViewSet(BaseViewSet):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
serializer = StateSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(project_id=project_id)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except IntegrityError:
|
||||||
|
return Response(
|
||||||
|
{"error": "State with the name already exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
state_dict = dict()
|
state_dict = dict()
|
||||||
@ -66,6 +88,17 @@ class StateViewSet(BaseViewSet):
|
|||||||
{"error": "Default state cannot be deleted"}, status=False
|
{"error": "Default state cannot be deleted"}, status=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for any issues in the state
|
||||||
|
issue_exist = Issue.objects.filter(state=pk).exists()
|
||||||
|
|
||||||
|
if issue_exist:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "The state is not empty, only empty states can be deleted"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
state.delete()
|
state.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
except State.DoesNotExist:
|
except State.DoesNotExist:
|
||||||
|
@ -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,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@ -20,7 +21,7 @@ def email_verification(first_name, email, token, current_site):
|
|||||||
realtivelink = "/request-email-verification/" + "?token=" + str(token)
|
realtivelink = "/request-email-verification/" + "?token=" + str(token)
|
||||||
abs_url = "http://" + current_site + realtivelink
|
abs_url = "http://" + current_site + realtivelink
|
||||||
|
|
||||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"Verify your Email!"
|
subject = f"Verify your Email!"
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@ -18,7 +19,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
|||||||
realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/"
|
realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/"
|
||||||
abs_url = "http://" + current_site + realtivelink
|
abs_url = "http://" + current_site + realtivelink
|
||||||
|
|
||||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"Verify your Email!"
|
subject = f"Verify your Email!"
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ from plane.db.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from .workspace_invitation_task import workspace_invitation
|
from .workspace_invitation_task import workspace_invitation
|
||||||
|
from plane.bgtasks.user_welcome_task import send_welcome_email
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
@ -40,7 +41,7 @@ def service_importer(service, importer_id):
|
|||||||
|
|
||||||
# Check if we need to import users as well
|
# Check if we need to import users as well
|
||||||
if len(users):
|
if len(users):
|
||||||
# For all invited users create the uers
|
# For all invited users create the users
|
||||||
new_users = User.objects.bulk_create(
|
new_users = User.objects.bulk_create(
|
||||||
[
|
[
|
||||||
User(
|
User(
|
||||||
@ -56,6 +57,15 @@ def service_importer(service, importer_id):
|
|||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
send_welcome_email.delay(
|
||||||
|
str(user.id),
|
||||||
|
True,
|
||||||
|
f"{user.email} was imported to Plane from {service}",
|
||||||
|
)
|
||||||
|
for user in new_users
|
||||||
|
]
|
||||||
|
|
||||||
workspace_users = User.objects.filter(
|
workspace_users = User.objects.filter(
|
||||||
email__in=[
|
email__in=[
|
||||||
user.get("email").strip().lower()
|
user.get("email").strip().lower()
|
||||||
|
@ -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
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@ -14,7 +15,7 @@ def magic_link(email, key, token, current_site):
|
|||||||
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
|
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
|
||||||
abs_url = "http://" + current_site + realtivelink
|
abs_url = "http://" + current_site + realtivelink
|
||||||
|
|
||||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"Login for Plane"
|
subject = f"Login for Plane"
|
||||||
|
|
||||||
@ -29,6 +30,5 @@ def magic_link(email, key, token, current_site):
|
|||||||
msg.send()
|
msg.send()
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return
|
return
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@ -22,7 +23,7 @@ def project_invitation(email, project_id, token, current_site):
|
|||||||
relativelink = f"/project-member-invitation/{project_member_invite.id}"
|
relativelink = f"/project-member-invitation/{project_member_invite.id}"
|
||||||
abs_url = "http://" + current_site + relativelink
|
abs_url = "http://" + current_site + relativelink
|
||||||
|
|
||||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
|
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
|
||||||
|
|
||||||
@ -49,6 +50,5 @@ def project_invitation(email, project_id, token, current_site):
|
|||||||
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
|
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return
|
return
|
||||||
|
56
apiserver/plane/bgtasks/user_welcome_task.py
Normal file
56
apiserver/plane/bgtasks/user_welcome_task.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from celery import shared_task
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
from slack_sdk import WebClient
|
||||||
|
from slack_sdk.errors import SlackApiError
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_welcome_email(user_id, created, message):
|
||||||
|
try:
|
||||||
|
instance = User.objects.get(pk=user_id)
|
||||||
|
|
||||||
|
if created and not instance.is_bot:
|
||||||
|
first_name = instance.first_name.capitalize()
|
||||||
|
to_email = instance.email
|
||||||
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
|
subject = f"Welcome to Plane ✈️!"
|
||||||
|
|
||||||
|
context = {"first_name": first_name, "email": instance.email}
|
||||||
|
|
||||||
|
html_content = render_to_string(
|
||||||
|
"emails/auth/user_welcome_email.html", context
|
||||||
|
)
|
||||||
|
|
||||||
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject, text_content, from_email_string, [to_email]
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
# Send message on slack as well
|
||||||
|
if settings.SLACK_BOT_TOKEN:
|
||||||
|
client = WebClient(token=settings.SLACK_BOT_TOKEN)
|
||||||
|
try:
|
||||||
|
_ = client.chat_postMessage(
|
||||||
|
channel="#trackers",
|
||||||
|
text=message,
|
||||||
|
)
|
||||||
|
except SlackApiError as e:
|
||||||
|
print(f"Got an error: {e.response['error']}")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return
|
@ -27,7 +27,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
|||||||
)
|
)
|
||||||
abs_url = "http://" + current_site + realtivelink
|
abs_url = "http://" + current_site + realtivelink
|
||||||
|
|
||||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
|
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
|
||||||
|
|
||||||
|
58
apiserver/plane/db/migrations/0029_auto_20230502_0126.py
Normal file
58
apiserver/plane/db/migrations/0029_auto_20230502_0126.py
Normal 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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -59,6 +59,7 @@ from .integration import (
|
|||||||
GithubRepositorySync,
|
GithubRepositorySync,
|
||||||
GithubIssueSync,
|
GithubIssueSync,
|
||||||
GithubCommentSync,
|
GithubCommentSync,
|
||||||
|
SlackProjectSync,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .importer import Importer
|
from .importer import Importer
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
32
apiserver/plane/db/models/integration/slack.py
Normal file
32
apiserver/plane/db/models/integration/slack.py
Normal 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",)
|
@ -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"]
|
||||||
|
@ -109,7 +109,7 @@ def send_welcome_email(sender, instance, created, **kwargs):
|
|||||||
if created and not instance.is_bot:
|
if created and not instance.is_bot:
|
||||||
first_name = instance.first_name.capitalize()
|
first_name = instance.first_name.capitalize()
|
||||||
to_email = instance.email
|
to_email = instance.email
|
||||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"Welcome to Plane ✈️!"
|
subject = f"Welcome to Plane ✈️!"
|
||||||
|
|
||||||
|
@ -174,11 +174,12 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
|||||||
# Host for sending e-mail.
|
# Host for sending e-mail.
|
||||||
EMAIL_HOST = os.environ.get("EMAIL_HOST")
|
EMAIL_HOST = os.environ.get("EMAIL_HOST")
|
||||||
# Port for sending e-mail.
|
# Port for sending e-mail.
|
||||||
EMAIL_PORT = 587
|
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
|
||||||
# Optional SMTP authentication information for EMAIL_HOST.
|
# Optional SMTP authentication information for EMAIL_HOST.
|
||||||
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
|
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
|
||||||
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
|
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
|
||||||
EMAIL_USE_TLS = True
|
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1"
|
||||||
|
EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>")
|
||||||
|
|
||||||
|
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
|
@ -83,3 +83,6 @@ LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
|||||||
|
|
||||||
CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL")
|
CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL")
|
||||||
CELERY_BROKER_URL = os.environ.get("REDIS_URL")
|
CELERY_BROKER_URL = os.environ.get("REDIS_URL")
|
||||||
|
|
||||||
|
|
||||||
|
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
@ -105,7 +105,7 @@ if (
|
|||||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||||
|
|
||||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||||
AWS_S3_ENDPOINT_URL = ""
|
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||||
|
|
||||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||||
AWS_S3_KEY_PREFIX = ""
|
AWS_S3_KEY_PREFIX = ""
|
||||||
@ -240,7 +240,15 @@ SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
|||||||
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
||||||
|
|
||||||
redis_url = os.environ.get("REDIS_URL")
|
redis_url = os.environ.get("REDIS_URL")
|
||||||
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
broker_url = (
|
||||||
|
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||||
|
)
|
||||||
|
|
||||||
CELERY_RESULT_BACKEND = broker_url
|
if DOCKERIZED:
|
||||||
CELERY_BROKER_URL = broker_url
|
CELERY_BROKER_URL = REDIS_URL
|
||||||
|
CELERY_RESULT_BACKEND = REDIS_URL
|
||||||
|
else:
|
||||||
|
CELERY_RESULT_BACKEND = broker_url
|
||||||
|
CELERY_BROKER_URL = broker_url
|
||||||
|
|
||||||
|
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||||
|
@ -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 = ""
|
||||||
@ -204,3 +204,5 @@ broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={cert
|
|||||||
|
|
||||||
CELERY_RESULT_BACKEND = broker_url
|
CELERY_RESULT_BACKEND = broker_url
|
||||||
CELERY_BROKER_URL = broker_url
|
CELERY_BROKER_URL = broker_url
|
||||||
|
|
||||||
|
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||||
|
@ -5,6 +5,7 @@ from urllib.parse import urlparse, parse_qs
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
def get_jwt_token():
|
def get_jwt_token():
|
||||||
@ -128,3 +129,24 @@ def get_github_repo_details(access_tokens_url, owner, repo):
|
|||||||
).json()
|
).json()
|
||||||
|
|
||||||
return open_issues, total_labels, collaborators
|
return open_issues, total_labels, collaborators
|
||||||
|
|
||||||
|
|
||||||
|
def get_release_notes():
|
||||||
|
token = settings.GITHUB_ACCESS_TOKEN
|
||||||
|
|
||||||
|
if token:
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer " + str(token),
|
||||||
|
"Accept": "application/vnd.github.v3+json",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/vnd.github.v3+json",
|
||||||
|
}
|
||||||
|
url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1"
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
return {"error": "Unable to render information from Github Repository"}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
@ -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,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Replace with your instance Public IP
|
# Replace with your instance Public IP
|
||||||
# NEXT_PUBLIC_API_BASE_URL = "http://localhost"
|
# NEXT_PUBLIC_API_BASE_URL = "http://localhost"
|
||||||
|
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||||
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
||||||
NEXT_PUBLIC_GITHUB_ID=""
|
NEXT_PUBLIC_GITHUB_ID=""
|
||||||
@ -8,3 +9,4 @@ NEXT_PUBLIC_ENABLE_OAUTH=0
|
|||||||
NEXT_PUBLIC_ENABLE_SENTRY=0
|
NEXT_PUBLIC_ENABLE_SENTRY=0
|
||||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
||||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
NEXT_PUBLIC_TRACK_EVENTS=0
|
||||||
|
NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
||||||
|
@ -92,13 +92,13 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
|||||||
<>
|
<>
|
||||||
<form className="space-y-5 py-5 px-5">
|
<form className="space-y-5 py-5 px-5">
|
||||||
{(codeSent || codeResent) && (
|
{(codeSent || codeResent) && (
|
||||||
<div className="rounded-md bg-green-50 p-4">
|
<div className="rounded-md bg-green-500/20 p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<CheckCircleIcon className="h-5 w-5 text-green-400" aria-hidden="true" />
|
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm font-medium text-green-800">
|
<p className="text-sm font-medium text-green-500">
|
||||||
{codeResent
|
{codeResent
|
||||||
? "Please check your mail for new code."
|
? "Please check your mail for new code."
|
||||||
: "Please check your mail for code."}
|
: "Please check your mail for code."}
|
||||||
@ -141,7 +141,9 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`mt-5 flex w-full justify-end text-xs outline-none ${
|
className={`mt-5 flex w-full justify-end text-xs outline-none ${
|
||||||
isResendDisabled ? "cursor-default text-gray-400" : "cursor-pointer text-theme"
|
isResendDisabled
|
||||||
|
? "cursor-default text-brand-secondary"
|
||||||
|
: "cursor-pointer text-brand-accent"
|
||||||
} `}
|
} `}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCodeResending(true);
|
setIsCodeResending(true);
|
||||||
@ -174,7 +176,8 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
|||||||
className="w-full text-center"
|
className="w-full text-center"
|
||||||
size="md"
|
size="md"
|
||||||
onClick={handleSubmit(handleSignin)}
|
onClick={handleSubmit(handleSignin)}
|
||||||
loading={isSubmitting || (!isValid && isDirty)}
|
disabled={!isValid && isDirty}
|
||||||
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
|
@ -50,7 +50,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
|||||||
if (!error?.response?.data) return;
|
if (!error?.response?.data) return;
|
||||||
Object.keys(error.response.data).forEach((key) => {
|
Object.keys(error.response.data).forEach((key) => {
|
||||||
const err = error.response.data[key];
|
const err = error.response.data[key];
|
||||||
console.log("err", err);
|
console.log(err);
|
||||||
setError(key as keyof EmailPasswordFormValues, {
|
setError(key as keyof EmailPasswordFormValues, {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message: Array.isArray(err) ? err.join(", ") : err,
|
message: Array.isArray(err) ? err.join(", ") : err,
|
||||||
@ -94,7 +94,9 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
|||||||
<div className="mt-2 flex items-center justify-between">
|
<div className="mt-2 flex items-center justify-between">
|
||||||
<div className="ml-auto text-sm">
|
<div className="ml-auto text-sm">
|
||||||
<Link href={"/forgot-password"}>
|
<Link href={"/forgot-password"}>
|
||||||
<a className="font-medium text-theme hover:text-indigo-500">Forgot your password?</a>
|
<a className="font-medium text-brand-accent hover:text-brand-accent">
|
||||||
|
Forgot your password?
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -102,7 +104,8 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
|||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-center"
|
className="w-full text-center"
|
||||||
loading={isSubmitting || (!isValid && isDirty)}
|
disabled={!isValid && isDirty}
|
||||||
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Signing in..." : "Sign In"}
|
{isSubmitting ? "Signing in..." : "Sign In"}
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
|
@ -33,11 +33,11 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-1 w-full">
|
<div className="w-full px-1">
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||||
>
|
>
|
||||||
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-gray-200 p-2 text-sm font-medium text-gray-600 duration-300 hover:bg-gray-50">
|
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-brand-base p-2 text-sm font-medium text-brand-secondary duration-300 hover:bg-brand-surface-2">
|
||||||
<Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" />
|
<Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" />
|
||||||
<span>Sign In with Github</span>
|
<span>Sign In with Github</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -47,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||||
<div className="h-12" id="googleSignInButton" ref={googleSignInButton} />
|
<div className="overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -36,16 +36,14 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||||||
alt="ProjectSettingImg"
|
alt="ProjectSettingImg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-medium text-gray-900">
|
<h1 className="text-xl font-medium">Oops! You are not authorized to view this page</h1>
|
||||||
Oops! You are not authorized to view this page
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="w-full text-base text-gray-500 max-w-md ">
|
<div className="w-full max-w-md text-base text-brand-secondary">
|
||||||
{user ? (
|
{user ? (
|
||||||
<p>
|
<p>
|
||||||
You have signed in as {user.email}. <br />
|
You have signed in as {user.email}. <br />
|
||||||
<Link href={`/signin?next=${currentPath}`}>
|
<Link href={`/signin?next=${currentPath}`}>
|
||||||
<a className="text-gray-900 font-medium">Sign in</a>
|
<a className="font-medium text-brand-base">Sign in</a>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
with different account that has access to this page.
|
with different account that has access to this page.
|
||||||
</p>
|
</p>
|
||||||
@ -53,7 +51,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
|||||||
<p>
|
<p>
|
||||||
You need to{" "}
|
You need to{" "}
|
||||||
<Link href={`/signin?next=${currentPath}`}>
|
<Link href={`/signin?next=${currentPath}`}>
|
||||||
<a className="text-gray-900 font-medium">Sign in</a>
|
<a className="font-medium text-brand-base">Sign in</a>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
with an account that has access to this page.
|
with an account that has access to this page.
|
||||||
</p>
|
</p>
|
||||||
|
@ -31,7 +31,7 @@ export const JoinProject: React.FC = () => {
|
|||||||
project_ids: [projectId as string],
|
project_ids: [projectId as string],
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await mutate(USER_PROJECT_VIEW(workspaceSlug.toString()));
|
await mutate(USER_PROJECT_VIEW(projectId.toString()));
|
||||||
setIsJoiningProject(false);
|
setIsJoiningProject(false);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -45,9 +45,9 @@ export const JoinProject: React.FC = () => {
|
|||||||
<div className="h-44 w-72">
|
<div className="h-44 w-72">
|
||||||
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-medium text-gray-900">You are not a member of this project</h1>
|
<h1 className="text-xl font-medium">You are not a member of this project</h1>
|
||||||
|
|
||||||
<div className="w-full max-w-md text-base text-gray-500 ">
|
<div className="w-full max-w-md text-base text-brand-secondary">
|
||||||
<p className="mx-auto w-full text-sm md:w-3/4">
|
<p className="mx-auto w-full text-sm md:w-3/4">
|
||||||
You are not a member of this project, but you can join this project by clicking the button
|
You are not a member of this project, but you can join this project by clicking the button
|
||||||
below.
|
below.
|
||||||
|
@ -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>
|
||||||
|
@ -16,7 +16,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-gray-300 text-center text-sm hover:bg-gray-100"
|
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-3 w-3" />
|
<ArrowLeftIcon className="h-3 w-3" />
|
||||||
@ -37,7 +37,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
|
|||||||
<>
|
<>
|
||||||
{link ? (
|
{link ? (
|
||||||
<Link href={link}>
|
<Link href={link}>
|
||||||
<a className="border-r-2 border-gray-300 px-3 text-sm">
|
<a className="border-r-2 border-brand-base px-3 text-sm">
|
||||||
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
|
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
|
||||||
{icon ?? null}
|
{icon ?? null}
|
||||||
{title}
|
{title}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// cmdk
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
import { THEMES_OBJ } from "constants/themes";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { SettingIcon } from "components/icons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
|
// useEffect only runs on the client, so now we can safely show the UI
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{THEMES_OBJ.map((theme) => (
|
||||||
|
<Command.Item
|
||||||
|
key={theme.value}
|
||||||
|
onSelect={() => {
|
||||||
|
setTheme(theme.value);
|
||||||
|
setIsPaletteOpen(false);
|
||||||
|
}}
|
||||||
|
className="focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
|
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
|
{theme.label}
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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
|
||||||
|
@ -44,6 +44,7 @@ import {
|
|||||||
ChangeIssueState,
|
ChangeIssueState,
|
||||||
ChangeIssuePriority,
|
ChangeIssuePriority,
|
||||||
ChangeIssueAssignee,
|
ChangeIssueAssignee,
|
||||||
|
ChangeInterfaceTheme,
|
||||||
} from "components/command-palette";
|
} from "components/command-palette";
|
||||||
import { BulkDeleteIssuesModal } from "components/core";
|
import { BulkDeleteIssuesModal } from "components/core";
|
||||||
import { CreateUpdateCycleModal } from "components/cycles";
|
import { CreateUpdateCycleModal } from "components/cycles";
|
||||||
@ -379,7 +380,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
|
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||||
@ -392,7 +393,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-surface-2 shadow-2xl transition-all">
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search) => {
|
filter={(value, search) => {
|
||||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||||
@ -415,7 +416,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{issueId && issueDetails && (
|
{issueId && issueDetails && (
|
||||||
<div className="flex p-3">
|
<div className="flex p-3">
|
||||||
<p className="overflow-hidden truncate rounded-md bg-gray-100 p-1 px-2 text-xs font-medium text-gray-500">
|
<p className="overflow-hidden truncate rounded-md bg-brand-surface-1 p-1 px-2 text-xs font-medium text-brand-secondary">
|
||||||
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
||||||
{issueDetails?.name}
|
{issueDetails?.name}
|
||||||
</p>
|
</p>
|
||||||
@ -423,11 +424,11 @@ export const CommandPalette: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-secondary"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Command.Input
|
<Command.Input
|
||||||
className="w-full border-0 border-b bg-transparent p-4 pl-11 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
className="w-full border-0 border-b border-brand-base bg-transparent p-4 pl-11 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
@ -441,7 +442,9 @@ export const CommandPalette: React.FC = () => {
|
|||||||
resultsCount === 0 &&
|
resultsCount === 0 &&
|
||||||
searchTerm !== "" &&
|
searchTerm !== "" &&
|
||||||
debouncedSearchTerm !== "" && (
|
debouncedSearchTerm !== "" && (
|
||||||
<div className="my-4 text-center text-gray-500">No results found.</div>
|
<div className="my-4 text-center text-brand-secondary">
|
||||||
|
No results found.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isLoading || isSearching) && (
|
{(isLoading || isSearching) && (
|
||||||
@ -502,8 +505,11 @@ export const CommandPalette: React.FC = () => {
|
|||||||
value={value}
|
value={value}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden text-gray-700">
|
<div className="flex items-center gap-2 overflow-hidden text-brand-secondary">
|
||||||
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" />
|
<Icon
|
||||||
|
className="h-4 w-4 text-brand-secondary"
|
||||||
|
color="#6b7280"
|
||||||
|
/>
|
||||||
<p className="block flex-1 truncate">{item.name}</p>
|
<p className="block flex-1 truncate">{item.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -528,8 +534,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<Squares2X2Icon className="h-4 w-4 text-gray-500" />
|
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||||
Change state...
|
Change state...
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -541,8 +547,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<ChartBarIcon className="h-4 w-4 text-gray-500" />
|
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Change priority...
|
Change priority...
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -554,8 +560,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<UsersIcon className="h-4 w-4 text-gray-500" />
|
<UsersIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Assign to...
|
Assign to...
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -566,15 +572,15 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
{issueDetails?.assignees.includes(user.id) ? (
|
{issueDetails?.assignees.includes(user.id) ? (
|
||||||
<>
|
<>
|
||||||
<UserMinusIcon className="h-4 w-4 text-gray-500" />
|
<UserMinusIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Un-assign from me
|
Un-assign from me
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<UserPlusIcon className="h-4 w-4 text-gray-500" />
|
<UserPlusIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Assign to me
|
Assign to me
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -582,8 +588,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
|
||||||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<TrashIcon className="h-4 w-4 text-gray-500" />
|
<TrashIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Delete issue
|
Delete issue
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -594,16 +600,19 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<LinkIcon className="h-4 w-4 text-gray-500" />
|
<LinkIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Copy issue URL to clipboard
|
Copy issue URL to clipboard
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Command.Group heading="Issue">
|
<Command.Group heading="Issue">
|
||||||
<Command.Item onSelect={createNewIssue} className="focus:bg-gray-200">
|
<Command.Item
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
onSelect={createNewIssue}
|
||||||
|
className="focus:bg-brand-surface-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new issue
|
Create new issue
|
||||||
</div>
|
</div>
|
||||||
@ -617,7 +626,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewProject}
|
onSelect={createNewProject}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new project
|
Create new project
|
||||||
</div>
|
</div>
|
||||||
@ -633,7 +642,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewCycle}
|
onSelect={createNewCycle}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new cycle
|
Create new cycle
|
||||||
</div>
|
</div>
|
||||||
@ -646,7 +655,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewModule}
|
onSelect={createNewModule}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new module
|
Create new module
|
||||||
</div>
|
</div>
|
||||||
@ -656,7 +665,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
|
|
||||||
<Command.Group heading="View">
|
<Command.Group heading="View">
|
||||||
<Command.Item onSelect={createNewView} className="focus:outline-none">
|
<Command.Item onSelect={createNewView} className="focus:outline-none">
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new view
|
Create new view
|
||||||
</div>
|
</div>
|
||||||
@ -666,7 +675,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
|
|
||||||
<Command.Group heading="Page">
|
<Command.Group heading="Page">
|
||||||
<Command.Item onSelect={createNewPage} className="focus:outline-none">
|
<Command.Item onSelect={createNewPage} className="focus:outline-none">
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
|
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Create new page
|
Create new page
|
||||||
</div>
|
</div>
|
||||||
@ -685,7 +694,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Search settings...
|
Search settings...
|
||||||
</div>
|
</div>
|
||||||
@ -696,11 +705,24 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={createNewWorkspace}
|
onSelect={createNewWorkspace}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<FolderPlusIcon className="h-4 w-4 text-gray-500" />
|
<FolderPlusIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Create new workspace
|
Create new workspace
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
<Command.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setPlaceholder("Change interface theme...");
|
||||||
|
setSearchTerm("");
|
||||||
|
setPages([...pages, "change-interface-theme"]);
|
||||||
|
}}
|
||||||
|
className="focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
|
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
|
Change interface theme...
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
<Command.Group heading="Help">
|
<Command.Group heading="Help">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
@ -713,8 +735,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
|
<RocketLaunchIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Open keyboard shortcuts
|
Open keyboard shortcuts
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -725,8 +747,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<DocumentIcon className="h-4 w-4 text-gray-500" />
|
<DocumentIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Open Plane documentation
|
Open Plane documentation
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -737,7 +759,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Join our Discord
|
Join our Discord
|
||||||
</div>
|
</div>
|
||||||
@ -752,7 +774,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
||||||
Report a bug
|
Report a bug
|
||||||
</div>
|
</div>
|
||||||
@ -764,8 +786,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" />
|
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Chat with us
|
Chat with us
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -779,8 +801,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => goToSettings()}
|
onSelect={() => goToSettings()}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
General
|
General
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -788,8 +810,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => goToSettings("members")}
|
onSelect={() => goToSettings("members")}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Members
|
Members
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -797,8 +819,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => goToSettings("billing")}
|
onSelect={() => goToSettings("billing")}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Billings and Plans
|
Billings and Plans
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -806,8 +828,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => goToSettings("integrations")}
|
onSelect={() => goToSettings("integrations")}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Integrations
|
Integrations
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -815,8 +837,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
onSelect={() => goToSettings("import-export")}
|
onSelect={() => goToSettings("import-export")}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-gray-700">
|
<div className="flex items-center gap-2 text-brand-secondary">
|
||||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Import/Export
|
Import/Export
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
@ -842,6 +864,9 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setIsPaletteOpen={setIsPaletteOpen}
|
setIsPaletteOpen={setIsPaletteOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{page === "change-interface-theme" && (
|
||||||
|
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
|
||||||
|
)}
|
||||||
</Command.List>
|
</Command.List>
|
||||||
</Command>
|
</Command>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -3,3 +3,4 @@ export * from "./shortcuts-modal";
|
|||||||
export * from "./change-issue-state";
|
export * from "./change-issue-state";
|
||||||
export * from "./change-issue-priority";
|
export * from "./change-issue-priority";
|
||||||
export * from "./change-issue-assignee";
|
export * from "./change-issue-assignee";
|
||||||
|
export * from "./change-interface-theme";
|
||||||
|
@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { MacCommandIcon } from "components/icons";
|
import { CommandIcon } from "components/icons";
|
||||||
// ui
|
// ui
|
||||||
import { Input } from "components/ui";
|
import { Input } from "components/ui";
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
@ -85,29 +85,29 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||||
<div className="bg-white p-5">
|
<div className="bg-brand-surface-2 p-5">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
|
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as="h3"
|
||||||
className="flex justify-between text-lg font-medium leading-6 text-gray-900"
|
className="flex justify-between text-lg font-medium leading-6 text-brand-base"
|
||||||
>
|
>
|
||||||
<span>Keyboard Shortcuts</span>
|
<span>Keyboard Shortcuts</span>
|
||||||
<span>
|
<span>
|
||||||
<button type="button" onClick={() => setIsOpen(false)}>
|
<button type="button" onClick={() => setIsOpen(false)}>
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
className="h-6 w-6 text-gray-400 hover:text-gray-500"
|
className="h-6 w-6 text-gray-400 hover:text-brand-secondary"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-gray-200 bg-gray-100 px-3 py-2">
|
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-brand-base bg-brand-surface-1 px-3 py-2">
|
||||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-gray-500" />
|
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-brand-secondary" />
|
||||||
<Input
|
<Input
|
||||||
className="w-full border-none bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none"
|
className="w-full border-none bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none"
|
||||||
id="search"
|
id="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
@ -123,17 +123,23 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
<div key={shortcut.keys} className="flex w-full flex-col">
|
<div key={shortcut.keys} className="flex w-full flex-col">
|
||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-gray-500">{shortcut.description}</p>
|
<p className="text-sm text-brand-secondary">
|
||||||
|
{shortcut.description}
|
||||||
|
</p>
|
||||||
<div className="flex items-center gap-x-2.5">
|
<div className="flex items-center gap-x-2.5">
|
||||||
{shortcut.keys.split(",").map((key, index) => (
|
{shortcut.keys.split(",").map((key, index) => (
|
||||||
<span key={index} className="flex items-center gap-1">
|
<span key={index} className="flex items-center gap-1">
|
||||||
{key === "Ctrl" ? (
|
{key === "Ctrl" ? (
|
||||||
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
|
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5">
|
||||||
<MacCommandIcon />
|
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||||
</span>
|
</span>
|
||||||
|
) : key === "Ctrl" ? (
|
||||||
|
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
|
||||||
|
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||||
|
</kbd>
|
||||||
) : (
|
) : (
|
||||||
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
|
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
|
||||||
{key === "Ctrl" ? <MacCommandIcon /> : key}
|
{key}
|
||||||
</kbd>
|
</kbd>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@ -145,7 +151,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-brand-secondary">
|
||||||
No shortcuts found for{" "}
|
No shortcuts found for{" "}
|
||||||
<span className="font-semibold italic">
|
<span className="font-semibold italic">
|
||||||
{`"`}
|
{`"`}
|
||||||
@ -162,17 +168,21 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
{shortcuts.map(({ keys, description }, index) => (
|
{shortcuts.map(({ keys, description }, index) => (
|
||||||
<div key={index} className="flex items-center justify-between">
|
<div key={index} className="flex items-center justify-between">
|
||||||
<p className="text-sm text-gray-500">{description}</p>
|
<p className="text-sm text-brand-secondary">{description}</p>
|
||||||
<div className="flex items-center gap-x-2.5">
|
<div className="flex items-center gap-x-2.5">
|
||||||
{keys.split(",").map((key, index) => (
|
{keys.split(",").map((key, index) => (
|
||||||
<span key={index} className="flex items-center gap-1">
|
<span key={index} className="flex items-center gap-1">
|
||||||
{key === "Ctrl" ? (
|
{key === "Ctrl" ? (
|
||||||
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
|
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-brand-secondary">
|
||||||
<MacCommandIcon />
|
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||||
</span>
|
</span>
|
||||||
|
) : key === "Ctrl" ? (
|
||||||
|
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
|
||||||
|
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||||
|
</kbd>
|
||||||
) : (
|
) : (
|
||||||
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
|
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
|
||||||
{key === "Ctrl" ? <MacCommandIcon /> : key}
|
{key}
|
||||||
</kbd>
|
</kbd>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -17,7 +17,7 @@ type Props = {
|
|||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
removeIssue: ((bridgeId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
@ -81,7 +81,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center justify-between gap-2 rounded bg-white p-2 shadow"
|
className="flex items-center justify-between gap-2 rounded bg-brand-surface-1 p-2 shadow"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{currentState &&
|
{currentState &&
|
||||||
@ -92,7 +92,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
: addSpaceIfCamelCase(singleGroup)}
|
: addSpaceIfCamelCase(singleGroup)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">0</span>
|
<span className="text-xs text-brand-secondary">0</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -96,17 +96,17 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
|
|
||||||
switch (selectedGroup) {
|
switch (selectedGroup) {
|
||||||
case "state":
|
case "state":
|
||||||
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor);
|
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
||||||
break;
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||||
icon = (
|
icon = (
|
||||||
<span
|
<span
|
||||||
className="h-[18px] w-[18px] flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
style={{ backgroundColor: labelColor }}
|
style={{ backgroundColor: labelColor }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -123,8 +123,8 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex justify-between items-center px-1 ${
|
className={`flex items-center justify-between px-1 ${
|
||||||
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
|
!isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
||||||
@ -143,7 +143,9 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
{getGroupTitle()}
|
{getGroupTitle()}
|
||||||
</h2>
|
</h2>
|
||||||
<span
|
<span
|
||||||
className={`${isCollapsed ? "ml-0.5" : ""} rounded-full bg-gray-100 py-1 px-3 text-sm`}
|
className={`${
|
||||||
|
isCollapsed ? "ml-0.5" : ""
|
||||||
|
} min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs`}
|
||||||
>
|
>
|
||||||
{groupedByIssues?.[groupTitle].length ?? 0}
|
{groupedByIssues?.[groupTitle].length ?? 0}
|
||||||
</span>
|
</span>
|
||||||
@ -153,7 +155,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
|
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCollapsed((prevData) => !prevData);
|
setIsCollapsed((prevData) => !prevData);
|
||||||
}}
|
}}
|
||||||
@ -167,7 +169,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
{!isCompleted && (
|
{!isCompleted && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
|
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToState}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
@ -29,7 +29,7 @@ type Props = {
|
|||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
removeIssue: ((bridgeId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
@ -66,7 +66,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
}, [currentState]);
|
}, [currentState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
|
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96"}`}>
|
||||||
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
|
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
addIssueToState={addIssueToState}
|
addIssueToState={addIssueToState}
|
||||||
@ -81,7 +81,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`relative h-full overflow-y-auto p-1 ${
|
className={`relative h-full overflow-y-auto p-1 ${
|
||||||
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
snapshot.isDraggingOver ? "bg-brand-base/20" : ""
|
||||||
} ${!isCollapsed ? "hidden" : "block"}`}
|
} ${!isCollapsed ? "hidden" : "block"}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
@ -91,15 +91,17 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
snapshot.isDraggingOver ? "block" : "hidden"
|
snapshot.isDraggingOver ? "block" : "hidden"
|
||||||
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-gray-100 opacity-50`}
|
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-brand-surface-1 opacity-50`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
snapshot.isDraggingOver ? "block" : "hidden"
|
snapshot.isDraggingOver ? "block" : "hidden"
|
||||||
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-white p-2 text-xs`}
|
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-brand-base p-2 text-xs`}
|
||||||
>
|
>
|
||||||
This board is ordered by{" "}
|
This board is ordered by{" "}
|
||||||
{replaceUnderscoreIfSnakeCase(orderBy ?? "created_at")}
|
{replaceUnderscoreIfSnakeCase(
|
||||||
|
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -128,7 +130,8 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id);
|
if (removeIssue && issue.bridge_id)
|
||||||
|
removeIssue(issue.bridge_id, issue.id);
|
||||||
}}
|
}}
|
||||||
isCompleted={isCompleted}
|
isCompleted={isCompleted}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
@ -146,7 +149,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
{type === "issue" ? (
|
{type === "issue" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-theme outline-none"
|
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToState}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@ -158,7 +161,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
customButton={
|
customButton={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-theme outline-none"
|
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
|
@ -261,8 +261,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</a>
|
</a>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 rounded bg-white shadow ${
|
className={`mb-3 rounded bg-brand-base shadow ${
|
||||||
snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
|
snapshot.isDragging ? "border-2 border-brand-accent shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
@ -312,12 +312,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
||||||
<a>
|
<a>
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
<div className="mb-2.5 text-xs font-medium text-gray-700">
|
<div className="mb-2.5 text-xs font-medium text-brand-secondary">
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h5
|
<h5
|
||||||
className="break-all text-sm group-hover:text-theme"
|
className="break-all text-sm group-hover:text-brand-accent"
|
||||||
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
|
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
|
||||||
>
|
>
|
||||||
{truncateText(issue.name, 100)}
|
{truncateText(issue.name, 100)}
|
||||||
@ -349,7 +349,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.sub_issue_count && (
|
{properties.sub_issue_count && (
|
||||||
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
|
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
|
||||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -358,7 +358,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
{issue.label_details.map((label) => (
|
{issue.label_details.map((label) => (
|
||||||
<div
|
<div
|
||||||
key={label.id}
|
key={label.id}
|
||||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
@ -389,20 +389,20 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.link && (
|
{properties.link && (
|
||||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
<div className="flex items-center gap-1 text-brand-secondary">
|
||||||
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
|
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
|
||||||
{issue.link_count}
|
{issue.link_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.attachment_count && (
|
{properties.attachment_count && (
|
||||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
<div className="flex items-center gap-1 text-brand-secondary">
|
||||||
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
|
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
|
||||||
{issue.attachment_count}
|
{issue.attachment_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -121,7 +121,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-brand-surface-2 shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||||
<form>
|
<form>
|
||||||
<Combobox
|
<Combobox
|
||||||
onChange={(val: string) => {
|
onChange={(val: string) => {
|
||||||
@ -136,12 +136,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
|||||||
>
|
>
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -154,7 +154,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
|||||||
{filteredIssues.length > 0 ? (
|
{filteredIssues.length > 0 ? (
|
||||||
<li className="p-2">
|
<li className="p-2">
|
||||||
{query === "" && (
|
{query === "" && (
|
||||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base">
|
||||||
Select issues to delete
|
Select issues to delete
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
@ -166,7 +166,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
|||||||
value={issue.id}
|
value={issue.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
||||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
active ? "bg-gray-900 bg-opacity-5 text-brand-base" : ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -182,7 +182,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
|||||||
backgroundColor: issue.state_detail.color,
|
backgroundColor: issue.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
@ -194,9 +194,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||||
<LayerDiagonalIcon height="56" width="56" />
|
<LayerDiagonalIcon height="56" width="56" />
|
||||||
<h3 className="text-gray-500">
|
<h3 className="text-brand-secondary">
|
||||||
No issues found. Create a new issue with{" "}
|
No issues found. Create a new issue with{" "}
|
||||||
<pre className="inline rounded bg-gray-200 px-2 py-1">C</pre>.
|
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -26,14 +26,17 @@ import {
|
|||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
|
||||||
// icon
|
// icon
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
PlusIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import cyclesService from "services/cycles.service";
|
import cyclesService from "services/cycles.service";
|
||||||
@ -48,20 +51,28 @@ import { IIssue } from "types";
|
|||||||
// constant
|
// constant
|
||||||
import { monthOptions, yearOptions } from "constants/calendar";
|
import { monthOptions, yearOptions } from "constants/calendar";
|
||||||
import modulesService from "services/modules.service";
|
import modulesService from "services/modules.service";
|
||||||
|
import { getStateGroupIcon } from "components/icons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
addIssueToDate: (date: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface ICalendarRange {
|
interface ICalendarRange {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CalendarView = () => {
|
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||||
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false);
|
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
||||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true);
|
const [isMonthlyView, setIsMonthlyView] = useState(true);
|
||||||
|
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const { params } = useIssuesView();
|
||||||
|
|
||||||
const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({
|
const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({
|
||||||
startDate: startOfWeek(currentDate),
|
startDate: startOfWeek(currentDate),
|
||||||
endDate: lastDayOfWeek(currentDate),
|
endDate: lastDayOfWeek(currentDate),
|
||||||
@ -77,11 +88,13 @@ export const CalendarView = () => {
|
|||||||
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () =>
|
? () =>
|
||||||
issuesService.getIssuesWithParams(
|
issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, {
|
||||||
workspaceSlug as string,
|
...params,
|
||||||
projectId as string,
|
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
|
||||||
targetDateFilter
|
calendarDateRange.endDate
|
||||||
)
|
)};before`,
|
||||||
|
group_by: null,
|
||||||
|
})
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -95,7 +108,13 @@ export const CalendarView = () => {
|
|||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
cycleId as string,
|
cycleId as string,
|
||||||
targetDateFilter
|
{
|
||||||
|
...params,
|
||||||
|
target_date: `${renderDateFormat(
|
||||||
|
calendarDateRange.startDate
|
||||||
|
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
|
||||||
|
group_by: null,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -110,7 +129,13 @@ export const CalendarView = () => {
|
|||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
moduleId as string,
|
moduleId as string,
|
||||||
targetDateFilter
|
{
|
||||||
|
...params,
|
||||||
|
target_date: `${renderDateFormat(
|
||||||
|
calendarDateRange.startDate
|
||||||
|
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
|
||||||
|
group_by: null,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -127,12 +152,16 @@ export const CalendarView = () => {
|
|||||||
|
|
||||||
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
||||||
|
|
||||||
const calendarIssues = cycleCalendarIssues ?? moduleCalendarIssues ?? projectCalendarIssues;
|
const calendarIssues = cycleId
|
||||||
|
? (cycleCalendarIssues as IIssue[])
|
||||||
|
: moduleId
|
||||||
|
? (moduleCalendarIssues as IIssue[])
|
||||||
|
: (projectCalendarIssues as IIssue[]);
|
||||||
|
|
||||||
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
||||||
const filterIssue =
|
const filterIssue =
|
||||||
calendarIssues && calendarIssues.length > 0
|
calendarIssues && calendarIssues.length > 0
|
||||||
? (calendarIssues as IIssue[]).filter(
|
? calendarIssues.filter(
|
||||||
(issue) =>
|
(issue) =>
|
||||||
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
|
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
|
||||||
)
|
)
|
||||||
@ -198,17 +227,17 @@ export const CalendarView = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return calendarIssues ? (
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<div className="h-full overflow-y-auto rounded-lg text-gray-600 -m-2">
|
<div className="-m-2 h-full overflow-y-auto rounded-lg text-brand-secondary">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="relative flex h-full w-full gap-2 items-center justify-start text-sm ">
|
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
||||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
<Popover className="flex h-full items-center justify-start rounded-lg">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button className={`group flex h-full items-start gap-1 text-gray-800`}>
|
<Popover.Button className={`group flex h-full items-start gap-1 text-brand-base`}>
|
||||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold">
|
<div className="flex items-center justify-center gap-2 text-2xl font-semibold">
|
||||||
<span className="text-black">{formatDate(currentDate, "Month")}</span>{" "}
|
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
||||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
<span>{formatDate(currentDate, "yyyy")}</span>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
@ -222,30 +251,30 @@ export const CalendarView = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute top-10 left-0 z-20 w-full max-w-xs flex flex-col transform overflow-hidden bg-white shadow-lg rounded-[10px]">
|
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
|
||||||
<div className="flex justify-center items-center text-sm gap-5 px-2 py-2">
|
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||||
{yearOptions.map((year) => (
|
{yearOptions.map((year) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
||||||
className={` ${
|
className={` ${
|
||||||
isSameYear(year.value, currentDate)
|
isSameYear(year.value, currentDate)
|
||||||
? "text-sm font-medium text-gray-800"
|
? "text-sm font-medium text-brand-base"
|
||||||
: "text-xs text-gray-400 "
|
: "text-xs text-brand-secondary "
|
||||||
} hover:text-sm hover:text-gray-800 hover:font-medium `}
|
} hover:text-sm hover:font-medium hover:text-brand-base`}
|
||||||
>
|
>
|
||||||
{year.label}
|
{year.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 px-2 border-t border-gray-200">
|
<div className="grid grid-cols-4 border-t border-brand-base px-2">
|
||||||
{monthOptions.map((month) => (
|
{monthOptions.map((month) => (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateDate(updateDateWithMonth(month.value, currentDate))
|
updateDate(updateDateWithMonth(month.value, currentDate))
|
||||||
}
|
}
|
||||||
className={`text-gray-400 text-xs px-2 py-2 hover:font-medium hover:text-gray-800 ${
|
className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${
|
||||||
isSameMonth(month.value, currentDate)
|
isSameMonth(month.value, currentDate)
|
||||||
? "font-medium text-gray-800"
|
? "font-medium text-brand-base"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -295,9 +324,9 @@ export const CalendarView = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full gap-2 items-center justify-end">
|
<div className="flex w-full items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
className="group flex cursor-pointer items-center gap-2 rounded-md border bg-white px-4 py-1.5 text-sm hover:bg-gray-100 hover:text-gray-900 focus:outline-none"
|
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMonthlyView) {
|
if (isMonthlyView) {
|
||||||
updateDate(new Date());
|
updateDate(new Date());
|
||||||
@ -310,13 +339,12 @@ export const CalendarView = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Today{" "}
|
Today
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
customButton={
|
||||||
<div
|
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
|
||||||
className={`group flex cursor-pointer items-center gap-2 rounded-md border bg-white px-3 py-1.5 text-sm hover:bg-gray-100 hover:text-gray-900 focus:outline-none `}
|
|
||||||
>
|
|
||||||
{isMonthlyView ? "Monthly" : "Weekly"}
|
{isMonthlyView ? "Monthly" : "Weekly"}
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
@ -330,7 +358,7 @@ export const CalendarView = () => {
|
|||||||
endDate: lastDayOfWeek(currentDate),
|
endDate: lastDayOfWeek(currentDate),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="w-52 text-sm text-gray-600"
|
className="w-52 text-sm text-brand-secondary"
|
||||||
>
|
>
|
||||||
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
||||||
<span className="flex items-center gap-2">Monthly View</span>
|
<span className="flex items-center gap-2">Monthly View</span>
|
||||||
@ -349,7 +377,7 @@ export const CalendarView = () => {
|
|||||||
endDate: getCurrentWeekEndDate(currentDate),
|
endDate: getCurrentWeekEndDate(currentDate),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="w-52 text-sm text-gray-600"
|
className="w-52 text-sm text-brand-secondary"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
<span className="flex items-center gap-2">Weekly View</span>
|
<span className="flex items-center gap-2">Weekly View</span>
|
||||||
@ -360,25 +388,12 @@ export const CalendarView = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-gray-200 py-2 px-1 text-sm text-gray-600">
|
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
|
||||||
<h4>Show weekends</h4>
|
<h4>Show weekends</h4>
|
||||||
<button
|
<ToggleSwitch
|
||||||
type="button"
|
value={showWeekEnds}
|
||||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
onChange={() => setShowWeekEnds(!showWeekEnds)}
|
||||||
showWeekEnds ? "bg-green-500" : "bg-gray-200"
|
|
||||||
}`}
|
|
||||||
role="switch"
|
|
||||||
aria-checked={showWeekEnds}
|
|
||||||
onClick={() => setShowWeekEnds(!showWeekEnds)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Show weekends</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
||||||
showWeekEnds ? "translate-x-2.5" : "translate-x-0"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
@ -392,7 +407,7 @@ export const CalendarView = () => {
|
|||||||
{weeks.map((date, index) => (
|
{weeks.map((date, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`flex items-center justify-start p-1.5 gap-2 border-gray-300 bg-gray-100 text-base font-medium text-gray-600 ${
|
className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-brand-secondary ${
|
||||||
!isMonthlyView
|
!isMonthlyView
|
||||||
? showWeekEnds
|
? showWeekEnds
|
||||||
? (index + 1) % 7 === 0
|
? (index + 1) % 7 === 0
|
||||||
@ -417,14 +432,17 @@ export const CalendarView = () => {
|
|||||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
} `}
|
} `}
|
||||||
>
|
>
|
||||||
{currentViewDaysData.map((date, index) => (
|
{currentViewDaysData.map((date, index) => {
|
||||||
|
const totalIssues = date.issues.length;
|
||||||
|
|
||||||
|
return (
|
||||||
<StrictModeDroppable droppableId={date.date}>
|
<StrictModeDroppable droppableId={date.date}>
|
||||||
{(provided, snapshot) => (
|
{(provided) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
className={`flex flex-col gap-1.5 border-t border-gray-300 p-2.5 text-left text-sm font-medium hover:bg-gray-100 ${
|
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
|
showWeekEnds
|
||||||
? (index + 1) % 7 === 0
|
? (index + 1) % 7 === 0
|
||||||
? ""
|
? ""
|
||||||
@ -435,8 +453,10 @@ export const CalendarView = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
|
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
|
||||||
{date.issues.length > 0 &&
|
{totalIssues > 0 &&
|
||||||
date.issues.map((issue: IIssue, index) => (
|
date.issues
|
||||||
|
.slice(0, showAllIssues ? totalIssues : 4)
|
||||||
|
.map((issue: IIssue, index) => (
|
||||||
<Draggable draggableId={issue.id} index={index}>
|
<Draggable draggableId={issue.id} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
@ -444,27 +464,61 @@ export const CalendarView = () => {
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
className={`w-full cursor-pointer truncate rounded bg-white p-1.5 hover:scale-105 ${
|
className={`w-full cursor-pointer truncate rounded border border-brand-base px-1.5 py-1 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
|
||||||
snapshot.isDragging ? "shadow-lg" : ""
|
snapshot.isDragging ? "bg-brand-surface-2 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.state_detail.group,
|
||||||
|
"12",
|
||||||
|
"12",
|
||||||
|
issue.state_detail.color
|
||||||
|
)}
|
||||||
{issue.name}
|
{issue.name}
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
|
{totalIssues > 4 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
|
||||||
|
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`absolute ${
|
||||||
|
isMonthlyView ? "bottom-2" : "top-2"
|
||||||
|
} right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center gap-1 text-center"
|
||||||
|
onClick={() => addIssueToDate(date.date)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-3 w-3 text-brand-secondary" />
|
||||||
|
Add issue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
267
apps/app/components/core/custom-theme-form.tsx
Normal file
267
apps/app/components/core/custom-theme-form.tsx
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
// ui
|
||||||
|
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
palette: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeForm: React.FC<any> = ({ handleFormSubmit, handleClose, status, data }) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
} = useForm<any>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
const [darkPalette, setDarkPalette] = useState(false);
|
||||||
|
|
||||||
|
const handleUpdateTheme = async (formData: any) => {
|
||||||
|
await handleFormSubmit({ ...formData, darkPalette });
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
|
// --color-bg-base: 25, 27, 27;
|
||||||
|
// --color-bg-surface-1: 31, 32, 35;
|
||||||
|
// --color-bg-surface-2: 39, 42, 45;
|
||||||
|
|
||||||
|
// --color-border: 46, 50, 52;
|
||||||
|
// --color-bg-sidebar: 19, 20, 22;
|
||||||
|
// --color-accent: 60, 133, 217;
|
||||||
|
|
||||||
|
// --color-text-base: 255, 255, 255;
|
||||||
|
// --color-text-secondary: 142, 148, 146;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateTheme)}>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-brand-base">Customize your theme</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-6">
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Input
|
||||||
|
id="bgBase"
|
||||||
|
label="Background"
|
||||||
|
name="bgBase"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.bgBase}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Background color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Background color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Input
|
||||||
|
id="bgSurface1"
|
||||||
|
label="Background surface 1"
|
||||||
|
name="bgSurface1"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.bgSurface1}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Background surface 1 color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Background surface 1 color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Input
|
||||||
|
id="bgSurface2"
|
||||||
|
label="Background surface 2"
|
||||||
|
name="bgSurface1"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.bgSurface1}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Background surface 2 color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Background surface 2 color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Input
|
||||||
|
id="border"
|
||||||
|
label="Border"
|
||||||
|
name="border"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.border}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Border color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Border color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Input
|
||||||
|
id="sidebar"
|
||||||
|
label="Sidebar"
|
||||||
|
name="sidebar"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.sidebar}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Sidebar color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Sidebar color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Input
|
||||||
|
id="accent"
|
||||||
|
label="Accent"
|
||||||
|
name="accent"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.accent}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Accent color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Accent color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-3">
|
||||||
|
<Input
|
||||||
|
id="textBase"
|
||||||
|
label="Text primary"
|
||||||
|
name="textBase"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.textBase}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Text primary color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Text primary color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-3">
|
||||||
|
<Input
|
||||||
|
id="textSecondary"
|
||||||
|
label="Text secondary"
|
||||||
|
name="textSecondary"
|
||||||
|
type="name"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.textSecondary}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Text secondary color is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Text secondary color should be hex format",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="palette"
|
||||||
|
label="All colors"
|
||||||
|
name="palette"
|
||||||
|
type="name"
|
||||||
|
placeholder="Enter comma separated hex colors"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.palette}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Color values is required",
|
||||||
|
pattern: {
|
||||||
|
value: /^(#(?:[0-9a-fA-F]{3}){1,2},){7}#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
|
message: "Color values should be hex format, separated by commas",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center gap-1"
|
||||||
|
onClick={() => setDarkPalette((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<span className="text-xs">Dark palette</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`pointer-events-none relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent ${
|
||||||
|
darkPalette ? "bg-brand-accent" : "bg-gray-300"
|
||||||
|
} transition-colors duration-300 ease-in-out focus:outline-none`}
|
||||||
|
role="switch"
|
||||||
|
aria-checked="false"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Dark palette</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`pointer-events-none inline-block h-3 w-3 ${
|
||||||
|
darkPalette ? "translate-x-3" : "translate-x-0"
|
||||||
|
} transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-300 ease-in-out`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{status
|
||||||
|
? isSubmitting
|
||||||
|
? "Updating Theme..."
|
||||||
|
: "Update Theme"
|
||||||
|
: isSubmitting
|
||||||
|
? "Creating Theme..."
|
||||||
|
: "Set Theme"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
65
apps/app/components/core/custom-theme-modal.tsx
Normal file
65
apps/app/components/core/custom-theme-modal.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// components
|
||||||
|
import { ThemeForm } from "./custom-theme-form";
|
||||||
|
// helpers
|
||||||
|
import { applyTheme } from "helpers/theme.helper";
|
||||||
|
// fetch-keys
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomThemeModal: React.FC<Props> = ({ isOpen, handleClose }) => {
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: any) => {
|
||||||
|
applyTheme(formData.palette, formData.darkPalette);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform rounded-lg bg-brand-surface-1 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
|
<ThemeForm
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleFormSubmit={handleFormSubmit}
|
||||||
|
status={false}
|
||||||
|
/>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
@ -117,7 +117,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||||
@ -130,7 +130,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
|
||||||
<form>
|
<form>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -139,11 +139,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
<Combobox as="div" {...field} multiple>
|
<Combobox as="div" {...field} multiple>
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -151,26 +151,26 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
<Combobox.Options
|
<Combobox.Options
|
||||||
static
|
static
|
||||||
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
className="max-h-80 scroll-py-2 divide-y divide-brand-base overflow-y-auto"
|
||||||
>
|
>
|
||||||
{filteredIssues.length > 0 ? (
|
{filteredIssues.length > 0 ? (
|
||||||
<li className="p-2">
|
<li className="p-2">
|
||||||
{query === "" && (
|
{query === "" && (
|
||||||
<h2 className="mb-2 px-3 text-xs font-semibold text-gray-900">
|
<h2 className="mb-2 px-3 text-xs font-medium text-brand-base">
|
||||||
Select issues to add
|
Select issues to add
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<ul className="text-sm text-gray-700">
|
<ul className="text-sm text-brand-base">
|
||||||
{filteredIssues.map((issue) => (
|
{filteredIssues.map((issue) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
as="label"
|
as="label"
|
||||||
htmlFor={`issue-${issue.id}`}
|
htmlFor={`issue-${issue.id}`}
|
||||||
value={issue.id}
|
value={issue.id}
|
||||||
className={({ active }) =>
|
className={({ active, selected }) =>
|
||||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${
|
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
|
||||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
active ? "bg-brand-surface-2 text-brand-base" : ""
|
||||||
}`
|
} ${selected ? "text-brand-base" : ""}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
@ -182,7 +182,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
backgroundColor: issue.state_detail.color,
|
backgroundColor: issue.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
<span className="flex-shrink-0 text-xs">
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
{issue.name}
|
{issue.name}
|
||||||
@ -194,10 +194,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||||
<LayerDiagonalIcon height="56" width="56" />
|
<LayerDiagonalIcon height="52" width="52" />
|
||||||
<h3 className="text-gray-500">
|
<h3 className="text-sm text-brand-secondary">
|
||||||
No issues found. Create a new issue with{" "}
|
No issues found. Create a new issue with{" "}
|
||||||
<pre className="inline rounded bg-gray-200 px-2 py-1">C</pre>.
|
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>
|
||||||
|
.
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -55,43 +55,47 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
message: "set the module to",
|
message: "set the module to",
|
||||||
icon: <RectangleGroupIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <RectangleGroupIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
message: "set the state to",
|
message: "set the state to",
|
||||||
icon: <Squares2X2Icon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <Squares2X2Icon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
message: "set the priority to",
|
message: "set the priority to",
|
||||||
icon: <ChartBarIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <ChartBarIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
message: "set the name to",
|
message: "set the name to",
|
||||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: (
|
||||||
|
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
message: "updated the description.",
|
message: "updated the description.",
|
||||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: (
|
||||||
|
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
estimate_point: {
|
estimate_point: {
|
||||||
message: "set the estimate point to",
|
message: "set the estimate point to",
|
||||||
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
|
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
message: "set the due date to",
|
message: "set the due date to",
|
||||||
icon: <CalendarDaysIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <CalendarDaysIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
message: "set the parent to",
|
message: "set the parent to",
|
||||||
icon: <UserIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <UserIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
issue: {
|
issue: {
|
||||||
message: "deleted the issue.",
|
message: "deleted the issue.",
|
||||||
icon: <TrashIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
icon: <TrashIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
estimate: {
|
estimate: {
|
||||||
message: "updated the estimate",
|
message: "updated the estimate",
|
||||||
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
|
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
message: "updated the link",
|
message: "updated the link",
|
||||||
@ -153,11 +157,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
) {
|
) {
|
||||||
const { workspace_detail, project, issue } = activity;
|
const { workspace_detail, project, issue } = activity;
|
||||||
value = (
|
value = (
|
||||||
<span className="text-gray-600">
|
<span className="text-brand-secondary">
|
||||||
created{" "}
|
created{" "}
|
||||||
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
|
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
|
||||||
<a className="inline-flex items-center hover:underline">
|
<a className="inline-flex items-center hover:underline">
|
||||||
this issue. <ArrowTopRightOnSquareIcon className="h-3.5 w-3.5 ml-1" />
|
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
@ -198,7 +202,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
|
|
||||||
if (activity.field === "comment") {
|
if (activity.field === "comment") {
|
||||||
return (
|
return (
|
||||||
<div key={activity.id}>
|
<div key={activity.id} className="mt-2">
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
<div className="relative px-1">
|
<div className="relative px-1">
|
||||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||||
@ -217,9 +221,9 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-white px-0.5 py-px">
|
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
|
||||||
<ChatBubbleLeftEllipsisIcon
|
<ChatBubbleLeftEllipsisIcon
|
||||||
className="h-3.5 w-3.5 text-gray-400"
|
className="h-3.5 w-3.5 text-brand-secondary"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@ -230,7 +234,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
{activity.actor_detail.first_name}
|
{activity.actor_detail.first_name}
|
||||||
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
|
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-xs text-gray-500">
|
<p className="mt-0.5 text-xs text-brand-secondary">
|
||||||
Commented {timeAgo(activity.created_at)}
|
Commented {timeAgo(activity.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -242,9 +246,8 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
: activity.old_value
|
: activity.old_value
|
||||||
}
|
}
|
||||||
editable={false}
|
editable={false}
|
||||||
onBlur={() => ({})}
|
|
||||||
noBorder
|
noBorder
|
||||||
customClassName="text-xs bg-gray-100"
|
customClassName="text-xs border border-brand-base bg-brand-base"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -259,7 +262,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
<div className="relative pb-1">
|
<div className="relative pb-1">
|
||||||
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
|
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
|
||||||
<span
|
<span
|
||||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-gray-200"
|
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-brand-surface-2"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -268,7 +271,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
<div>
|
<div>
|
||||||
<div className="relative px-1.5">
|
<div className="relative px-1.5">
|
||||||
<div className="mt-1.5">
|
<div className="mt-1.5">
|
||||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 ring-white">
|
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-2 ring-white">
|
||||||
{activity.field ? (
|
{activity.field ? (
|
||||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||||
) : activity.actor_detail.avatar &&
|
) : activity.actor_detail.avatar &&
|
||||||
@ -292,7 +295,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 py-3">
|
<div className="min-w-0 flex-1 py-3">
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-brand-secondary">
|
||||||
<span className="text-gray font-medium">
|
<span className="text-gray font-medium">
|
||||||
{activity.actor_detail.first_name}
|
{activity.actor_detail.first_name}
|
||||||
{activity.actor_detail.is_bot
|
{activity.actor_detail.is_bot
|
||||||
@ -300,7 +303,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
: " " + activity.actor_detail.last_name}
|
: " " + activity.actor_detail.last_name}
|
||||||
</span>
|
</span>
|
||||||
<span> {action} </span>
|
<span> {action} </span>
|
||||||
<span className="text-xs font-medium text-gray-900"> {value} </span>
|
<span className="text-xs font-medium text-brand-base"> {value} </span>
|
||||||
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,7 @@ import issuesService from "services/issues.service";
|
|||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
// types
|
// types
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
|
|
||||||
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||||
@ -37,7 +37,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
@ -57,9 +57,9 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className="flex items-center gap-x-2 rounded-full border bg-white px-2 py-1"
|
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
|
||||||
>
|
>
|
||||||
<span className="font-medium capitalize text-gray-500">
|
<span className="capitalize text-brand-secondary">
|
||||||
{replaceUnderscoreIfSnakeCase(key)}:
|
{replaceUnderscoreIfSnakeCase(key)}:
|
||||||
</span>
|
</span>
|
||||||
{filters[key as keyof IIssueFilterOptions] === null ||
|
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||||
@ -75,7 +75,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
key={state?.id}
|
key={state?.id}
|
||||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium text-white"
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
|
||||||
style={{
|
style={{
|
||||||
color: state?.color,
|
color: state?.color,
|
||||||
backgroundColor: `${state?.color}20`,
|
backgroundColor: `${state?.color}20`,
|
||||||
@ -122,16 +122,16 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
{filters.priority?.map((priority: any) => (
|
{filters.priority?.map((priority: any) => (
|
||||||
<p
|
<p
|
||||||
key={priority}
|
key={priority}
|
||||||
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium capitalize text-white ${
|
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
|
||||||
priority === "urgent"
|
priority === "urgent"
|
||||||
? "bg-red-100 text-red-600 hover:bg-red-100"
|
? "bg-red-500/20 text-red-500"
|
||||||
: priority === "high"
|
: priority === "high"
|
||||||
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
|
? "bg-orange-500/20 text-orange-500"
|
||||||
: priority === "medium"
|
: priority === "medium"
|
||||||
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
|
? "bg-yellow-500/20 text-yellow-500"
|
||||||
: priority === "low"
|
: priority === "low"
|
||||||
? "bg-green-100 text-green-500 hover:bg-green-100"
|
? "bg-green-500/20 text-green-500"
|
||||||
: "bg-gray-100 text-gray-700 hover:bg-gray-100"
|
: "bg-brand-surface-1 text-brand-secondary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{getPriorityIcon(priority)}</span>
|
<span>{getPriorityIcon(priority)}</span>
|
||||||
@ -170,7 +170,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={memberId}
|
key={memberId}
|
||||||
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize"
|
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
|
||||||
>
|
>
|
||||||
<Avatar user={member} />
|
<Avatar user={member} />
|
||||||
<span>{member?.first_name}</span>
|
<span>{member?.first_name}</span>
|
||||||
@ -203,7 +203,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
<XMarkIcon className="h-3 w-3" />
|
<XMarkIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (key as keyof IIssueFilterOptions) === "created_by" ? (
|
) : key === "created_by" ? (
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{filters.created_by?.map((memberId: string) => {
|
{filters.created_by?.map((memberId: string) => {
|
||||||
const member = members?.find((m) => m.member.id === memberId)?.member;
|
const member = members?.find((m) => m.member.id === memberId)?.member;
|
||||||
@ -211,7 +211,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${memberId}-${key}`}
|
key={`${memberId}-${key}`}
|
||||||
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize"
|
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
|
||||||
>
|
>
|
||||||
<Avatar user={member} />
|
<Avatar user={member} />
|
||||||
<span>{member?.first_name}</span>
|
<span>{member?.first_name}</span>
|
||||||
@ -253,25 +253,20 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
const color = label.color !== "" ? label.color : "#0f172a";
|
const color = label.color !== "" ? label.color : "#0f172a";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||||
style={{
|
style={{
|
||||||
background: `${color}33`, // add 20% opacity
|
color: color,
|
||||||
|
backgroundColor: `${color}20`, // add 20% opacity
|
||||||
}}
|
}}
|
||||||
key={labelId}
|
key={labelId}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="h-2 w-2 rounded-full"
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span>{label.name}</span>
|
||||||
style={{
|
|
||||||
color: color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label.name}
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -339,10 +334,10 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
created_by: null,
|
created_by: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="flex items-center gap-x-1 rounded-full border bg-white px-3 py-1.5 text-xs"
|
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<span className="font-medium">Clear all filters</span>
|
<span>Clear all filters</span>
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border bg-white p-4 shadow ${
|
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-brand-base bg-brand-surface-2 p-4 shadow ${
|
||||||
isOpen ? "block" : "hidden"
|
isOpen ? "block" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -65,7 +65,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
|||||||
return (
|
return (
|
||||||
<Popover className="relative z-[2]" ref={ref}>
|
<Popover className="relative z-[2]" ref={ref}>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="rounded border border-gray-500 bg-white px-2 py-1 text-xs text-gray-700"
|
className="rounded-md border border-brand-base bg-brand-surface-2 px-2 py-1 text-xs text-brand-secondary"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@ -79,16 +79,16 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-white shadow-lg">
|
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-brand-base bg-brand-surface-2 shadow-lg">
|
||||||
<div className="h-96 w-80 overflow-auto rounded border bg-white p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
<div className="h-96 w-80 overflow-auto rounded border border-brand-base bg-brand-surface-2 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List as="span" className="inline-block rounded bg-gray-200 p-1">
|
<Tab.List as="span" className="inline-block rounded bg-brand-surface-2 p-1">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
|
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
|
||||||
selected ? "bg-theme text-white" : "text-black"
|
selected ? "bg-brand-accent text-white" : "text-brand-base"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -110,7 +110,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||||
@ -124,9 +124,9 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
|
||||||
Upload Image
|
Upload Image
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@ -135,7 +135,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
||||||
(image === null && isDragActive) || !value
|
(image === null && isDragActive) || !value
|
||||||
? "border-2 border-dashed border-gray-300 hover:border-gray-400"
|
? "border-2 border-dashed border-brand-base hover:border-gray-400"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -143,7 +143,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600"
|
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-brand-surface-1 px-2 py-0.5 text-xs font-medium text-gray-600"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@ -157,7 +157,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<UserCircleIcon className="mx-auto h-16 w-16 text-gray-400" />
|
<UserCircleIcon className="mx-auto h-16 w-16 text-gray-400" />
|
||||||
<span className="mt-2 block text-sm font-medium text-gray-900">
|
<span className="mt-2 block text-sm font-medium text-brand-base">
|
||||||
{isDragActive
|
{isDragActive
|
||||||
? "Drop image here to upload"
|
? "Drop image here to upload"
|
||||||
: "Drag & drop image here"}
|
: "Drag & drop image here"}
|
||||||
|
@ -11,3 +11,4 @@ export * from "./link-modal";
|
|||||||
export * from "./image-picker-popover";
|
export * from "./image-picker-popover";
|
||||||
export * from "./filter-list";
|
export * from "./filter-list";
|
||||||
export * from "./feeds";
|
export * from "./feeds";
|
||||||
|
export * from "./theme-switch";
|
||||||
|
@ -10,10 +10,14 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
// components
|
// components
|
||||||
import { SelectFilters } from "components/views";
|
import { SelectFilters } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu, ToggleSwitch } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, ListBulletIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
ChevronDownIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
Squares2X2Icon,
|
||||||
|
CalendarDaysIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -53,30 +57,30 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
issueView === "list" ? "bg-gray-200" : ""
|
issueView === "list" ? "bg-brand-surface-2" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIssueView("list")}
|
onClick={() => setIssueView("list")}
|
||||||
>
|
>
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
issueView === "kanban" ? "bg-gray-200" : ""
|
issueView === "kanban" ? "bg-brand-surface-2" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIssueView("kanban")}
|
onClick={() => setIssueView("kanban")}
|
||||||
>
|
>
|
||||||
<Squares2X2Icon className="h-4 w-4" />
|
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
issueView === "calendar" ? "bg-gray-200" : ""
|
issueView === "calendar" ? "bg-brand-surface-2" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIssueView("calendar")}
|
onClick={() => setIssueView("calendar")}
|
||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-4 w-4" />
|
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<SelectFilters
|
<SelectFilters
|
||||||
@ -113,8 +117,8 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border bg-transparent px-3 py-1.5 text-xs hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${
|
className={`group flex items-center gap-2 rounded-md border border-brand-base bg-transparent px-3 py-1.5 text-xs hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none ${
|
||||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@ -130,15 +134,17 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2">
|
<div className="relative divide-y-2 divide-brand-base">
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
|
{issueView !== "calendar" && (
|
||||||
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-gray-600">Group by</h4>
|
<h4 className="text-brand-secondary">Group by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
|
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||||
"Select"
|
?.name ?? "Select"
|
||||||
}
|
}
|
||||||
width="lg"
|
width="lg"
|
||||||
>
|
>
|
||||||
@ -155,7 +161,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-gray-600">Order by</h4>
|
<h4 className="text-brand-secondary">Order by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||||
@ -177,8 +183,10 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-gray-600">Issue type</h4>
|
<h4 className="text-brand-secondary">Issue type</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||||
@ -200,25 +208,15 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{issueView !== "calendar" && (
|
||||||
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-gray-600">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-gray-200"
|
|
||||||
}`}
|
|
||||||
role="switch"
|
|
||||||
aria-checked={showEmptyGroups}
|
|
||||||
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Show empty groups</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
||||||
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<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()}>
|
||||||
@ -226,15 +224,18 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="font-medium text-theme"
|
className="font-medium text-brand-accent"
|
||||||
onClick={() => setNewFilterDefaultView()}
|
onClick={() => setNewFilterDefaultView()}
|
||||||
>
|
>
|
||||||
Set as default
|
Set as default
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{issueView !== "calendar" && (
|
||||||
<div className="space-y-2 py-3">
|
<div className="space-y-2 py-3">
|
||||||
<h4 className="text-sm text-gray-600">Display Properties</h4>
|
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{Object.keys(properties).map((key) => {
|
{Object.keys(properties).map((key) => {
|
||||||
if (key === "estimate" && !isEstimateActive) return null;
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
@ -245,8 +246,8 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||||
properties[key as keyof Properties]
|
properties[key as keyof Properties]
|
||||||
? "border-theme bg-theme text-white"
|
? "border-brand-accent bg-brand-accent text-white"
|
||||||
: "border-gray-300"
|
: "border-brand-base"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
>
|
>
|
||||||
@ -256,6 +257,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -46,7 +46,7 @@ import {
|
|||||||
MODULE_DETAILS,
|
MODULE_DETAILS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
STATE_LIST,
|
STATES_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// image
|
// image
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
} = useIssuesView();
|
} = useIssuesView();
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
@ -280,6 +280,17 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addIssueToDate = useCallback(
|
||||||
|
(date: string) => {
|
||||||
|
setCreateIssueModal(true);
|
||||||
|
setPreloadedData({
|
||||||
|
target_date: date,
|
||||||
|
actionType: "createIssue",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setCreateIssueModal, setPreloadedData]
|
||||||
|
);
|
||||||
|
|
||||||
const makeIssueCopy = useCallback(
|
const makeIssueCopy = useCallback(
|
||||||
(issue: IIssue) => {
|
(issue: IIssue) => {
|
||||||
setCreateIssueModal(true);
|
setCreateIssueModal(true);
|
||||||
@ -303,10 +314,26 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const removeIssueFromCycle = useCallback(
|
const removeIssueFromCycle = useCallback(
|
||||||
(bridgeId: string) => {
|
(bridgeId: string, issueId: string) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
|
|
||||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
mutate(
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
|
||||||
|
(prevData: any) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
if (selectedGroup) {
|
||||||
|
const filteredData: any = {};
|
||||||
|
for (const key in prevData) {
|
||||||
|
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
|
||||||
|
}
|
||||||
|
return filteredData;
|
||||||
|
} else {
|
||||||
|
const filteredData = prevData.filter((i: any) => i.id !== issueId);
|
||||||
|
return filteredData;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.removeIssueFromCycle(
|
.removeIssueFromCycle(
|
||||||
@ -315,8 +342,12 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
cycleId as string,
|
cycleId as string,
|
||||||
bridgeId
|
bridgeId
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
console.log(res);
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
message: "Issue removed successfully.",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -326,10 +357,26 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const removeIssueFromModule = useCallback(
|
const removeIssueFromModule = useCallback(
|
||||||
(bridgeId: string) => {
|
(bridgeId: string, issueId: string) => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||||
|
|
||||||
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
mutate(
|
||||||
|
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
|
||||||
|
(prevData: any) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
if (selectedGroup) {
|
||||||
|
const filteredData: any = {};
|
||||||
|
for (const key in prevData) {
|
||||||
|
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
|
||||||
|
}
|
||||||
|
return filteredData;
|
||||||
|
} else {
|
||||||
|
const filteredData = prevData.filter((item: any) => item.id !== issueId);
|
||||||
|
return filteredData;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
modulesService
|
modulesService
|
||||||
.removeIssueFromModule(
|
.removeIssueFromModule(
|
||||||
@ -338,8 +385,12 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
moduleId as string,
|
moduleId as string,
|
||||||
bridgeId
|
bridgeId
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
console.log(res);
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
message: "Issue removed successfully.",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -391,11 +442,10 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
handleClose={() => setTransferIssuesModal(false)}
|
handleClose={() => setTransferIssuesModal(false)}
|
||||||
isOpen={transferIssuesModal}
|
isOpen={transferIssuesModal}
|
||||||
/>
|
/>
|
||||||
{issueView !== "calendar" && (
|
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between gap-2 ${
|
className={`flex items-center justify-between gap-2 ${
|
||||||
issueView === "list" && areFiltersApplied ? "px-8 mt-6" : "-mt-2"
|
issueView === "list" ? (areFiltersApplied ? "mt-6 px-8" : "") : "-mt-2"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FilterList filters={filters} setFilters={setFilters} />
|
<FilterList filters={filters} setFilters={setFilters} />
|
||||||
@ -422,18 +472,18 @@ 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`} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
<StrictModeDroppable droppableId="trashBox">
|
<StrictModeDroppable droppableId="trashBox">
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||||
} fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${
|
} fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-red-500/20 p-3 text-xs font-medium italic text-red-500 ${
|
||||||
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
|
snapshot.isDraggingOver ? "bg-red-500/100 text-white" : ""
|
||||||
} duration-200`}
|
} duration-200`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
@ -487,7 +537,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CalendarView />
|
<CalendarView addIssueToDate={addIssueToDate} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : type === "issue" ? (
|
) : type === "issue" ? (
|
||||||
@ -508,8 +558,8 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
title="Create a new issue"
|
title="Create a new issue"
|
||||||
description={
|
description={
|
||||||
<span>
|
<span>
|
||||||
Use <pre className="inline rounded bg-gray-200 px-2 py-1">C</pre> shortcut to
|
Use <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>{" "}
|
||||||
create a new issue
|
shortcut to create a new issue
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
Icon={PlusIcon}
|
Icon={PlusIcon}
|
||||||
|
@ -56,7 +56,7 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
@ -70,11 +70,11 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
|
||||||
Add Link
|
Add Link
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2 space-y-3">
|
<div className="mt-2 space-y-3">
|
||||||
|
@ -14,7 +14,7 @@ type Props = {
|
|||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
@ -36,7 +36,7 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{groupedByIssues && (
|
{groupedByIssues && (
|
||||||
<div className="flex flex-col space-y-5 bg-white">
|
<div>
|
||||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
{Object.keys(groupedByIssues).map((singleGroup) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
@ -44,7 +44,6 @@ import {
|
|||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
import { DIVIDER } from "@blueprintjs/core/lib/esm/common/classes";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -216,9 +215,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</a>
|
</a>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div className="border-b mx-6 border-gray-300 last:border-b-0">
|
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between gap-2 py-3"
|
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);
|
||||||
@ -226,24 +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}`}>
|
||||||
|
<div className="flex-grow cursor-pointer px-4 pt-2.5 md:py-2.5">
|
||||||
<a className="group relative flex items-center gap-2">
|
<a className="group relative flex items-center gap-2">
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipHeading="Issue ID"
|
tooltipHeading="Issue ID"
|
||||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||||
>
|
>
|
||||||
<span className="flex-shrink-0 text-xs text-gray-400">
|
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<span className="text-sm text-gray-800">{truncateText(issue.name, 50)}</span>
|
<span className="text-[0.825rem] text-brand-base">
|
||||||
|
{truncateText(issue.name, 50)}
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</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 px-3 py-1.5 text-xs 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>
|
||||||
)}
|
)}
|
||||||
@ -277,7 +279,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
{issue.label_details.map((label) => (
|
{issue.label_details.map((label) => (
|
||||||
<span
|
<span
|
||||||
key={label.id}
|
key={label.id}
|
||||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="h-1.5 w-1.5 rounded-full"
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
@ -309,20 +311,20 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.link && (
|
{properties.link && (
|
||||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
<div className="flex items-center gap-1 text-brand-secondary">
|
||||||
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
|
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
|
||||||
{issue.link_count}
|
{issue.link_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.attachment_count && (
|
{properties.attachment_count && (
|
||||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
<div className="flex items-center gap-1 text-brand-secondary">
|
||||||
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
|
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
|
||||||
{issue.attachment_count}
|
{issue.attachment_count}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -360,7 +362,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -37,7 +37,7 @@ type Props = {
|
|||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
@ -104,17 +104,17 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
|
|
||||||
switch (selectedGroup) {
|
switch (selectedGroup) {
|
||||||
case "state":
|
case "state":
|
||||||
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor);
|
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
||||||
break;
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||||
icon = (
|
icon = (
|
||||||
<span
|
<span
|
||||||
className="h-[18px] w-[18px] flex-shrink-0 rounded-full"
|
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||||
style={{ backgroundColor: labelColor }}
|
style={{ backgroundColor: labelColor }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -130,27 +130,23 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure key={groupTitle} as="div" defaultOpen>
|
<Disclosure as="div" defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="bg-white">
|
<div>
|
||||||
<div
|
<div className="flex items-center justify-between px-4 py-2.5">
|
||||||
className={`flex items-center justify-between bg-gray-100 px-5 py-3 ${
|
|
||||||
open ? "" : "rounded-[10px]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
{selectedGroup !== null && (
|
{selectedGroup !== null && (
|
||||||
<span className="flex items-center">{getGroupIcon()}</span>
|
<div className="flex items-center">{getGroupIcon()}</div>
|
||||||
)}
|
)}
|
||||||
{selectedGroup !== null ? (
|
{selectedGroup !== null ? (
|
||||||
<h2 className="text-base font-semibold capitalize leading-6 text-gray-800">
|
<h2 className="text-sm font-semibold capitalize leading-6 text-brand-base">
|
||||||
{getGroupTitle()}
|
{getGroupTitle()}
|
||||||
</h2>
|
</h2>
|
||||||
) : (
|
) : (
|
||||||
<h2 className="font-medium leading-5">All Issues</h2>
|
<h2 className="font-medium leading-5">All Issues</h2>
|
||||||
)}
|
)}
|
||||||
<span className="rounded-full bg-gray-200 py-0.5 px-3 text-sm text-black">
|
<span className="text-brand-2 min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs">
|
||||||
{groupedByIssues[groupTitle as keyof IIssue].length}
|
{groupedByIssues[groupTitle as keyof IIssue].length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -158,7 +154,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
{type === "issue" ? (
|
{type === "issue" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 text-gray-500 hover:bg-gray-100"
|
className="p-1 text-brand-secondary hover:bg-brand-surface-2"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToState}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@ -168,7 +164,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
customButton={
|
||||||
<div className="flex items-center cursor-pointer">
|
<div className="flex cursor-pointer items-center">
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -208,14 +204,16 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id);
|
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
|
||||||
}}
|
}}
|
||||||
isCompleted={isCompleted}
|
isCompleted={isCompleted}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
|
<p className="bg-brand-base px-4 py-2.5 text-sm text-brand-secondary">
|
||||||
|
No issues.
|
||||||
|
</p>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { ArrowTopRightOnSquareIcon, LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
import { ExternalLinkIcon } from "components/icons";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -33,15 +32,15 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
|
|||||||
<div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1">
|
<div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1">
|
||||||
<Link href={link.url}>
|
<Link href={link.url}>
|
||||||
<a
|
<a
|
||||||
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 outline-none"
|
className="grid h-7 w-7 place-items-center rounded bg-brand-surface-1 p-1 outline-none hover:bg-brand-surface-2"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon width="14" height="14" />
|
<ArrowTopRightOnSquareIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
|
className="grid h-7 w-7 place-items-center rounded bg-brand-surface-1 p-1 text-red-500 outline-none duration-300 hover:bg-red-500/20"
|
||||||
onClick={() => handleDeleteLink(link.id)}
|
onClick={() => handleDeleteLink(link.id)}
|
||||||
>
|
>
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
@ -49,13 +48,13 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link href={link.url}>
|
<Link href={link.url}>
|
||||||
<a className="relative flex gap-2 rounded-md border bg-gray-50 p-2" target="_blank">
|
<a className="relative flex gap-2 rounded-md bg-brand-base p-2" target="_blank">
|
||||||
<div className="mt-0.5">
|
<div className="mt-0.5">
|
||||||
<LinkIcon className="h-3.5 w-3.5" />
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="w-4/5 break-all">{link.title}</h5>
|
<h5 className="w-4/5 break-all">{link.title}</h5>
|
||||||
<p className="mt-0.5 text-gray-500">
|
<p className="mt-0.5 text-brand-secondary">
|
||||||
Added {timeAgo(link.created_at)}
|
Added {timeAgo(link.created_at)}
|
||||||
<br />
|
<br />
|
||||||
by{" "}
|
by{" "}
|
||||||
|
@ -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"
|
||||||
|
@ -78,7 +78,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
return 2;
|
return 2;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 3;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@ -94,19 +94,19 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
return setTab("States");
|
return setTab("States");
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return setTab("States");
|
return setTab("Assignees");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab.List
|
<Tab.List
|
||||||
as="div"
|
as="div"
|
||||||
className={`flex w-full items-center justify-between rounded-md bg-gray-100 px-1 py-1.5
|
className={`flex w-full items-center justify-between rounded-md bg-brand-surface-1 px-1 py-1.5
|
||||||
${module ? "text-xs" : "text-sm"} `}
|
${module ? "text-xs" : "text-sm"} `}
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`w-full rounded px-3 py-1 text-gray-900 ${
|
`w-full rounded px-3 py-1 text-brand-base ${
|
||||||
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
|
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -114,8 +114,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`w-full rounded px-3 py-1 text-gray-900 ${
|
`w-full rounded px-3 py-1 text-brand-base ${
|
||||||
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
|
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -123,8 +123,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`w-full rounded px-3 py-1 text-gray-900 ${
|
`w-full rounded px-3 py-1 text-brand-base ${
|
||||||
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
|
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -166,7 +166,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
<SingleProgressStats
|
<SingleProgressStats
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
<div className="h-5 w-5 rounded-full border-2 border-white bg-brand-surface-2">
|
||||||
<Image
|
<Image
|
||||||
src={User}
|
src={User}
|
||||||
height="100%"
|
height="100%"
|
||||||
|
@ -19,8 +19,8 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={`flex w-full items-center justify-between rounded p-2 text-xs ${
|
className={`flex w-full items-center justify-between rounded p-2 text-xs ${
|
||||||
onClick ? "cursor-pointer hover:bg-gray-100" : ""
|
onClick ? "cursor-pointer hover:bg-brand-surface-1" : ""
|
||||||
} ${selected ? "bg-gray-100" : ""}`}
|
} ${selected ? "bg-brand-surface-1" : ""}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="flex w-1/2 items-center justify-start gap-2">{title}</div>
|
<div className="flex w-1/2 items-center justify-start gap-2">{title}</div>
|
||||||
|
60
apps/app/components/core/theme-switch.tsx
Normal file
60
apps/app/components/core/theme-switch.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useState, useEffect, ChangeEvent } from "react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { THEMES_OBJ } from "constants/themes";
|
||||||
|
import { CustomSelect } from "components/ui";
|
||||||
|
import { CustomThemeModal } from "./custom-theme-modal";
|
||||||
|
|
||||||
|
export const ThemeSwitch = () => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [customThemeModal, setCustomThemeModal] = useState(false);
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
// useEffect only runs on the client, so now we can safely show the UI
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CustomSelect
|
||||||
|
value={theme}
|
||||||
|
label={theme ? THEMES_OBJ.find((t) => t.value === theme)?.label : "Select your theme"}
|
||||||
|
onChange={({ value, type }: { value: string; type: string }) => {
|
||||||
|
if (value === "custom") {
|
||||||
|
if (!customThemeModal) setCustomThemeModal(true);
|
||||||
|
} else {
|
||||||
|
const cssVars = [
|
||||||
|
"--color-bg-base",
|
||||||
|
"--color-bg-surface-1",
|
||||||
|
"--color-bg-surface-2",
|
||||||
|
|
||||||
|
"--color-border",
|
||||||
|
"--color-bg-sidebar",
|
||||||
|
"--color-accent",
|
||||||
|
|
||||||
|
"--color-text-base",
|
||||||
|
"--color-text-secondary",
|
||||||
|
];
|
||||||
|
cssVars.forEach((cssVar) => document.documentElement.style.removeProperty(cssVar));
|
||||||
|
}
|
||||||
|
document.documentElement.style.setProperty("color-scheme", type);
|
||||||
|
setTheme(value);
|
||||||
|
}}
|
||||||
|
input
|
||||||
|
width="w-full"
|
||||||
|
position="right"
|
||||||
|
>
|
||||||
|
{THEMES_OBJ.map(({ value, label, type }) => (
|
||||||
|
<CustomSelect.Option key={value} value={{ value, type }}>
|
||||||
|
{label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
{/* <CustomThemeModal isOpen={customThemeModal} handleClose={() => setCustomThemeModal(false)} /> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -64,7 +64,7 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
|||||||
{completedCycles ? (
|
{completedCycles ? (
|
||||||
completedCycles.completed_cycles.length > 0 ? (
|
completedCycles.completed_cycles.length > 0 ? (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
<div className="flex items-center gap-2 text-sm text-brand-secondary">
|
||||||
<ExclamationIcon height={14} width={14} />
|
<ExclamationIcon height={14} width={14} />
|
||||||
<span>Completed cycles are not editable.</span>
|
<span>Completed cycles are not editable.</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +25,6 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||||
const [showNoCurrentCycleMessage, setShowNoCurrentCycleMessage] = useState(true);
|
|
||||||
|
|
||||||
const handleDeleteCycle = (cycle: ICycle) => {
|
const handleDeleteCycle = (cycle: ICycle) => {
|
||||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||||
@ -61,14 +60,9 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : type === "current" ? (
|
) : type === "current" ? (
|
||||||
showNoCurrentCycleMessage && (
|
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
|
||||||
<div className="flex items-center justify-between bg-white w-full px-6 py-4 rounded-[10px]">
|
<h3 className="text-base font-medium text-brand-base ">No current cycle is present.</h3>
|
||||||
<h3 className="text-base font-medium text-black "> No current cycle is present.</h3>
|
|
||||||
<button onClick={() => setShowNoCurrentCycleMessage(false)}>
|
|
||||||
<XMarkIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
type="cycle"
|
type="cycle"
|
||||||
|
@ -139,7 +139,7 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
@ -153,30 +153,36 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
||||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<ExclamationTriangleIcon
|
<ExclamationTriangleIcon
|
||||||
className="h-6 w-6 text-red-600"
|
className="h-6 w-6 text-red-600"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-brand-base"
|
||||||
|
>
|
||||||
Delete Cycle
|
Delete Cycle
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-brand-secondary">
|
||||||
Are you sure you want to delete cycle-{" "}
|
Are you sure you want to delete cycle-{" "}
|
||||||
<span className="font-bold">{data?.name}</span>? All of the data related
|
<span className="break-all font-medium text-brand-base">
|
||||||
to the cycle will be permanently removed. This action cannot be undone.
|
{data?.name}
|
||||||
|
</span>
|
||||||
|
? All of the data related to the cycle will be permanently removed. This
|
||||||
|
action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6">
|
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||||
|
@ -37,30 +37,30 @@ export const EmptyCycle = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-5 ">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-5 ">
|
||||||
<div className="relative h-32 w-72">
|
<div className="relative h-32 w-72">
|
||||||
<div className="absolute right-0 top-0 flex w-64 flex-col rounded-[10px] bg-white text-xs shadow">
|
<div className="absolute right-0 top-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
||||||
<span className="text-sm font-semibold text-black">Cycle Name</span>
|
<span className="text-sm font-semibold text-brand-base">Cycle Name</span>
|
||||||
<div className="flex h-full w-full items-center gap-4">
|
<div className="flex h-full w-full items-center gap-4">
|
||||||
<span className="h-2 w-20 rounded-full bg-gray-200" />
|
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||||
<span className="h-2 w-20 rounded-full bg-gray-200" />
|
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 bg-gray-100 px-4 py-3">
|
<div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
|
||||||
<LinearProgressIndicator data={emptyCycleData} />
|
<LinearProgressIndicator data={emptyCycleData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute left-0 bottom-0 flex w-64 flex-col rounded-[10px] bg-white text-xs shadow">
|
<div className="absolute left-0 bottom-0 flex w-64 flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
|
||||||
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
<div className="flex flex-col items-start justify-center gap-2.5 p-3.5">
|
||||||
<span className="text-sm font-semibold text-black">Cycle Name</span>
|
<span className="text-sm font-semibold text-brand-base">Cycle Name</span>
|
||||||
<div className="flex h-full w-full items-center gap-4">
|
<div className="flex h-full w-full items-center gap-4">
|
||||||
<span className="h-2 w-20 rounded-full bg-gray-200" />
|
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||||
<span className="h-2 w-20 rounded-full bg-gray-200" />
|
<span className="h-2 w-20 rounded-full bg-brand-surface-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 bg-gray-100 px-4 py-3">
|
<div className="border-t border-brand-base bg-brand-surface-1 px-4 py-3">
|
||||||
<LinearProgressIndicator data={emptyCycleData} />
|
<LinearProgressIndicator data={emptyCycleData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,7 +68,7 @@ export const EmptyCycle = () => {
|
|||||||
|
|
||||||
<div className="flex flex-col items-center justify-center gap-4 text-center ">
|
<div className="flex flex-col items-center justify-center gap-4 text-center ">
|
||||||
<h3 className="text-xl font-semibold">Create New Cycle</h3>
|
<h3 className="text-xl font-semibold">Create New Cycle</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-brand-secondary">
|
||||||
Sprint more effectively with Cycles by confining your project <br /> to a fixed amount of
|
Sprint more effectively with Cycles by confining your project <br /> to a fixed amount of
|
||||||
time. Create new cycle now.
|
time. Create new cycle now.
|
||||||
</p>
|
</p>
|
||||||
|
@ -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,6 +64,7 @@ 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) => {
|
||||||
|
if (isDateGreaterThanToday(payload.end_date)) {
|
||||||
await cyclesService
|
await cyclesService
|
||||||
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
|
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -78,6 +83,14 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
setIsDateValid(false);
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Unable to create cycle in past date. Please enter a valid date.",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkEmptyDate =
|
const checkEmptyDate =
|
||||||
@ -94,13 +107,12 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleCreateUpdateCycle)}>
|
<form onSubmit={handleSubmit(handleCreateUpdateCycle)}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">
|
<h3 className="text-lg font-medium leading-6 text-brand-base">
|
||||||
{status ? "Update" : "Create"} Cycle
|
{status ? "Update" : "Create"} Cycle
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
mode="transparent"
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
@ -124,7 +136,6 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
name="description"
|
name="description"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
className="h-32 resize-none text-sm"
|
className="h-32 resize-none text-sm"
|
||||||
mode="transparent"
|
|
||||||
error={errors.description}
|
error={errors.description}
|
||||||
register={register}
|
register={register}
|
||||||
/>
|
/>
|
||||||
@ -153,7 +164,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "The date you have entered is invalid. Please check and enter a valid date.",
|
message:
|
||||||
|
"The date you have entered is invalid. Please check and enter a valid date.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,7 +196,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "The date you have entered is invalid. Please check and enter a valid date.",
|
message:
|
||||||
|
"The date you have entered is invalid. Please check and enter a valid date.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,7 +209,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t px-5 pt-5">
|
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-brand-base px-5 pt-5">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -207,7 +220,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
? "cursor-pointer"
|
? "cursor-pointer"
|
||||||
: "cursor-not-allowed"
|
: "cursor-not-allowed"
|
||||||
}
|
}
|
||||||
loading={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true}
|
disabled={checkEmptyDate ? false : isDateValid ? false : true}
|
||||||
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{status
|
{status
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
|
@ -151,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
@ -164,7 +164,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
<CycleForm
|
<CycleForm
|
||||||
handleFormSubmit={handleFormSubmit}
|
handleFormSubmit={handleFormSubmit}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
|
@ -59,9 +59,9 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={`flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
|
className={`flex cursor-pointer items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs shadow-sm duration-300 hover:bg-brand-surface-1 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
|
||||||
>
|
>
|
||||||
<CyclesIcon className="h-3 w-3 text-gray-500" />
|
<CyclesIcon className="h-3 w-3 text-brand-secondary" />
|
||||||
<div className="flex items-center gap-2 truncate">
|
<div className="flex items-center gap-2 truncate">
|
||||||
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
|
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +75,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
className={`absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-white shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none`}
|
className={`absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-brand-surface-2 shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none`}
|
||||||
>
|
>
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{options ? (
|
{options ? (
|
||||||
@ -93,7 +93,7 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
|
|||||||
: ""
|
: ""
|
||||||
} ${
|
} ${
|
||||||
active ? "bg-indigo-50" : ""
|
active ? "bg-indigo-50" : ""
|
||||||
} relative cursor-pointer select-none p-2 text-gray-900`
|
} relative cursor-pointer select-none p-2 text-brand-base`
|
||||||
}
|
}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
>
|
>
|
||||||
@ -103,14 +103,14 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
|
|||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-sm text-gray-500">No options</p>
|
<p className="text-center text-sm text-brand-secondary">No options</p>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-sm text-gray-500">Loading...</p>
|
<p className="text-center text-sm text-brand-secondary">Loading...</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="relative w-full flex select-none items-center gap-x-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-gray-900"
|
className="relative w-full flex select-none items-center gap-x-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-brand-base"
|
||||||
onClick={openCycleModal}
|
onClick={openCycleModal}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4 text-gray-400" aria-hidden="true" />
|
<PlusIcon className="h-4 w-4 text-gray-400" aria-hidden="true" />
|
||||||
|
@ -92,13 +92,8 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
|
|
||||||
cyclesService
|
cyclesService
|
||||||
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
|
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
|
||||||
.then((res) => {
|
.then(() => mutate(CYCLE_DETAILS(cycleId as string)))
|
||||||
console.log(res);
|
.catch((e) => console.log(e));
|
||||||
mutate(CYCLE_DETAILS(cycleId as string));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
@ -140,31 +135,31 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
<div
|
<div
|
||||||
className={`fixed top-0 ${
|
className={`fixed top-0 ${
|
||||||
isOpen ? "right-0" : "-right-[24rem]"
|
isOpen ? "right-0" : "-right-[24rem]"
|
||||||
} z-20 h-full w-[24rem] overflow-y-auto border-l bg-gray-50 py-5 duration-300`}
|
} z-20 h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-sidebar py-5 duration-300`}
|
||||||
>
|
>
|
||||||
{cycle ? (
|
{cycle ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-start justify-center">
|
<div className="flex flex-col items-start justify-center">
|
||||||
<div className="flex gap-2.5 px-5 text-sm">
|
<div className="flex gap-2.5 px-5 text-sm">
|
||||||
<div className="flex items-center ">
|
<div className="flex items-center">
|
||||||
<span
|
<span className="flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-center text-xs capitalize">
|
||||||
className={`flex items-center rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-center text-sm capitalize text-gray-800 `}
|
|
||||||
>
|
|
||||||
{capitalizeFirstLetter(cycleStatus)}
|
{capitalizeFirstLetter(cycleStatus)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-gray-800">
|
<div className="relative flex h-full w-52 items-center gap-2">
|
||||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
disabled={isCompleted ?? false}
|
disabled={isCompleted ?? false}
|
||||||
className={`group flex h-full items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${
|
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
|
||||||
open ? "bg-gray-100" : ""
|
cycle.start_date ? "" : "text-brand-secondary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-3 w-3" />
|
<CalendarDaysIcon className="h-3 w-3" />
|
||||||
<span>{renderShortDate(new Date(`${cycle?.start_date}`))}</span>
|
<span>
|
||||||
|
{renderShortDate(new Date(`${cycle?.start_date}`), "Start date")}
|
||||||
|
</span>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -214,20 +209,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
<span>
|
<span>
|
||||||
<ArrowLongRightIcon className="h-3 w-3" />
|
<ArrowLongRightIcon className="h-3 w-3 text-brand-secondary" />
|
||||||
</span>
|
</span>
|
||||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
disabled={isCompleted ?? false}
|
disabled={isCompleted ?? false}
|
||||||
className={`group flex items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${
|
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
|
||||||
open ? "bg-gray-100" : ""
|
cycle.end_date ? "" : "text-brand-secondary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-3 w-3 " />
|
<CalendarDaysIcon className="h-3 w-3" />
|
||||||
|
|
||||||
<span>{renderShortDate(new Date(`${cycle?.end_date}`))}</span>
|
<span>{renderShortDate(new Date(`${cycle?.end_date}`), "End date")}</span>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -280,10 +275,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-6 px-6 py-6 w-full">
|
<div className="flex w-full flex-col gap-6 px-6 py-6">
|
||||||
<div className="flex flex-col items-start justify-start gap-2 w-full">
|
<div className="flex w-full flex-col items-start justify-start gap-2">
|
||||||
<div className="flex items-start justify-between gap-2 w-full">
|
<div className="flex w-full items-start justify-between gap-2">
|
||||||
<h4 className="text-xl font-semibold text-gray-900">{cycle.name}</h4>
|
<h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4>
|
||||||
<CustomMenu width="lg" ellipsis>
|
<CustomMenu width="lg" ellipsis>
|
||||||
{!isCompleted && (
|
{!isCompleted && (
|
||||||
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
||||||
@ -302,15 +297,15 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="whitespace-normal text-sm leading-5 text-black">
|
<span className="whitespace-normal text-sm leading-5 text-brand-secondary">
|
||||||
{cycle.description}
|
{cycle.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 text-sm">
|
<div className="flex flex-col gap-4 text-sm">
|
||||||
<div className="flex items-center justify-start gap-1">
|
<div className="flex items-center justify-start gap-1">
|
||||||
<div className="flex w-40 items-center justify-start gap-2">
|
<div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
|
||||||
<UserCircleIcon className="h-5 w-5 text-gray-400" />
|
<UserCircleIcon className="h-5 w-5" />
|
||||||
<span>Lead</span>
|
<span>Lead</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -328,17 +323,17 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
{cycle.owned_by.first_name.charAt(0)}
|
{cycle.owned_by.first_name.charAt(0)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-gray-900">{cycle.owned_by.first_name}</span>
|
<span className="text-brand-secondary">{cycle.owned_by.first_name}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-start gap-1">
|
<div className="flex items-center justify-start gap-1">
|
||||||
<div className="flex w-40 items-center justify-start gap-2">
|
<div className="flex w-40 items-center justify-start gap-2 text-brand-secondary">
|
||||||
<ChartPieIcon className="h-5 w-5 text-gray-400" />
|
<ChartPieIcon className="h-5 w-5" />
|
||||||
<span>Progress</span>
|
<span>Progress</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5 text-gray-800">
|
<div className="flex items-center gap-2.5 text-brand-secondary">
|
||||||
<span className="h-4 w-4">
|
<span className="h-4 w-4">
|
||||||
<ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} />
|
<ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} />
|
||||||
</span>
|
</span>
|
||||||
@ -349,7 +344,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-6 py-6 ">
|
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-brand-base p-6">
|
||||||
<Disclosure defaultOpen>
|
<Disclosure defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div
|
<div
|
||||||
@ -357,7 +352,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-2 ">
|
<div className="flex w-full items-center justify-between gap-2 ">
|
||||||
<div className="flex items-center justify-start gap-2 text-sm">
|
<div className="flex items-center justify-start gap-2 text-sm">
|
||||||
<span className="font-medium text-gray-500">Progress</span>
|
<span className="font-medium text-brand-secondary">Progress</span>
|
||||||
{!open && progressPercentage ? (
|
{!open && progressPercentage ? (
|
||||||
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
|
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
|
||||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||||
@ -376,7 +371,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<ExclamationIcon height={14} width={14} />
|
<ExclamationIcon height={14} width={14} />
|
||||||
<span className="text-xs italic text-gray-500">
|
<span className="text-xs italic text-brand-secondary">
|
||||||
{cycleStatus === "upcoming"
|
{cycleStatus === "upcoming"
|
||||||
? "Cycle is yet to start."
|
? "Cycle is yet to start."
|
||||||
: "Invalid date. Please enter valid date."}
|
: "Invalid date. Please enter valid date."}
|
||||||
@ -391,7 +386,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
<div className="flex items-start justify-between gap-4 py-2 text-xs">
|
<div className="flex items-start justify-between gap-4 py-2 text-xs">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span>
|
<span>
|
||||||
<DocumentIcon className="h-3 w-3 text-gray-500" />
|
<DocumentIcon className="h-3 w-3 text-brand-secondary" />
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Pending Issues -{" "}
|
Pending Issues -{" "}
|
||||||
@ -400,7 +395,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-gray-900">
|
<div className="flex items-center gap-3 text-brand-base">
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
||||||
<span>Ideal</span>
|
<span>Ideal</span>
|
||||||
@ -429,7 +424,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</Disclosure>
|
</Disclosure>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-6 py-6 ">
|
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-brand-base p-6">
|
||||||
<Disclosure defaultOpen>
|
<Disclosure defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div
|
<div
|
||||||
@ -437,7 +432,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
<div className="flex items-center justify-start gap-2 text-sm">
|
<div className="flex items-center justify-start gap-2 text-sm">
|
||||||
<span className="font-medium text-gray-500">Other Information</span>
|
<span className="font-medium text-brand-secondary">Other Information</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cycle.total_issues > 0 ? (
|
{cycle.total_issues > 0 ? (
|
||||||
@ -450,7 +445,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<ExclamationIcon height={14} width={14} />
|
<ExclamationIcon height={14} width={14} />
|
||||||
<span className="text-xs italic text-gray-500">
|
<span className="text-xs italic text-brand-secondary">
|
||||||
No issues found. Please add issue.
|
No issues found. Please add issue.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -238,7 +238,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col rounded-[10px] bg-white text-xs shadow">
|
<div className="flex flex-col rounded-[10px] bg-brand-base text-xs shadow">
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||||
<a className="w-full">
|
<a className="w-full">
|
||||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||||
@ -269,20 +269,20 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-start gap-5">
|
<div className="flex items-center justify-start gap-5 text-brand-secondary">
|
||||||
<div className="flex items-start gap-1 ">
|
<div className="flex items-start gap-1 ">
|
||||||
<CalendarDaysIcon className="h-4 w-4 text-gray-900" />
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
<span className="text-gray-400">Start :</span>
|
<span>Start :</span>
|
||||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-1 ">
|
<div className="flex items-start gap-1 ">
|
||||||
<TargetIcon className="h-4 w-4 text-gray-900" />
|
<TargetIcon className="h-4 w-4" />
|
||||||
<span className="text-gray-400">End :</span>
|
<span>End :</span>
|
||||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-4">
|
<div className="mt-4 flex items-center justify-between text-brand-secondary">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||||
<Image
|
<Image
|
||||||
@ -293,11 +293,11 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
|||||||
alt={cycle.owned_by.first_name}
|
alt={cycle.owned_by.first_name}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
|
<span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full capitalize">
|
||||||
{cycle.owned_by.first_name.charAt(0)}
|
{cycle.owned_by.first_name.charAt(0)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-gray-900">{cycle.owned_by.first_name}</span>
|
<span>{cycle.owned_by.first_name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{!isCompleted && (
|
{!isCompleted && (
|
||||||
@ -306,7 +306,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleEditCycle();
|
handleEditCycle();
|
||||||
}}
|
}}
|
||||||
className="flex cursor-pointer items-center rounded p-1 duration-300 hover:bg-gray-100"
|
className="flex cursor-pointer items-center rounded p-1 duration-300 hover:bg-brand-surface-1"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-4 w-4" />
|
||||||
@ -350,7 +350,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
|||||||
<Disclosure>
|
<Disclosure>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div
|
<div
|
||||||
className={`flex h-full w-full flex-col border-t border-gray-200 bg-gray-100 ${
|
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-brand-base bg-brand-surface-2 text-brand-secondary ${
|
||||||
open ? "" : "flex-row"
|
open ? "" : "flex-row"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -368,7 +368,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="overflow-hidden rounded-b-md bg-white py-3 shadow">
|
<div className="overflow-hidden rounded-b-md bg-brand-surface-2 py-3 shadow">
|
||||||
<div className="col-span-2 space-y-3 px-4">
|
<div className="col-span-2 space-y-3 px-4">
|
||||||
<div className="space-y-3 text-xs">
|
<div className="space-y-3 text-xs">
|
||||||
{stateGroups.map((group) => (
|
{stateGroups.map((group) => (
|
||||||
@ -388,7 +388,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
{cycle[group.key as keyof ICycle] as number}{" "}
|
{cycle[group.key as keyof ICycle] as number}{" "}
|
||||||
<span className="text-gray-500">
|
<span className="text-brand-secondary">
|
||||||
-{" "}
|
-{" "}
|
||||||
{cycle.total_issues > 0
|
{cycle.total_issues > 0
|
||||||
? `${Math.round(
|
? `${Math.round(
|
||||||
|
@ -89,7 +89,7 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10">
|
<div className="fixed inset-0 z-10">
|
||||||
@ -103,33 +103,33 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-white py-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
<Dialog.Panel className="relative transform rounded-lg bg-brand-surface-1 py-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between px-5">
|
<div className="flex items-center justify-between px-5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<TransferIcon className="h-4 w-5" color="#495057" />
|
<TransferIcon className="h-4 w-5" color="#495057" />
|
||||||
<h4 className="text-gray-700 font-medium text-[1.50rem]">Transfer Issues</h4>
|
<h4 className="text-xl font-medium text-brand-base">Transfer Issues</h4>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleClose}>
|
<button onClick={handleClose}>
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 pb-3 mt-2 px-5 border-b border-gray-200">
|
<div className="flex items-center gap-2 border-b border-brand-base px-5 pb-3">
|
||||||
<MagnifyingGlassIcon className="h-4 w-4 text-gray-500" />
|
<MagnifyingGlassIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
<input
|
<input
|
||||||
className="outline-none"
|
className="bg-brand-surface-1 outline-none"
|
||||||
placeholder="Search for a cycle..."
|
placeholder="Search for a cycle..."
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
value={query}
|
value={query}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start w-full gap-2 px-5">
|
<div className="flex w-full flex-col items-start gap-2 px-5">
|
||||||
{filteredOptions ? (
|
{filteredOptions ? (
|
||||||
filteredOptions.length > 0 ? (
|
filteredOptions.length > 0 ? (
|
||||||
filteredOptions.map((option: ICycle) => (
|
filteredOptions.map((option: ICycle) => (
|
||||||
<button
|
<button
|
||||||
key={option.id}
|
key={option.id}
|
||||||
className="flex items-center gap-4 py-3 px-2 text-gray-600 text-sm rounded w-full hover:bg-gray-100"
|
className="flex w-full items-center gap-4 rounded px-4 py-3 text-sm text-brand-secondary hover:bg-brand-surface-1"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
transferIssue({
|
transferIssue({
|
||||||
new_cycle_id: option?.id,
|
new_cycle_id: option?.id,
|
||||||
@ -138,25 +138,25 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContrastIcon className="h-5 w-5" />
|
<ContrastIcon className="h-5 w-5" />
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex w-full justify-between">
|
||||||
<span>{option?.name}</span>
|
<span>{option?.name}</span>
|
||||||
<span className=" flex bg-gray-200 capitalize px-2 rounded-full items-center">
|
<span className=" flex items-center rounded-full bg-brand-surface-2 px-2 capitalize">
|
||||||
{getDateRangeStatus(option?.start_date, option?.end_date)}
|
{getDateRangeStatus(option?.start_date, option?.end_date)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center gap-4 p-5 text-sm w-full">
|
<div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
|
||||||
<ExclamationIcon height={14} width={14} />
|
<ExclamationIcon height={14} width={14} />
|
||||||
<span className="text-center text-gray-500">
|
<span className="text-center text-brand-secondary">
|
||||||
You don’t have any current cycle. Please create one to transfer the
|
You don’t have any current cycle. Please create one to transfer the
|
||||||
issues.
|
issues.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-gray-500">Loading...</p>
|
<p className="text-center text-brand-secondary">Loading...</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,8 +37,8 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
|
|||||||
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
|
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
|
||||||
: 0;
|
: 0;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between -mt-4 mb-4">
|
<div className="-mt-2 mb-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
<div className="flex items-center gap-2 text-sm text-brand-secondary">
|
||||||
<ExclamationIcon height={14} width={14} />
|
<ExclamationIcon height={14} width={14} />
|
||||||
<span>Completed cycles are not editable.</span>
|
<span>Completed cycles are not editable.</span>
|
||||||
</div>
|
</div>
|
||||||
@ -46,7 +46,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
|
|||||||
{transferableIssuesCount > 0 && (
|
{transferableIssuesCount > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<PrimaryButton onClick={handleClick} className="flex items-center gap-3 rounded-lg">
|
<PrimaryButton onClick={handleClick} className="flex items-center gap-3 rounded-lg">
|
||||||
<TransferIcon className="h-4 w-4" color="white"/>
|
<TransferIcon className="h-4 w-4" color="white" />
|
||||||
<span className="text-white">Transfer Issues</span>
|
<span className="text-white">Transfer Issues</span>
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [openColorPicker, setOpenColorPicker] = useState(false);
|
const [openColorPicker, setOpenColorPicker] = useState(false);
|
||||||
const [activeColor, setActiveColor] = useState<string>("#020617");
|
const [activeColor, setActiveColor] = useState<string>("#858e96");
|
||||||
|
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<Popover className="relative z-[1]" ref={ref}>
|
<Popover className="relative z-[1]" ref={ref}>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="rounded-full bg-gray-100 p-2 outline-none sm:text-sm"
|
className="rounded-full bg-brand-surface-1 p-2 outline-none sm:text-sm"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@ -69,8 +69,8 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] bg-white shadow-lg">
|
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-brand-base bg-brand-surface-2 shadow-lg">
|
||||||
<div className="h-[230px] w-[250px] overflow-auto border rounded-[4px] bg-white p-2 shadow-xl">
|
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-brand-base bg-brand-surface-2 p-2 shadow-xl">
|
||||||
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
||||||
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
|
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
@ -82,7 +82,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
setOpenColorPicker(false);
|
setOpenColorPicker(false);
|
||||||
}}
|
}}
|
||||||
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
|
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
|
||||||
selected ? "border-theme text-theme" : "border-transparent text-gray-500"
|
selected ? "" : "border-transparent text-brand-secondary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
@ -95,12 +95,12 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
{recentEmojis.length > 0 && (
|
{recentEmojis.length > 0 && (
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<h3 className="mb-2 ml-1 text-xs text-gray-400">Recent</h3>
|
<h3 className="mb-2 text-xs text-brand-secondary">Recent</h3>
|
||||||
<div className="grid grid-cols-8 gap-2">
|
<div className="grid grid-cols-8 gap-2">
|
||||||
{recentEmojis.map((emoji) => (
|
{recentEmojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-4 w-4 select-none text-base hover:bg-hover-gray flex items-center justify-between"
|
className="flex h-4 w-4 select-none items-center justify-between text-sm"
|
||||||
key={emoji}
|
key={emoji}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(emoji);
|
onChange(emoji);
|
||||||
@ -113,13 +113,13 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<hr className="w-full h-[1px] mb-2" />
|
<hr className="mb-2 h-[1px] w-full border-brand-base" />
|
||||||
<div>
|
<div>
|
||||||
<div className="grid grid-cols-8 gap-x-2 gap-y-3">
|
<div className="grid grid-cols-8 gap-x-2 gap-y-3">
|
||||||
{emojis.map((emoji) => (
|
{emojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-4 w-4 ml-1 select-none text-base hover:bg-hover-gray flex justify-center items-center"
|
className="mb-1 flex h-4 w-4 select-none items-center text-sm"
|
||||||
key={emoji}
|
key={emoji}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(emoji);
|
onChange(emoji);
|
||||||
@ -136,7 +136,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Tab.Panel className="flex h-full w-full flex-col justify-center">
|
<Tab.Panel className="flex h-full w-full flex-col justify-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="pb-2 px-1 flex items-center justify-between">
|
<div className="flex items-center justify-between px-1 pb-2">
|
||||||
{[
|
{[
|
||||||
"#FF6B00",
|
"#FF6B00",
|
||||||
"#8CC1FF",
|
"#8CC1FF",
|
||||||
@ -147,7 +147,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
"#000000",
|
"#000000",
|
||||||
].map((curCol) => (
|
].map((curCol) => (
|
||||||
<span
|
<span
|
||||||
className="w-4 h-4 rounded-full cursor-pointer"
|
className="h-4 w-4 cursor-pointer rounded-full"
|
||||||
style={{ backgroundColor: curCol }}
|
style={{ backgroundColor: curCol }}
|
||||||
onClick={() => setActiveColor(curCol)}
|
onClick={() => setActiveColor(curCol)}
|
||||||
/>
|
/>
|
||||||
@ -158,14 +158,14 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="w-4 h-4 rounded-full conical-gradient"
|
className="conical-gradient h-4 w-4 rounded-full"
|
||||||
style={{ backgroundColor: activeColor }}
|
style={{ backgroundColor: activeColor }}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<TwitterPicker
|
<TwitterPicker
|
||||||
className={`m-2 !absolute top-4 left-4 z-10 ${
|
className={`!absolute top-4 left-4 z-10 m-2 ${
|
||||||
openColorPicker ? "block" : "hidden"
|
openColorPicker ? "block" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
color={activeColor}
|
color={activeColor}
|
||||||
@ -178,13 +178,12 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="w-full h-[1px] mb-1" />
|
<hr className="mb-1 h-[1px] w-full border-brand-base" />
|
||||||
|
<div className="mt-1 ml-1 grid grid-cols-8 gap-x-2 gap-y-3">
|
||||||
<div className="grid grid-cols-8 mt-1 ml-1 gap-x-2 gap-y-3">
|
|
||||||
{icons.material_rounded.map((icon) => (
|
{icons.material_rounded.map((icon) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-4 w-4 mb-1 select-none text-lg hover:bg-hover-gray flex items-center"
|
className="mb-1 flex h-4 w-4 select-none items-center text-lg"
|
||||||
key={icon.name}
|
key={icon.name}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onIconsClick) onIconsClick(icon.name);
|
if (onIconsClick) onIconsClick(icon.name);
|
||||||
|
@ -6,19 +6,20 @@ import { mutate } from "swr";
|
|||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import estimatesService from "services/estimates.service";
|
import estimatesService from "services/estimates.service";
|
||||||
// ui
|
|
||||||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { checkDuplicates } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { IEstimate } from "types";
|
import { IEstimate, IEstimateFormData } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ESTIMATES_LIST } from "constants/fetch-keys";
|
import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -26,66 +27,90 @@ type Props = {
|
|||||||
data?: IEstimate;
|
data?: IEstimate;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IEstimate> = {
|
type FormValues = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
value1: string;
|
||||||
|
value2: string;
|
||||||
|
value3: string;
|
||||||
|
value4: string;
|
||||||
|
value5: string;
|
||||||
|
value6: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: Partial<FormValues> = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
value1: "",
|
||||||
|
value2: "",
|
||||||
|
value3: "",
|
||||||
|
value4: "",
|
||||||
|
value5: "",
|
||||||
|
value6: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
|
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
} = useForm<IEstimate>({
|
} = useForm<FormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const createEstimate = async (formData: IEstimate) => {
|
const createEstimate = async (payload: IEstimateFormData) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload = {
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
};
|
|
||||||
|
|
||||||
await estimatesService
|
await estimatesService
|
||||||
.createEstimate(workspaceSlug as string, projectId as string, payload)
|
.createEstimate(workspaceSlug as string, projectId as string, payload)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
mutate<IEstimate[]>(
|
mutate(ESTIMATES_LIST(projectId as string));
|
||||||
ESTIMATES_LIST(projectId as string),
|
onClose();
|
||||||
(prevData) => [res, ...(prevData ?? [])],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
handleClose();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
|
if (err.status === 400)
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Error: Estimate could not be created",
|
message: "Estimate with that name already exists. Please try again with another name.",
|
||||||
|
});
|
||||||
|
else
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Estimate could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEstimate = async (formData: IEstimate) => {
|
const updateEstimate = async (payload: IEstimateFormData) => {
|
||||||
if (!workspaceSlug || !projectId || !data) return;
|
if (!workspaceSlug || !projectId || !data) return;
|
||||||
|
|
||||||
const payload = {
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
};
|
|
||||||
|
|
||||||
mutate<IEstimate[]>(
|
mutate<IEstimate[]>(
|
||||||
ESTIMATES_LIST(projectId as string),
|
ESTIMATES_LIST(projectId.toString()),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
prevData?.map((p) => {
|
prevData?.map((p) => {
|
||||||
if (p.id === data.id) return { ...p, ...payload };
|
if (p.id === data.id)
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
name: payload.estimate.name,
|
||||||
|
description: payload.estimate.description,
|
||||||
|
points: p.points.map((point, index) => ({
|
||||||
|
...point,
|
||||||
|
value: payload.estimate_points[index].value,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}),
|
}),
|
||||||
@ -95,23 +120,104 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||||||
await estimatesService
|
await estimatesService
|
||||||
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload)
|
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
mutate(ESTIMATES_LIST(projectId.toString()));
|
||||||
|
mutate(ESTIMATE_DETAILS(data.id));
|
||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Error: Estimate could not be updated",
|
message: "Estimate could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
handleClose();
|
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (formData: FormValues) => {
|
||||||
|
if (!formData.name || formData.name === "") {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Estimate title cannot be empty.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
formData.value1 === "" ||
|
||||||
|
formData.value2 === "" ||
|
||||||
|
formData.value3 === "" ||
|
||||||
|
formData.value4 === "" ||
|
||||||
|
formData.value5 === "" ||
|
||||||
|
formData.value6 === ""
|
||||||
|
) {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Estimate point cannot be empty.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
checkDuplicates([
|
||||||
|
formData.value1,
|
||||||
|
formData.value2,
|
||||||
|
formData.value3,
|
||||||
|
formData.value4,
|
||||||
|
formData.value5,
|
||||||
|
formData.value6,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Estimate points cannot have duplicate values.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: IEstimateFormData = {
|
||||||
|
estimate: {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
},
|
||||||
|
estimate_points: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const point = {
|
||||||
|
key: i,
|
||||||
|
value: formData[`value${i + 1}` as keyof FormValues],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data)
|
||||||
|
payload.estimate_points.push({
|
||||||
|
id: data.points[i].id,
|
||||||
|
...point,
|
||||||
|
});
|
||||||
|
else payload.estimate_points.push({ ...point });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) await updateEstimate(payload);
|
||||||
|
else await createEstimate(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (data)
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
...data,
|
...data,
|
||||||
|
value1: data.points[0]?.value,
|
||||||
|
value2: data.points[1]?.value,
|
||||||
|
value3: data.points[2]?.value,
|
||||||
|
value4: data.points[3]?.value,
|
||||||
|
value5: data.points[4]?.value,
|
||||||
|
value6: data.points[5]?.value,
|
||||||
});
|
});
|
||||||
|
else reset({ ...defaultValues });
|
||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -127,7 +233,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
@ -141,10 +247,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
<form
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
onSubmit={data ? handleSubmit(updateEstimate) : handleSubmit(createEstimate)}
|
|
||||||
>
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-lg font-medium leading-6">
|
<div className="text-lg font-medium leading-6">
|
||||||
{data ? "Update" : "Create"} Estimate
|
{data ? "Update" : "Create"} Estimate
|
||||||
@ -156,17 +260,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||||||
type="name"
|
type="name"
|
||||||
placeholder="Title"
|
placeholder="Title"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
mode="transparent"
|
|
||||||
className="resize-none text-xl"
|
className="resize-none text-xl"
|
||||||
error={errors.name}
|
|
||||||
register={register}
|
register={register}
|
||||||
validations={{
|
|
||||||
required: "Title is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 255,
|
|
||||||
message: "Title should be less than 255 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -175,11 +270,107 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
|||||||
name="description"
|
name="description"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
className="h-32 resize-none text-sm"
|
className="h-32 resize-none text-sm"
|
||||||
mode="transparent"
|
|
||||||
error={errors.description}
|
|
||||||
register={register}
|
register={register}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||||
|
<span className="rounded-lg px-2 text-sm text-brand-secondary">1</span>
|
||||||
|
<span className="rounded-r-lg bg-brand-base">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="value1"
|
||||||
|
type="name"
|
||||||
|
className="rounded-l-none"
|
||||||
|
placeholder="Point 1"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||||
|
<span className="rounded-lg px-2 text-sm text-brand-secondary">2</span>
|
||||||
|
<span className="rounded-r-lg bg-brand-base">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="value2"
|
||||||
|
type="name"
|
||||||
|
className="rounded-l-none"
|
||||||
|
placeholder="Point 2"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||||
|
<span className="rounded-lg px-2 text-sm text-brand-secondary">3</span>
|
||||||
|
<span className="rounded-r-lg bg-brand-base">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="value3"
|
||||||
|
type="name"
|
||||||
|
className="rounded-l-none"
|
||||||
|
placeholder="Point 3"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||||
|
<span className="rounded-lg px-2 text-sm text-brand-secondary">4</span>
|
||||||
|
<span className="rounded-r-lg bg-brand-base">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="value4"
|
||||||
|
type="name"
|
||||||
|
className="rounded-l-none"
|
||||||
|
placeholder="Point 4"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||||
|
<span className="rounded-lg px-2 text-sm text-brand-secondary">5</span>
|
||||||
|
<span className="rounded-r-lg bg-brand-base">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="value5"
|
||||||
|
type="name"
|
||||||
|
className="rounded-l-none"
|
||||||
|
placeholder="Point 5"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||||
|
<span className="rounded-lg px-2 text-sm text-brand-secondary">6</span>
|
||||||
|
<span className="rounded-r-lg bg-brand-base">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="value6"
|
||||||
|
type="name"
|
||||||
|
className="rounded-l-none"
|
||||||
|
placeholder="Point 6"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
|
@ -46,7 +46,7 @@ export const DeleteEstimateModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
@ -60,10 +60,10 @@ export const DeleteEstimateModal: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||||
<div className="flex flex-col gap-6 p-6">
|
<div className="flex flex-col gap-6 p-6">
|
||||||
<div className="flex w-full items-center justify-start gap-6">
|
<div className="flex w-full items-center justify-start gap-6">
|
||||||
<span className="place-items-center rounded-full bg-red-100 p-4">
|
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||||
<ExclamationTriangleIcon
|
<ExclamationTriangleIcon
|
||||||
className="h-6 w-6 text-red-600"
|
className="h-6 w-6 text-red-600"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -74,9 +74,9 @@ export const DeleteEstimateModal: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
<p className="break-all text-sm leading-7 text-gray-500">
|
<p className="break-all text-sm leading-7 text-brand-secondary">
|
||||||
Are you sure you want to delete estimate-{" "}
|
Are you sure you want to delete estimate-{" "}
|
||||||
<span className="break-all font-semibold">{data.name}</span>
|
<span className="break-all font-medium text-brand-base">{data.name}</span>
|
||||||
{""}? All of the data related to the estiamte will be permanently removed.
|
{""}? All of the data related to the estiamte will be permanently removed.
|
||||||
This action cannot be undone.
|
This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,357 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import { mutate } from "swr";
|
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// ui
|
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
|
||||||
|
|
||||||
// icons
|
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
|
||||||
// types
|
|
||||||
import type { IEstimate, IEstimatePoint } from "types";
|
|
||||||
|
|
||||||
import estimatesService from "services/estimates.service";
|
|
||||||
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
data?: IEstimatePoint[];
|
|
||||||
estimate: IEstimate | null;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FormValues {
|
|
||||||
value1: string;
|
|
||||||
value2: string;
|
|
||||||
value3: string;
|
|
||||||
value4: string;
|
|
||||||
value5: string;
|
|
||||||
value6: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultValues: FormValues = {
|
|
||||||
value1: "",
|
|
||||||
value2: "",
|
|
||||||
value3: "",
|
|
||||||
value4: "",
|
|
||||||
value5: "",
|
|
||||||
value6: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, onClose }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
} = useForm<FormValues>({ defaultValues });
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onClose();
|
|
||||||
reset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createEstimatePoints = async (formData: FormValues) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
estimate_points: [
|
|
||||||
{
|
|
||||||
key: 0,
|
|
||||||
value: formData.value1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 1,
|
|
||||||
value: formData.value2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 2,
|
|
||||||
value: formData.value3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 3,
|
|
||||||
value: formData.value4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 4,
|
|
||||||
value: formData.value5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 5,
|
|
||||||
value: formData.value6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await estimatesService
|
|
||||||
.createEstimatePoints(
|
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
estimate?.id as string,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
handleClose();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Estimate points could not be created. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEstimatePoints = async (formData: FormValues) => {
|
|
||||||
if (!workspaceSlug || !projectId || !data || data.length === 0) return;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
estimate_points: data.map((d, index) => ({
|
|
||||||
id: d.id,
|
|
||||||
value: (formData as any)[`value${index + 1}`],
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
await estimatesService
|
|
||||||
.patchEstimatePoints(
|
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
estimate?.id as string,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
handleClose();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Estimate points could not be created. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (formData: FormValues) => {
|
|
||||||
if (data && data.length !== 0) await updateEstimatePoints(formData);
|
|
||||||
else await createEstimatePoints(formData);
|
|
||||||
|
|
||||||
if (estimate) mutate(ESTIMATE_POINTS_LIST(estimate.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data || data.length < 6) return;
|
|
||||||
|
|
||||||
reset({
|
|
||||||
...defaultValues,
|
|
||||||
value1: data[0].value,
|
|
||||||
value2: data[1].value,
|
|
||||||
value3: data[2].value,
|
|
||||||
value4: data[3].value,
|
|
||||||
value5: data[4].value,
|
|
||||||
value6: data[5].value,
|
|
||||||
});
|
|
||||||
}, [data, reset]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<h4 className="text-lg font-medium leading-6">
|
|
||||||
{data ? "Update" : "Create"} Estimate Points
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="bg-gray-100 h-full flex items-center rounded-lg">
|
|
||||||
<span className="px-2 rounded-lg text-sm text-gray-600">1</span>
|
|
||||||
<span className="bg-white rounded-lg">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="value1"
|
|
||||||
type="name"
|
|
||||||
placeholder="Value"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "value is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 10,
|
|
||||||
message: "Name should be less than 10 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="bg-gray-100 h-full flex items-center rounded-lg">
|
|
||||||
<span className="px-2 rounded-lg text-sm text-gray-600">2</span>
|
|
||||||
<span className="bg-white rounded-lg">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="value2"
|
|
||||||
type="name"
|
|
||||||
placeholder="Value"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "value is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 10,
|
|
||||||
message: "Name should be less than 10 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="bg-gray-100 h-full flex items-center rounded-lg">
|
|
||||||
<span className="px-2 rounded-lg text-sm text-gray-600">3</span>
|
|
||||||
<span className="bg-white rounded-lg">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="value3"
|
|
||||||
type="name"
|
|
||||||
placeholder="Value"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "value is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 10,
|
|
||||||
message: "Name should be less than 10 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="bg-gray-100 h-full flex items-center rounded-lg">
|
|
||||||
<span className="px-2 rounded-lg text-sm text-gray-600">4</span>
|
|
||||||
<span className="bg-white rounded-lg">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="value4"
|
|
||||||
type="name"
|
|
||||||
placeholder="Value"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "value is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 10,
|
|
||||||
message: "Name should be less than 10 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="bg-gray-100 h-full flex items-center rounded-lg">
|
|
||||||
<span className="px-2 rounded-lg text-sm text-gray-600">5</span>
|
|
||||||
<span className="bg-white rounded-lg">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="value5"
|
|
||||||
type="name"
|
|
||||||
placeholder="Value"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "value is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 10,
|
|
||||||
message: "Name should be less than 10 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="bg-gray-100 h-full flex items-center rounded-lg">
|
|
||||||
<span className="px-2 rounded-lg text-sm text-gray-600">6</span>
|
|
||||||
<span className="bg-white rounded-lg">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="value6"
|
|
||||||
type="name"
|
|
||||||
placeholder="Value"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "value is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 10,
|
|
||||||
message: "Name should be less than 10 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
|
||||||
<SecondaryButton onClick={() => handleClose()}>Cancel</SecondaryButton>
|
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
|
||||||
{data && data.length > 0
|
|
||||||
? isSubmitting
|
|
||||||
? "Updating Points..."
|
|
||||||
: "Update Points"
|
|
||||||
: isSubmitting
|
|
||||||
? "Creating Points..."
|
|
||||||
: "Create Points"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user