Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2a58bf04a | ||
|
|
c51407c85e | ||
|
|
3817511024 | ||
|
|
2950877767 | ||
|
|
3d6f2dd3dc |
+3
-1
@@ -1,8 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
// This tells ESLint to load the config from the package `eslint-config-custom`
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["app/"],
|
||||
rootDir: ["apps/*"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,19 +3,15 @@ DJANGO_SETTINGS_MODULE="plane.settings.production"
|
||||
DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane
|
||||
# Cache
|
||||
REDIS_URL=redis://redis:6379/
|
||||
# SMTP
|
||||
# SMPT
|
||||
EMAIL_HOST=""
|
||||
EMAIL_HOST_USER=""
|
||||
EMAIL_HOST_PASSWORD=""
|
||||
EMAIL_PORT="587"
|
||||
EMAIL_USE_TLS="1"
|
||||
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||
# AWS
|
||||
# AWS
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID=""
|
||||
AWS_SECRET_ACCESS_KEY=""
|
||||
AWS_S3_BUCKET_NAME=""
|
||||
AWS_S3_ENDPOINT_URL=""
|
||||
# FE
|
||||
WEB_URL="localhost/"
|
||||
# OAUTH
|
||||
@@ -25,4 +21,4 @@ DISABLE_COLLECTSTATIC=1
|
||||
DOCKERIZED=1
|
||||
# GPT Envs
|
||||
OPENAI_API_KEY=0
|
||||
GPT_ENGINE=0
|
||||
GPT_ENGINE=0
|
||||
@@ -68,4 +68,4 @@ from .importer import ImporterSerializer
|
||||
|
||||
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
|
||||
|
||||
from .estimate import EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer
|
||||
from .estimate import EstimateSerializer, EstimatePointSerializer
|
||||
|
||||
@@ -23,16 +23,3 @@ class EstimatePointSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class EstimateReadSerializer(BaseSerializer):
|
||||
points = EstimatePointSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Estimate
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"points",
|
||||
"name",
|
||||
"description",
|
||||
]
|
||||
|
||||
+50
-36
@@ -79,9 +79,10 @@ from plane.api.views import (
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
StateDeleteIssueCheckEndpoint,
|
||||
## End States
|
||||
# Estimates
|
||||
EstimateViewSet,
|
||||
EstimatePointViewSet,
|
||||
ProjectEstimatePointEndpoint,
|
||||
BulkEstimatePointEndpoint,
|
||||
## End Estimates
|
||||
@@ -145,9 +146,6 @@ from plane.api.views import (
|
||||
# Gpt
|
||||
GPTIntegrationEndpoint,
|
||||
## End Gpt
|
||||
# Release Notes
|
||||
ReleaseNotesEndpoint,
|
||||
## End Release Notes
|
||||
)
|
||||
|
||||
|
||||
@@ -508,40 +506,63 @@ urlpatterns = [
|
||||
),
|
||||
name="project-state",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
|
||||
StateDeleteIssueCheckEndpoint.as_view(),
|
||||
name="state-delete-check",
|
||||
),
|
||||
# End States ##
|
||||
# Estimates
|
||||
# States
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||
EstimateViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-estimates",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:pk>/",
|
||||
EstimateViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-estimates",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
|
||||
EstimatePointViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-estimate-points",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<uuid:pk>/",
|
||||
EstimatePointViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-estimates",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
|
||||
ProjectEstimatePointEndpoint.as_view(),
|
||||
name="project-estimate-points",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||
BulkEstimatePointEndpoint.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/bulk-estimate-points/",
|
||||
BulkEstimatePointEndpoint.as_view(),
|
||||
name="bulk-create-estimate-points",
|
||||
),
|
||||
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 ##
|
||||
# End States ##
|
||||
# Shortcuts
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
|
||||
@@ -1263,11 +1284,4 @@ urlpatterns = [
|
||||
name="importer",
|
||||
),
|
||||
## End Gpt
|
||||
# Release Notes
|
||||
path(
|
||||
"release-notes/",
|
||||
ReleaseNotesEndpoint.as_view(),
|
||||
name="release-notes",
|
||||
),
|
||||
## End Release Notes
|
||||
]
|
||||
|
||||
@@ -42,7 +42,7 @@ from .workspace import (
|
||||
UserWorkspaceDashboardEndpoint,
|
||||
WorkspaceThemeViewSet,
|
||||
)
|
||||
from .state import StateViewSet, StateDeleteIssueCheckEndpoint
|
||||
from .state import StateViewSet
|
||||
from .shortcut import ShortCutViewSet
|
||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
from .cycle import (
|
||||
@@ -133,9 +133,8 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||
from .gpt import GPTIntegrationEndpoint
|
||||
|
||||
from .estimate import (
|
||||
EstimateViewSet,
|
||||
EstimatePointViewSet,
|
||||
ProjectEstimatePointEndpoint,
|
||||
BulkEstimatePointEndpoint,
|
||||
)
|
||||
|
||||
|
||||
from .release import ReleaseNotesEndpoint
|
||||
|
||||
@@ -10,7 +10,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.exceptions import NotFound
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
# Module imports
|
||||
@@ -39,7 +39,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
|
||||
try:
|
||||
return self.model.objects.all()
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
@@ -10,11 +10,110 @@ from sentry_sdk import capture_exception
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Project, Estimate, EstimatePoint
|
||||
from plane.api.serializers import (
|
||||
EstimateSerializer,
|
||||
EstimatePointSerializer,
|
||||
EstimateReadSerializer,
|
||||
)
|
||||
from plane.api.serializers import EstimateSerializer, EstimatePointSerializer
|
||||
|
||||
|
||||
class EstimateViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
model = Estimate
|
||||
serializer_class = EstimateSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
|
||||
class EstimatePointViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
model = EstimatePoint
|
||||
serializer_class = EstimatePointSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.filter(estimate_id=self.kwargs.get("estimate_id"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
estimate_id=self.kwargs.get("estimate_id"),
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, estimate_id):
|
||||
try:
|
||||
serializer = EstimatePointSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(estimate_id=estimate_id, project_id=project_id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"error": "The estimate point is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
else:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, estimate_id, pk):
|
||||
try:
|
||||
estimate_point = EstimatePoint.objects.get(
|
||||
pk=pk,
|
||||
estimate_id=estimate_id,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
serializer = EstimatePointSerializer(
|
||||
estimate_point, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save(estimate_id=estimate_id, project_id=project_id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except EstimatePoint.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Estimate Point does not exist"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"error": "The estimate point value is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
else:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class ProjectEstimatePointEndpoint(BaseAPIView):
|
||||
@@ -42,35 +141,17 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
|
||||
class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
class BulkEstimatePointEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
model = Estimate
|
||||
serializer_class = EstimateSerializer
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
def post(self, request, slug, project_id, estimate_id):
|
||||
try:
|
||||
estimates = Estimate.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).prefetch_related("points")
|
||||
serializer = EstimateReadSerializer(estimates, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
estimate = Estimate.objects.get(
|
||||
pk=estimate_id, workspace__slug=slug, project=project_id
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
if not request.data.get("estimate", False):
|
||||
return Response(
|
||||
{"error": "Estimate is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
estimate_points = request.data.get("estimate_points", [])
|
||||
|
||||
if not len(estimate_points) or len(estimate_points) > 8:
|
||||
@@ -79,18 +160,6 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
|
||||
if not estimate_serializer.is_valid():
|
||||
return Response(
|
||||
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
try:
|
||||
estimate = estimate_serializer.save(project_id=project_id)
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{"errror": "Estimate with the name already exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
estimate_points = EstimatePoint.objects.bulk_create(
|
||||
[
|
||||
EstimatePoint(
|
||||
@@ -109,17 +178,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
estimate_point_serializer = EstimatePointSerializer(
|
||||
estimate_points, many=True
|
||||
)
|
||||
serializer = EstimatePointSerializer(estimate_points, many=True)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"estimate": estimate_serializer.data,
|
||||
"estimate_points": estimate_point_serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Estimate.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Estimate does not exist"},
|
||||
@@ -132,58 +193,14 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def retrieve(self, request, slug, project_id, estimate_id):
|
||||
def patch(self, request, slug, project_id, estimate_id):
|
||||
try:
|
||||
estimate = Estimate.objects.get(
|
||||
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = EstimateReadSerializer(estimate)
|
||||
return Response(
|
||||
serializer.data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Estimate.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, estimate_id):
|
||||
try:
|
||||
if not request.data.get("estimate", False):
|
||||
return Response(
|
||||
{"error": "Estimate is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not len(request.data.get("estimate_points", [])):
|
||||
return Response(
|
||||
{"error": "Estimate points are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
estimate = Estimate.objects.get(pk=estimate_id)
|
||||
|
||||
estimate_serializer = EstimateSerializer(
|
||||
estimate, data=request.data.get("estimate"), partial=True
|
||||
)
|
||||
if not estimate_serializer.is_valid():
|
||||
return Response(
|
||||
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
try:
|
||||
estimate = estimate_serializer.save()
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{"errror": "Estimate with the name already exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
estimate_points_data = request.data.get("estimate_points", [])
|
||||
|
||||
estimate_points = EstimatePoint.objects.filter(
|
||||
@@ -195,6 +212,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
estimate_id=estimate_id,
|
||||
)
|
||||
|
||||
print(estimate_points)
|
||||
updated_estimate_points = []
|
||||
for estimate_point in estimate_points:
|
||||
# Find the data for that estimate point
|
||||
@@ -203,50 +221,24 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
for point in estimate_points_data
|
||||
if point.get("id") == str(estimate_point.id)
|
||||
]
|
||||
print(estimate_point_data)
|
||||
if len(estimate_point_data):
|
||||
estimate_point.value = estimate_point_data[0].get(
|
||||
"value", estimate_point.value
|
||||
)
|
||||
updated_estimate_points.append(estimate_point)
|
||||
|
||||
try:
|
||||
EstimatePoint.objects.bulk_update(
|
||||
updated_estimate_points, ["value"], batch_size=10
|
||||
)
|
||||
except IntegrityError as e:
|
||||
return Response(
|
||||
{"error": "Values need to be unique for each key"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
|
||||
return Response(
|
||||
{
|
||||
"estimate": estimate_serializer.data,
|
||||
"estimate_points": estimate_point_serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
EstimatePoint.objects.bulk_update(
|
||||
updated_estimate_points, ["value"], batch_size=10
|
||||
)
|
||||
serializer = EstimatePointSerializer(estimate_points, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Estimate.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
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)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -104,7 +104,7 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# Python imports
|
||||
import json
|
||||
import random
|
||||
from itertools import chain
|
||||
from itertools import groupby, chain
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Prefetch, OuterRef, Func, F, Q, Count
|
||||
from django.db.models import Prefetch, OuterRef, Func, F, Q
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -47,7 +46,6 @@ from plane.db.models import (
|
||||
Label,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
State,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
@@ -592,31 +590,8 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
.prefetch_related("labels")
|
||||
)
|
||||
|
||||
state_distribution = (
|
||||
State.objects.filter(workspace__slug=slug, project_id=project_id)
|
||||
.annotate(
|
||||
state_count=Count(
|
||||
"state_issue",
|
||||
filter=Q(state_issue__parent_id=issue_id),
|
||||
)
|
||||
)
|
||||
.order_by("group")
|
||||
.values("group", "state_count")
|
||||
)
|
||||
|
||||
result = {item["group"]: item["state_count"] for item in state_distribution}
|
||||
|
||||
serializer = IssueLiteSerializer(
|
||||
sub_issues,
|
||||
many=True,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"sub_issues": serializer.data,
|
||||
"state_distribution": result,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
serializer = IssueLiteSerializer(sub_issues, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
|
||||
@@ -14,7 +14,7 @@ from rest_framework.permissions import AllowAny
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework import status
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# sso authentication
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests as google_auth_request
|
||||
@@ -48,7 +48,7 @@ def validate_google_token(token, client_id):
|
||||
}
|
||||
return data
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
raise exceptions.AuthenticationFailed("Error with Google connection.")
|
||||
|
||||
|
||||
@@ -305,7 +305,8 @@ class OauthEndpoint(BaseAPIView):
|
||||
)
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"error": "Something went wrong. Please try again later or contact the support team."
|
||||
|
||||
@@ -344,7 +344,7 @@ class RecentPagesEndpoint(BaseAPIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# Python imports
|
||||
from itertools import groupby
|
||||
|
||||
# Django imports
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
@@ -11,10 +8,10 @@ from sentry_sdk import capture_exception
|
||||
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
from . import BaseViewSet
|
||||
from plane.api.serializers import StateSerializer
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import State, Issue
|
||||
from plane.db.models import State
|
||||
|
||||
|
||||
class StateViewSet(BaseViewSet):
|
||||
@@ -39,25 +36,6 @@ class StateViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
serializer = StateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{"error": "State with the name already exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
try:
|
||||
state_dict = dict()
|
||||
@@ -88,37 +66,7 @@ class StateViewSet(BaseViewSet):
|
||||
{"error": "Default state cannot be deleted"}, status=False
|
||||
)
|
||||
|
||||
# Check for any issues in the state
|
||||
issue_exist = Issue.objects.filter(state=pk).exists()
|
||||
|
||||
if issue_exist:
|
||||
return Response(
|
||||
{
|
||||
"error": "The state is not empty, only empty states can be deleted"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
state.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except State.DoesNotExist:
|
||||
return Response({"error": "State does not exists"}, status=status.HTTP_404)
|
||||
|
||||
|
||||
class StateDeleteIssueCheckEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id, pk):
|
||||
try:
|
||||
issue_count = Issue.objects.filter(
|
||||
state=pk, workspace__slug=slug, project_id=project_id
|
||||
).count()
|
||||
return Response({"issue_count": issue_count}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -145,6 +145,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
@@ -332,6 +333,7 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
@@ -778,7 +780,7 @@ class WorkspaceThemeViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -21,7 +20,7 @@ def email_verification(first_name, email, token, current_site):
|
||||
realtivelink = "/request-email-verification/" + "?token=" + str(token)
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Verify your Email!"
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -19,7 +18,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/"
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Verify your Email!"
|
||||
|
||||
|
||||
@@ -947,5 +947,6 @@ def issue_activity(
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -15,7 +14,7 @@ def magic_link(email, key, token, current_site):
|
||||
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Login for Plane"
|
||||
|
||||
@@ -30,5 +29,6 @@ def magic_link(email, key, token, current_site):
|
||||
msg.send()
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -23,7 +22,7 @@ def project_invitation(email, project_id, token, current_site):
|
||||
relativelink = f"/project-member-invitation/{project_member_invite.id}"
|
||||
abs_url = "http://" + current_site + relativelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
|
||||
|
||||
@@ -50,5 +49,6 @@ def project_invitation(email, project_id, token, current_site):
|
||||
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
||||
|
||||
@@ -27,7 +27,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
)
|
||||
abs_url = "http://" + current_site + realtivelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ def send_welcome_email(sender, instance, created, **kwargs):
|
||||
if created and not instance.is_bot:
|
||||
first_name = instance.first_name.capitalize()
|
||||
to_email = instance.email
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
||||
subject = f"Welcome to Plane ✈️!"
|
||||
|
||||
|
||||
@@ -174,12 +174,11 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
# Host for sending e-mail.
|
||||
EMAIL_HOST = os.environ.get("EMAIL_HOST")
|
||||
# Port for sending e-mail.
|
||||
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
|
||||
EMAIL_PORT = 587
|
||||
# Optional SMTP authentication information for EMAIL_HOST.
|
||||
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1"
|
||||
EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>")
|
||||
EMAIL_USE_TLS = True
|
||||
|
||||
|
||||
SIMPLE_JWT = {
|
||||
@@ -211,4 +210,4 @@ SIMPLE_JWT = {
|
||||
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_ACCEPT_CONTENT = ['application/json']
|
||||
CELERY_ACCEPT_CONTENT = ['application/json']
|
||||
@@ -83,6 +83,3 @@ LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
||||
|
||||
CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL")
|
||||
CELERY_BROKER_URL = os.environ.get("REDIS_URL")
|
||||
|
||||
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
@@ -105,7 +105,7 @@ if (
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||
AWS_S3_ENDPOINT_URL = ""
|
||||
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
@@ -240,15 +240,7 @@ SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
|
||||
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
||||
|
||||
redis_url = os.environ.get("REDIS_URL")
|
||||
broker_url = (
|
||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
)
|
||||
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
|
||||
if DOCKERIZED:
|
||||
CELERY_BROKER_URL = REDIS_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
else:
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
@@ -203,6 +203,4 @@ redis_url = os.environ.get("REDIS_URL")
|
||||
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
CELERY_BROKER_URL = broker_url
|
||||
@@ -5,7 +5,6 @@ from urllib.parse import urlparse, parse_qs
|
||||
from datetime import datetime, timedelta
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_jwt_token():
|
||||
@@ -129,24 +128,3 @@ def get_github_repo_details(access_tokens_url, owner, repo):
|
||||
).json()
|
||||
|
||||
return open_issues, total_labels, collaborators
|
||||
|
||||
|
||||
def get_release_notes():
|
||||
token = settings.GITHUB_ACCESS_TOKEN
|
||||
|
||||
if token:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + str(token),
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
else:
|
||||
headers = {
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1"
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {"error": "Unable to render information from Github Repository"}
|
||||
|
||||
return response.json()
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY . .
|
||||
|
||||
# build
|
||||
RUN yarn build
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 plane
|
||||
RUN adduser --system --uid 1001 captain
|
||||
USER captain
|
||||
|
||||
COPY --from=builder /app/next.config.js .
|
||||
COPY --from=builder /app/package.json .
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
EXPOSE 3000
|
||||
@@ -1,45 +0,0 @@
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,267 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,282 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { SelectFilters } from "components/views";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ListBulletIcon,
|
||||
Squares2X2Icon,
|
||||
CalendarDaysIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { Properties } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
export const IssuesFilterView: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
} = useIssuesView();
|
||||
|
||||
const [properties, setProperties] = useIssuesProperties(
|
||||
workspaceSlug as string,
|
||||
projectId as string
|
||||
);
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
issueView === "list" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setIssueView("list")}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
issueView === "kanban" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setIssueView("kanban")}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||
issueView === "calendar" ? "bg-brand-surface-2" : ""
|
||||
}`}
|
||||
onClick={() => setIssueView("calendar")}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists) {
|
||||
setFilters(
|
||||
{
|
||||
...(filters ?? {}),
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
} else {
|
||||
setFilters(
|
||||
{
|
||||
...(filters ?? {}),
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center gap-2 rounded-md border border-brand-base bg-transparent px-3 py-1.5 text-xs hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none ${
|
||||
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
|
||||
}`}
|
||||
>
|
||||
View
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-brand-base">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{issueView !== "calendar" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) =>
|
||||
issueView === "kanban" && option.key === null ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Show empty states</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
showEmptyGroups ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showEmptyGroups}
|
||||
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
>
|
||||
<span className="sr-only">Show empty groups</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-brand-accent"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{issueView !== "calendar" && (
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-brand-accent bg-brand-accent text-brand-base"
|
||||
: "border-brand-base"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
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)} /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,407 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import estimatesService from "services/estimates.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
// helpers
|
||||
import { checkDuplicates } from "helpers/array.helper";
|
||||
// types
|
||||
import { IEstimate, IEstimateFormData } from "types";
|
||||
// fetch-keys
|
||||
import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data?: IEstimate;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
description: string;
|
||||
value1: string;
|
||||
value2: string;
|
||||
value3: string;
|
||||
value4: string;
|
||||
value5: string;
|
||||
value6: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<FormValues> = {
|
||||
name: "",
|
||||
description: "",
|
||||
value1: "",
|
||||
value2: "",
|
||||
value3: "",
|
||||
value4: "",
|
||||
value5: "",
|
||||
value6: "",
|
||||
};
|
||||
|
||||
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
|
||||
const {
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<FormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
reset();
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const createEstimate = async (payload: IEstimateFormData) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await estimatesService
|
||||
.createEstimate(workspaceSlug as string, projectId as string, payload)
|
||||
.then(() => {
|
||||
mutate(ESTIMATES_LIST(projectId as string));
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.status === 400)
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate with that name already exists. Please try again with another name.",
|
||||
});
|
||||
else
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateEstimate = async (payload: IEstimateFormData) => {
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
|
||||
mutate<IEstimate[]>(
|
||||
ESTIMATES_LIST(projectId.toString()),
|
||||
(prevData) =>
|
||||
prevData?.map((p) => {
|
||||
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;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
await estimatesService
|
||||
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload)
|
||||
.then(() => {
|
||||
mutate(ESTIMATES_LIST(projectId.toString()));
|
||||
mutate(ESTIMATE_DETAILS(data.id));
|
||||
handleClose();
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate could not be updated. Please try again.",
|
||||
});
|
||||
});
|
||||
|
||||
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: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (data) await updateEstimate(payload);
|
||||
else await createEstimate(payload);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data)
|
||||
reset({
|
||||
...defaultValues,
|
||||
...data,
|
||||
value1: data.points[0]?.value,
|
||||
value2: data.points[1]?.value,
|
||||
value3: data.points[2]?.value,
|
||||
value4: data.points[3]?.value,
|
||||
value5: data.points[4]?.value,
|
||||
value6: data.points[5]?.value,
|
||||
});
|
||||
else reset({ ...defaultValues });
|
||||
}, [data, reset]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium leading-6">
|
||||
{data ? "Update" : "Create"} Estimate
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="name"
|
||||
placeholder="Title"
|
||||
autoComplete="off"
|
||||
className="resize-none text-xl"
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
className="h-32 resize-none text-sm"
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||
<span className="rounded-lg px-2 text-sm text-brand-secondary">1</span>
|
||||
<span className="rounded-r-lg bg-brand-base">
|
||||
<Input
|
||||
id="name"
|
||||
name="value1"
|
||||
type="name"
|
||||
className="rounded-l-none"
|
||||
placeholder="Point 1"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||
<span className="rounded-lg px-2 text-sm text-brand-secondary">2</span>
|
||||
<span className="rounded-r-lg bg-brand-base">
|
||||
<Input
|
||||
id="name"
|
||||
name="value2"
|
||||
type="name"
|
||||
className="rounded-l-none"
|
||||
placeholder="Point 2"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||
<span className="rounded-lg px-2 text-sm text-brand-secondary">3</span>
|
||||
<span className="rounded-r-lg bg-brand-base">
|
||||
<Input
|
||||
id="name"
|
||||
name="value3"
|
||||
type="name"
|
||||
className="rounded-l-none"
|
||||
placeholder="Point 3"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||
<span className="rounded-lg px-2 text-sm text-brand-secondary">4</span>
|
||||
<span className="rounded-r-lg bg-brand-base">
|
||||
<Input
|
||||
id="name"
|
||||
name="value4"
|
||||
type="name"
|
||||
className="rounded-l-none"
|
||||
placeholder="Point 4"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||
<span className="rounded-lg px-2 text-sm text-brand-secondary">5</span>
|
||||
<span className="rounded-r-lg bg-brand-base">
|
||||
<Input
|
||||
id="name"
|
||||
name="value5"
|
||||
type="name"
|
||||
className="rounded-l-none"
|
||||
placeholder="Point 5"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
|
||||
<span className="rounded-lg px-2 text-sm text-brand-secondary">6</span>
|
||||
<span className="rounded-r-lg bg-brand-base">
|
||||
<Input
|
||||
id="name"
|
||||
name="value6"
|
||||
type="name"
|
||||
className="rounded-l-none"
|
||||
placeholder="Point 6"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating Estimate..."
|
||||
: "Update Estimate"
|
||||
: isSubmitting
|
||||
? "Creating Estimate..."
|
||||
: "Create Estimate"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { DeleteEstimateModal } from "components/estimates";
|
||||
// ui
|
||||
import { CustomMenu, SecondaryButton } from "components/ui";
|
||||
//icons
|
||||
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IEstimate } from "types";
|
||||
|
||||
type Props = {
|
||||
estimate: IEstimate;
|
||||
editEstimate: (estimate: IEstimate) => void;
|
||||
handleEstimateDelete: (estimateId: string) => void;
|
||||
};
|
||||
|
||||
export const SingleEstimate: React.FC<Props> = ({
|
||||
estimate,
|
||||
editEstimate,
|
||||
handleEstimateDelete,
|
||||
}) => {
|
||||
const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projectDetails, mutateProjectDetails } = useProjectDetails();
|
||||
|
||||
const handleUseEstimate = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const payload = {
|
||||
estimate: estimate.id,
|
||||
};
|
||||
|
||||
mutateProjectDetails((prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return { ...prevData, estimate: estimate.id };
|
||||
}, false);
|
||||
|
||||
await projectService
|
||||
.updateProject(workspaceSlug as string, projectId as string, payload)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Estimate points could not be used. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gap-2 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
|
||||
{estimate.name}
|
||||
{projectDetails?.estimate && projectDetails?.estimate === estimate.id && (
|
||||
<span className="rounded bg-green-500/20 px-2 py-0.5 text-xs capitalize text-green-500">
|
||||
In use
|
||||
</span>
|
||||
)}
|
||||
</h6>
|
||||
<p className="font-sm w-[40vw] truncate text-[14px] font-normal text-brand-secondary">
|
||||
{estimate.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{projectDetails?.estimate !== estimate.id && estimate.points.length > 0 && (
|
||||
<SecondaryButton onClick={handleUseEstimate} className="py-1">
|
||||
Use
|
||||
</SecondaryButton>
|
||||
)}
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
editEstimate(estimate);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-3.5 w-3.5" />
|
||||
<span>Edit estimate</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{projectDetails?.estimate !== estimate.id && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIsDeleteEstimateModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-3.5 w-3.5" />
|
||||
<span>Delete estimate</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
{estimate.points.length > 0 ? (
|
||||
<div className="flex text-xs text-brand-secondary">
|
||||
Estimate points (
|
||||
<span className="flex gap-1">
|
||||
{orderArrayBy(estimate.points, "key").map((point, index) => (
|
||||
<h6 key={point.id} className="text-brand-secondary">
|
||||
{point.value}
|
||||
{index !== estimate.points.length - 1 && ","}{" "}
|
||||
</h6>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-xs text-brand-secondary">No estimate points</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeleteEstimateModal
|
||||
isOpen={isDeleteEstimateModalOpen}
|
||||
handleClose={() => setIsDeleteEstimateModalOpen(false)}
|
||||
data={estimate}
|
||||
handleDelete={() => {
|
||||
handleEstimateDelete(estimate.id);
|
||||
setIsDeleteEstimateModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
// hooks
|
||||
import useIntegrationPopup from "hooks/use-integration-popup";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
// types
|
||||
import { IWorkspaceIntegration } from "types";
|
||||
|
||||
type Props = {
|
||||
workspaceIntegration: false | IWorkspaceIntegration | undefined;
|
||||
provider: string | undefined;
|
||||
};
|
||||
|
||||
export const GithubAuth: React.FC<Props> = ({ workspaceIntegration, provider }) => {
|
||||
const { startAuth, isConnecting } = useIntegrationPopup(provider);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{workspaceIntegration && workspaceIntegration?.id ? (
|
||||
<PrimaryButton disabled>Successfully Connected</PrimaryButton>
|
||||
) : (
|
||||
<PrimaryButton onClick={startAuth} loading={isConnecting}>
|
||||
{isConnecting ? "Connecting..." : "Connect"}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
// types
|
||||
import { IJiraImporterForm } from "types";
|
||||
|
||||
export const JiraConfirmImport: React.FC = () => {
|
||||
const { watch } = useFormContext<IJiraImporterForm>();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-2">
|
||||
<h3 className="text-lg font-semibold">Confirm</h3>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<p className="text-sm text-gray-500">Migrating</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_issues")}</h4>
|
||||
<p className="text-sm text-gray-500">Issues</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_states")}</h4>
|
||||
<p className="text-sm text-gray-500">States</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_modules")}</h4>
|
||||
<p className="text-sm text-gray-500">Modules</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_labels")}</h4>
|
||||
<p className="text-sm text-gray-500">Labels</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">
|
||||
{watch("data.users").filter((user) => user.import).length}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">User</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,178 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// next
|
||||
import Link from "next/link";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
|
||||
// components
|
||||
import { Input, CustomSelect } from "components/ui";
|
||||
|
||||
import { IJiraImporterForm } from "types";
|
||||
|
||||
export const JiraGetImportDetail: React.FC = () => {
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<IJiraImporterForm>();
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full space-y-8 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Jira Personal Access Token</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Get to know your access token by navigating to{" "}
|
||||
<Link href="https://id.atlassian.com/manage-profile/security/api-tokens">
|
||||
<a
|
||||
className="font-medium text-gray-600 hover:text-gray-900"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Atlassian Settings
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<Input
|
||||
id="metadata.api_token"
|
||||
name="metadata.api_token"
|
||||
placeholder="XXXXXXXX"
|
||||
validations={{
|
||||
required: "Please enter your personal access token.",
|
||||
}}
|
||||
register={register}
|
||||
error={errors.metadata?.api_token}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Jira Project Key</h3>
|
||||
<p className="text-sm text-gray-500">If XXX-123 is your issue, then enter XXX</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Input
|
||||
id="metadata.project_key"
|
||||
name="metadata.project_key"
|
||||
placeholder="LIN"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Please enter your project key.",
|
||||
}}
|
||||
error={errors.metadata?.project_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Jira Email Address</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Enter the Gmail account that you use in Jira account
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Input
|
||||
id="metadata.email"
|
||||
name="metadata.email"
|
||||
type="email"
|
||||
placeholder="name@company.com"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Please enter email address.",
|
||||
}}
|
||||
error={errors.metadata?.email}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Jira Installation or Cloud Host Name</h3>
|
||||
<p className="text-sm text-gray-500">Enter your companies cloud host name</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Input
|
||||
id="metadata.cloud_hostname"
|
||||
name="metadata.cloud_hostname"
|
||||
type="email"
|
||||
placeholder="my-company.atlassian.net"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Please enter your cloud host name.",
|
||||
}}
|
||||
error={errors.metadata?.cloud_hostname}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Import to project</h3>
|
||||
<p className="text-sm text-gray-500">Select which project you want to import to.</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="project_id"
|
||||
rules={{ required: "Please select a project." }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
input
|
||||
width="w-full"
|
||||
onChange={onChange}
|
||||
label={
|
||||
<span>
|
||||
{value && value !== ""
|
||||
? projects.find((p) => p.id === value)?.name
|
||||
: "Select Project"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{projects.length > 0 ? (
|
||||
projects.map((project) => (
|
||||
<CustomSelect.Option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</CustomSelect.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500">
|
||||
<p>You don{"'"}t have any project. Please create a project first.</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const event = new KeyboardEvent("keydown", { key: "p" });
|
||||
document.dispatchEvent(event);
|
||||
}}
|
||||
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 text-gray-500" />
|
||||
<span>Create new project</span>
|
||||
</button>
|
||||
</div>
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
import { FC } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-hook-form
|
||||
import { useFormContext, useFieldArray, Controller } from "react-hook-form";
|
||||
|
||||
// hooks
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
|
||||
// components
|
||||
import { ToggleSwitch, Input, CustomSelect, CustomSearchSelect, Avatar } from "components/ui";
|
||||
|
||||
import { IJiraImporterForm } from "types";
|
||||
|
||||
export const JiraImportUsers: FC = () => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<IJiraImporterForm>();
|
||||
|
||||
const { fields } = useFieldArray({
|
||||
control,
|
||||
name: "data.users",
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { workspaceMembers: members } = useWorkspaceMembers(workspaceSlug?.toString());
|
||||
|
||||
const options =
|
||||
members?.map((member) => ({
|
||||
value: member.member.email,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name + " (" + member.member.email + ")"
|
||||
: member.member.email}
|
||||
</div>
|
||||
),
|
||||
})) ?? [];
|
||||
|
||||
return (
|
||||
<div className="h-full w-full space-y-10 divide-y-2 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Users</h3>
|
||||
<p className="text-sm text-gray-500">Update, invite or choose not to invite assignee</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="data.invite_users"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch onChange={onChange} value={value} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{watch("data.invite_users") && (
|
||||
<div className="pt-6">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-1 text-gray-500">Name</div>
|
||||
<div className="col-span-1 text-gray-500">Import as</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{fields.map((user, index) => (
|
||||
<div className="grid grid-cols-3 gap-3" key={`${user.email}-${user.username}`}>
|
||||
<div className="col-span-1">
|
||||
<p>{user.username}</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`data.users.${index}.import`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
width="w-full"
|
||||
label={
|
||||
<span className="capitalize">
|
||||
{Boolean(value) ? value : ("Ignore" as any)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<CustomSelect.Option value="invite">Invite by email</CustomSelect.Option>
|
||||
<CustomSelect.Option value="map">Map to existing</CustomSelect.Option>
|
||||
<CustomSelect.Option value={false}>Do not import</CustomSelect.Option>
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
{watch(`data.users.${index}.import`) === "invite" && (
|
||||
<Input
|
||||
id={`data.users.${index}.email`}
|
||||
name={`data.users.${index}.email`}
|
||||
type="text"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
error={errors?.data?.users?.[index]?.email}
|
||||
/>
|
||||
)}
|
||||
{watch(`data.users.${index}.import`) === "map" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`data.users.${index}.email`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
input
|
||||
label={value !== "" ? value : "Select user from project"}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
export * from "./root";
|
||||
export * from "./give-details";
|
||||
export * from "./jira-project-detail";
|
||||
export * from "./import-users";
|
||||
export * from "./confirm-import";
|
||||
|
||||
import { IJiraImporterForm } from "types";
|
||||
|
||||
export type TJiraIntegrationSteps =
|
||||
| "import-configure"
|
||||
| "display-import-data"
|
||||
| "select-import-data"
|
||||
| "import-users"
|
||||
| "import-confirmation";
|
||||
|
||||
export interface IJiraIntegrationData {
|
||||
state: TJiraIntegrationSteps;
|
||||
}
|
||||
|
||||
export const jiraFormDefaultValues: IJiraImporterForm = {
|
||||
metadata: {
|
||||
cloud_hostname: "",
|
||||
api_token: "",
|
||||
project_key: "",
|
||||
email: "",
|
||||
},
|
||||
config: {
|
||||
epics_to_modules: false,
|
||||
},
|
||||
data: {
|
||||
users: [],
|
||||
invite_users: true,
|
||||
total_issues: 0,
|
||||
total_labels: 0,
|
||||
total_modules: 0,
|
||||
total_states: 0,
|
||||
},
|
||||
project_id: "",
|
||||
};
|
||||
@@ -1,168 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
|
||||
// services
|
||||
import jiraImporterService from "services/integration/jira.service";
|
||||
|
||||
// fetch keys
|
||||
import { JIRA_IMPORTER_DETAIL } from "constants/fetch-keys";
|
||||
|
||||
import { IJiraImporterForm, IJiraMetadata } from "types";
|
||||
|
||||
// components
|
||||
import { Spinner, ToggleSwitch } from "components/ui";
|
||||
|
||||
import type { IJiraIntegrationData, TJiraIntegrationSteps } from "./";
|
||||
|
||||
type Props = {
|
||||
setCurrentStep: React.Dispatch<React.SetStateAction<IJiraIntegrationData>>;
|
||||
setDisableTopBarAfter: React.Dispatch<React.SetStateAction<TJiraIntegrationSteps | null>>;
|
||||
};
|
||||
|
||||
export const JiraProjectDetail: React.FC<Props> = (props) => {
|
||||
const { setCurrentStep, setDisableTopBarAfter } = props;
|
||||
|
||||
const {
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<IJiraImporterForm>();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const params: IJiraMetadata = {
|
||||
api_token: watch("metadata.api_token"),
|
||||
project_key: watch("metadata.project_key"),
|
||||
email: watch("metadata.email"),
|
||||
cloud_hostname: watch("metadata.cloud_hostname"),
|
||||
};
|
||||
|
||||
const { data: projectInfo, error } = useSWR(
|
||||
workspaceSlug &&
|
||||
!errors.metadata?.api_token &&
|
||||
!errors.metadata?.project_key &&
|
||||
!errors.metadata?.email &&
|
||||
!errors.metadata?.cloud_hostname
|
||||
? JIRA_IMPORTER_DETAIL(workspaceSlug.toString(), params)
|
||||
: null,
|
||||
workspaceSlug &&
|
||||
!errors.metadata?.api_token &&
|
||||
!errors.metadata?.project_key &&
|
||||
!errors.metadata?.email &&
|
||||
!errors.metadata?.cloud_hostname
|
||||
? () => jiraImporterService.getJiraProjectInfo(workspaceSlug.toString(), params)
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectInfo) return;
|
||||
|
||||
setValue("data.total_issues", projectInfo.issues);
|
||||
setValue("data.total_labels", projectInfo.labels);
|
||||
setValue(
|
||||
"data.users",
|
||||
projectInfo.users?.map((user) => ({
|
||||
email: user.emailAddress,
|
||||
import: false,
|
||||
username: user.displayName,
|
||||
}))
|
||||
);
|
||||
setValue("data.total_states", projectInfo.states);
|
||||
setValue("data.total_modules", projectInfo.modules);
|
||||
}, [projectInfo, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) setDisableTopBarAfter("display-import-data");
|
||||
else setDisableTopBarAfter(null);
|
||||
}, [error, setDisableTopBarAfter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectInfo && !error) setDisableTopBarAfter("display-import-data");
|
||||
else if (!error) setDisableTopBarAfter(null);
|
||||
}, [projectInfo, error, setDisableTopBarAfter]);
|
||||
|
||||
if (!projectInfo && !error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
Something went wrong. Please{" "}
|
||||
<button
|
||||
onClick={() => setCurrentStep({ state: "import-configure" })}
|
||||
type="button"
|
||||
className="inline text-blue-500 underline"
|
||||
>
|
||||
go back
|
||||
</button>{" "}
|
||||
and check your Jira project details.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full space-y-10 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Import Data</h3>
|
||||
<p className="text-sm text-gray-500">Import Completed. We have found:</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.issues}</h4>
|
||||
<p className="text-sm text-gray-500">Issues</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.states}</h4>
|
||||
<p className="text-sm text-gray-500">States</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.modules}</h4>
|
||||
<p className="text-sm text-gray-500">Modules</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.labels}</h4>
|
||||
<p className="text-sm text-gray-500">Labels</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.users?.length}</h4>
|
||||
<p className="text-sm text-gray-500">Users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="text-lg font-semibold">Import Epics</h3>
|
||||
<p className="text-sm text-gray-500">Import epics as modules</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="config.epics_to_modules"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch onChange={onChange} value={value} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,224 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react hook form
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
// icons
|
||||
import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||
import { CogIcon, CloudUploadIcon, UsersIcon, CheckIcon } from "components/icons";
|
||||
|
||||
// services
|
||||
import jiraImporterService from "services/integration/jira.service";
|
||||
|
||||
// fetch keys
|
||||
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||
|
||||
// components
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
import {
|
||||
JiraGetImportDetail,
|
||||
JiraProjectDetail,
|
||||
JiraImportUsers,
|
||||
JiraConfirmImport,
|
||||
jiraFormDefaultValues,
|
||||
TJiraIntegrationSteps,
|
||||
IJiraIntegrationData,
|
||||
} from "./";
|
||||
|
||||
import JiraLogo from "public/services/jira.png";
|
||||
|
||||
import { IJiraImporterForm } from "types";
|
||||
|
||||
const integrationWorkflowData: Array<{
|
||||
title: string;
|
||||
key: TJiraIntegrationSteps;
|
||||
icon: any; // TODO: Fix this later prev type React.FC<React.SVGProps<SVGSVGElement>>
|
||||
}> = [
|
||||
{
|
||||
title: "Configure",
|
||||
key: "import-configure",
|
||||
icon: CogIcon,
|
||||
},
|
||||
{
|
||||
title: "Import Data",
|
||||
key: "display-import-data",
|
||||
icon: ListBulletIcon,
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
key: "import-users",
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
title: "Confirm",
|
||||
key: "import-confirmation",
|
||||
icon: CheckIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export const JiraImporterRoot = () => {
|
||||
const [currentStep, setCurrentStep] = useState<IJiraIntegrationData>({
|
||||
state: "import-configure",
|
||||
});
|
||||
const [disableTopBarAfter, setDisableTopBarAfter] = useState<TJiraIntegrationSteps | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const methods = useForm<IJiraImporterForm>({
|
||||
defaultValues: jiraFormDefaultValues,
|
||||
mode: "all",
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const isValid = methods.formState.isValid;
|
||||
|
||||
const onSubmit = async (data: IJiraImporterForm) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await jiraImporterService
|
||||
.createJiraImporter(workspaceSlug.toString(), data)
|
||||
.then(() => {
|
||||
mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString()));
|
||||
router.push(`/${workspaceSlug}/settings/import-export`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const activeIntegrationState = () => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||
(i) => i?.key === currentStep?.state
|
||||
);
|
||||
|
||||
return currentElementIndex;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-2">
|
||||
<Link href={`/${workspaceSlug}/settings/import-export`}>
|
||||
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900">
|
||||
<div>
|
||||
<ArrowLeftIcon className="h-3 w-3" />
|
||||
</div>
|
||||
<div>Cancel import & go back</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex h-full flex-col space-y-4 rounded-[10px] border border-gray-200 bg-white p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<Image src={JiraLogo} alt="jira logo" />
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
{integrationWorkflowData.map((integration, index) => (
|
||||
<React.Fragment key={integration.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentStep({ state: integration.key });
|
||||
}}
|
||||
disabled={
|
||||
index > activeIntegrationState() + 1 ||
|
||||
Boolean(index === activeIntegrationState() + 1 && disableTopBarAfter)
|
||||
}
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${
|
||||
index <= activeIntegrationState()
|
||||
? `border-[#3F76FF] bg-[#3F76FF] text-white ${
|
||||
index === activeIntegrationState()
|
||||
? "border-opacity-100 bg-opacity-100"
|
||||
: "border-opacity-80 bg-opacity-80"
|
||||
}`
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<integration.icon
|
||||
width="18px"
|
||||
height="18px"
|
||||
color={index <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
|
||||
/>
|
||||
</button>
|
||||
{index < integrationWorkflowData.length - 1 && (
|
||||
<div
|
||||
key={index}
|
||||
className={`border-b px-7 ${
|
||||
index <= activeIntegrationState() - 1 ? `border-[#3F76FF]` : `border-gray-300`
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-full w-full pt-6">
|
||||
<FormProvider {...methods}>
|
||||
<form className="flex h-full w-full flex-col">
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{currentStep.state === "import-configure" && <JiraGetImportDetail />}
|
||||
{currentStep.state === "display-import-data" && (
|
||||
<JiraProjectDetail
|
||||
setDisableTopBarAfter={setDisableTopBarAfter}
|
||||
setCurrentStep={setCurrentStep}
|
||||
/>
|
||||
)}
|
||||
{currentStep?.state === "import-users" && <JiraImportUsers />}
|
||||
{currentStep?.state === "import-confirmation" && <JiraConfirmImport />}
|
||||
</div>
|
||||
|
||||
<div className="-mx-4 mt-4 flex justify-end gap-4 border-t p-4 pb-0">
|
||||
{currentStep?.state !== "import-configure" && (
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||
(i) => i?.key === currentStep?.state
|
||||
);
|
||||
setCurrentStep({
|
||||
state: integrationWorkflowData[currentElementIndex - 1]?.key,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</SecondaryButton>
|
||||
)}
|
||||
<PrimaryButton
|
||||
disabled={
|
||||
disableTopBarAfter === currentStep?.state ||
|
||||
!isValid ||
|
||||
methods.formState.isSubmitting
|
||||
}
|
||||
onClick={() => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||
(i) => i?.key === currentStep?.state
|
||||
);
|
||||
|
||||
if (currentElementIndex === integrationWorkflowData.length - 1) {
|
||||
methods.handleSubmit(onSubmit)();
|
||||
} else {
|
||||
setCurrentStep({
|
||||
state: integrationWorkflowData[currentElementIndex + 1]?.key,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentStep?.state === "import-confirmation" ? "Confirm & Import" : "Next"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
import React from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon, EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
label?: string | JSX.Element;
|
||||
className?: string;
|
||||
ellipsis?: boolean;
|
||||
verticalEllipsis?: boolean;
|
||||
height?: "sm" | "md" | "rg" | "lg";
|
||||
width?: "sm" | "md" | "lg" | "xl" | "auto";
|
||||
textAlignment?: "left" | "center" | "right";
|
||||
noBorder?: boolean;
|
||||
noChevron?: boolean;
|
||||
optionsPosition?: "left" | "right";
|
||||
customButton?: JSX.Element;
|
||||
};
|
||||
|
||||
type MenuItemProps = {
|
||||
children: JSX.Element | string;
|
||||
renderAs?: "button" | "a";
|
||||
href?: string;
|
||||
onClick?: (args?: any) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CustomMenu = ({
|
||||
children,
|
||||
label,
|
||||
className = "",
|
||||
ellipsis = false,
|
||||
verticalEllipsis = false,
|
||||
height = "md",
|
||||
width = "auto",
|
||||
textAlignment,
|
||||
noBorder = false,
|
||||
noChevron = false,
|
||||
optionsPosition = "right",
|
||||
customButton,
|
||||
}: Props) => (
|
||||
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Menu.Button as="div">{customButton}</Menu.Button>
|
||||
) : (
|
||||
<div>
|
||||
{ellipsis || verticalEllipsis ? (
|
||||
<Menu.Button
|
||||
type="button"
|
||||
className="relative grid place-items-center rounded p-1 hover:bg-brand-surface-2 focus:outline-none"
|
||||
>
|
||||
<EllipsisHorizontalIcon
|
||||
className={`h-4 w-4 ${verticalEllipsis ? "rotate-90" : ""}`}
|
||||
/>
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<Menu.Button
|
||||
type="button"
|
||||
className={`flex cursor-pointer items-center justify-between gap-1 px-2.5 py-1 text-xs duration-300 hover:bg-brand-surface-2 ${
|
||||
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
|
||||
} ${
|
||||
textAlignment === "right"
|
||||
? "text-right"
|
||||
: textAlignment === "center"
|
||||
? "text-center"
|
||||
: "text-left"
|
||||
} ${
|
||||
noBorder
|
||||
? "rounded-md"
|
||||
: "rounded-md border border-brand-base shadow-sm focus:outline-none"
|
||||
} ${
|
||||
width === "sm"
|
||||
? "w-10"
|
||||
: width === "md"
|
||||
? "w-20"
|
||||
: width === "lg"
|
||||
? "w-32"
|
||||
: width === "xl"
|
||||
? "w-48"
|
||||
: "w-full"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
|
||||
</Menu.Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className={`absolute z-20 mt-1 overflow-y-scroll whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-1 p-1 text-xs shadow-lg focus:outline-none ${
|
||||
optionsPosition === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||
} ${
|
||||
height === "sm"
|
||||
? "max-h-28"
|
||||
: height === "md"
|
||||
? "max-h-44"
|
||||
: height === "rg"
|
||||
? "max-h-56"
|
||||
: height === "lg"
|
||||
? "max-h-80"
|
||||
: ""
|
||||
} ${
|
||||
width === "sm"
|
||||
? "w-10"
|
||||
: width === "md"
|
||||
? "w-20"
|
||||
: width === "lg"
|
||||
? "w-32"
|
||||
: width === "xl"
|
||||
? "w-48"
|
||||
: "min-w-full"
|
||||
}`}
|
||||
>
|
||||
<div className="py-1">{children}</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({
|
||||
children,
|
||||
renderAs,
|
||||
href,
|
||||
onClick,
|
||||
className = "",
|
||||
}) => (
|
||||
<Menu.Item as="div">
|
||||
{({ active, close }) =>
|
||||
renderAs === "a" ? (
|
||||
<Link href={href ?? ""}>
|
||||
<a
|
||||
className={`${className} ${
|
||||
active ? "bg-brand-surface-2" : ""
|
||||
} hover:text-brand-muted-1 inline-block w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2`}
|
||||
onClick={close}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`${className} ${
|
||||
active ? "bg-brand-surface-2" : ""
|
||||
} hover:text-brand-muted-1 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</Menu.Item>
|
||||
);
|
||||
|
||||
CustomMenu.MenuItem = MenuItem;
|
||||
|
||||
export { CustomMenu };
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
type Props = {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ToggleSwitch: React.FC<Props> = (props) => {
|
||||
const { value, onChange, label, disabled, className } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
value ? "bg-green-500" : "bg-gray-200"
|
||||
} ${className || " "}`}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
value ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
export const THEMES = [
|
||||
"light",
|
||||
"dark",
|
||||
"light-contrast",
|
||||
"dark-contrast",
|
||||
// "custom"
|
||||
];
|
||||
|
||||
export const THEMES_OBJ = [
|
||||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
type: "light",
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
type: "dark",
|
||||
},
|
||||
{
|
||||
value: "light-contrast",
|
||||
label: "Light High Contrast",
|
||||
type: "light",
|
||||
},
|
||||
{
|
||||
value: "dark-contrast",
|
||||
label: "Dark High Contrast",
|
||||
type: "dark",
|
||||
},
|
||||
// {
|
||||
// value: "custom",
|
||||
// label: "Custom",
|
||||
// type: "light",
|
||||
// },
|
||||
];
|
||||
@@ -1,35 +0,0 @@
|
||||
export const hexToRgb = (hex: string) => {
|
||||
hex = hex.toLowerCase();
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)].join(",")
|
||||
: null;
|
||||
};
|
||||
|
||||
export const applyTheme = (palette: string, isDarkPalette: boolean) => {
|
||||
const values: string[] = [];
|
||||
palette.split(",").map((color: string) => {
|
||||
const cssVarColor = hexToRgb(color);
|
||||
if (cssVarColor) values.push(cssVarColor);
|
||||
});
|
||||
|
||||
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",
|
||||
"color-scheme",
|
||||
];
|
||||
|
||||
values.push(isDarkPalette ? "dark" : "light");
|
||||
|
||||
cssVars.forEach((cssVar, i) =>
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(cssVar, values[i])
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
// hooks
|
||||
import useUser from "./use-user";
|
||||
|
||||
const useWorkspaceMembers = (workspaceSlug?: string) => {
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug) : null
|
||||
);
|
||||
|
||||
const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id);
|
||||
|
||||
const isOwner = workspaceMembers?.some(
|
||||
(item) => item.member.id === (user as any)?.id && item.role === 20
|
||||
);
|
||||
const isMember = workspaceMembers?.some(
|
||||
(item) => item.member.id === (user as any)?.id && item.role === 15
|
||||
);
|
||||
const isViewer = workspaceMembers?.some(
|
||||
(item) => item.member.id === (user as any)?.id && item.role === 10
|
||||
);
|
||||
const isGuest = workspaceMembers?.some(
|
||||
(item) => item.member.id === (user as any)?.id && item.role === 5
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceMembers,
|
||||
workspaceMemberErrors,
|
||||
hasJoined,
|
||||
isOwner,
|
||||
isMember,
|
||||
isViewer,
|
||||
isGuest,
|
||||
};
|
||||
};
|
||||
|
||||
export default useWorkspaceMembers;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,34 +0,0 @@
|
||||
import APIService from "services/api.service";
|
||||
|
||||
// types
|
||||
import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
class JiraImportedService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
}
|
||||
|
||||
async getJiraProjectInfo(workspaceSlug: string, params: IJiraMetadata): Promise<IJiraResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/importers/jira`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise<IJiraResponse> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const jiraImporterService = new JiraImportedService();
|
||||
|
||||
export default jiraImporterService;
|
||||
@@ -1,60 +0,0 @@
|
||||
function withOpacity(variableName) {
|
||||
return ({ opacityValue }) => {
|
||||
if (opacityValue !== undefined) return `rgba(var(${variableName}), ${opacityValue})`;
|
||||
|
||||
return `rgb(var(${variableName}))`;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
theme: "#3f76ff",
|
||||
"hover-gray": "#f5f5f5",
|
||||
primary: "#f9fafb", // gray-50
|
||||
secondary: "white",
|
||||
brand: {
|
||||
accent: withOpacity("--color-accent"),
|
||||
base: withOpacity("--color-bg-base"),
|
||||
},
|
||||
},
|
||||
borderColor: {
|
||||
brand: {
|
||||
base: withOpacity("--color-border"),
|
||||
"surface-1": withOpacity("--color-bg-surface-1"),
|
||||
"surface-2": withOpacity("--color-bg-surface-2"),
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
brand: {
|
||||
base: withOpacity("--color-bg-base"),
|
||||
"surface-1": withOpacity("--color-bg-surface-1"),
|
||||
"surface-2": withOpacity("--color-bg-surface-2"),
|
||||
sidebar: withOpacity("--color-bg-sidebar"),
|
||||
},
|
||||
},
|
||||
textColor: {
|
||||
brand: {
|
||||
base: withOpacity("--color-text-base"),
|
||||
secondary: withOpacity("--color-text-secondary"),
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
leftToaster: {
|
||||
"0%": { left: "-20rem" },
|
||||
"100%": { left: "0" },
|
||||
},
|
||||
rightToaster: {
|
||||
"0%": { right: "-20rem" },
|
||||
"100%": { right: "0" },
|
||||
},
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
};
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
export interface IJiraImporterForm {
|
||||
metadata: IJiraMetadata;
|
||||
config: IJiraConfig;
|
||||
data: IJiraData;
|
||||
project_id: string;
|
||||
}
|
||||
|
||||
export interface IJiraConfig {
|
||||
epics_to_modules: boolean;
|
||||
}
|
||||
|
||||
export interface IJiraData {
|
||||
users: User[];
|
||||
invite_users: boolean;
|
||||
total_issues: number;
|
||||
total_labels: number;
|
||||
total_states: number;
|
||||
total_modules: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
import: "invite" | "map" | false;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface IJiraMetadata {
|
||||
cloud_hostname: string;
|
||||
api_token: string;
|
||||
project_key: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface IJiraResponse {
|
||||
issues: number;
|
||||
modules: number;
|
||||
labels: number;
|
||||
states: number;
|
||||
users: IJiraResponseUser[];
|
||||
}
|
||||
|
||||
export interface IJiraResponseUser {
|
||||
self: string;
|
||||
accountId: string;
|
||||
accountType: string;
|
||||
emailAddress: string;
|
||||
avatarUrls: AvatarUrls;
|
||||
displayName: string;
|
||||
active: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export interface IJiraResponseAvatarUrls {
|
||||
"48x48": string;
|
||||
"24x24": string;
|
||||
"16x16": string;
|
||||
"32x32": string;
|
||||
}
|
||||
-8029
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
# Replace with your instance Public IP
|
||||
# NEXT_PUBLIC_API_BASE_URL = "http://localhost"
|
||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
||||
NEXT_PUBLIC_GITHUB_ID=""
|
||||
@@ -8,4 +7,4 @@ NEXT_PUBLIC_SENTRY_DSN=""
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
NEXT_PUBLIC_ENABLE_SENTRY=0
|
||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -4,10 +4,9 @@ RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
CMD ["yarn","dev"]
|
||||
@@ -0,0 +1,51 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=app --docker
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
RUN yarn turbo run build --filter=app
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 plane
|
||||
RUN adduser --system --uid 1001 captain
|
||||
USER captain
|
||||
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
COPY --from=installer /app/apps/app/package.json .
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
|
||||
# COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone/node_modules ./apps/app/node_modules
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
EXPOSE 3000
|
||||
+1
-1
@@ -141,7 +141,7 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
<button
|
||||
type="button"
|
||||
className={`mt-5 flex w-full justify-end text-xs outline-none ${
|
||||
isResendDisabled ? "cursor-default text-gray-400" : "cursor-pointer text-brand-accent"
|
||||
isResendDisabled ? "cursor-default text-gray-400" : "cursor-pointer text-theme"
|
||||
} `}
|
||||
onClick={() => {
|
||||
setIsCodeResending(true);
|
||||
+2
-2
@@ -50,7 +50,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
if (!error?.response?.data) return;
|
||||
Object.keys(error.response.data).forEach((key) => {
|
||||
const err = error.response.data[key];
|
||||
console.log(err);
|
||||
console.log("err", err);
|
||||
setError(key as keyof EmailPasswordFormValues, {
|
||||
type: "manual",
|
||||
message: Array.isArray(err) ? err.join(", ") : err,
|
||||
@@ -94,7 +94,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="ml-auto text-sm">
|
||||
<Link href={"/forgot-password"}>
|
||||
<a className="font-medium text-brand-accent hover:text-indigo-500">Forgot your password?</a>
|
||||
<a className="font-medium text-theme hover:text-indigo-500">Forgot your password?</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
+1
-1
@@ -37,7 +37,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
<Link
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
>
|
||||
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-brand-base p-2 text-sm font-medium text-gray-600 duration-300 hover:bg-gray-50">
|
||||
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-gray-200 p-2 text-sm font-medium text-gray-600 duration-300 hover:bg-gray-50">
|
||||
<Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" />
|
||||
<span>Sign In with Github</span>
|
||||
</button>
|
||||
+4
-4
@@ -36,16 +36,16 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
alt="ProjectSettingImg"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-xl font-medium text-brand-base">
|
||||
<h1 className="text-xl font-medium text-gray-900">
|
||||
Oops! You are not authorized to view this page
|
||||
</h1>
|
||||
|
||||
<div className="w-full text-base text-brand-secondary max-w-md ">
|
||||
<div className="w-full text-base text-gray-500 max-w-md ">
|
||||
{user ? (
|
||||
<p>
|
||||
You have signed in as {user.email}. <br />
|
||||
<Link href={`/signin?next=${currentPath}`}>
|
||||
<a className="text-brand-base font-medium">Sign in</a>
|
||||
<a className="text-gray-900 font-medium">Sign in</a>
|
||||
</Link>{" "}
|
||||
with different account that has access to this page.
|
||||
</p>
|
||||
@@ -53,7 +53,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
<p>
|
||||
You need to{" "}
|
||||
<Link href={`/signin?next=${currentPath}`}>
|
||||
<a className="text-brand-base font-medium">Sign in</a>
|
||||
<a className="text-gray-900 font-medium">Sign in</a>
|
||||
</Link>{" "}
|
||||
with an account that has access to this page.
|
||||
</p>
|
||||
+1
-1
@@ -31,7 +31,7 @@ export const JoinProject: React.FC = () => {
|
||||
project_ids: [projectId as string],
|
||||
})
|
||||
.then(async () => {
|
||||
await mutate(USER_PROJECT_VIEW(projectId.toString()));
|
||||
await mutate(USER_PROJECT_VIEW(workspaceSlug.toString()));
|
||||
setIsJoiningProject(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -16,7 +16,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
||||
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"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeftIcon className="h-3 w-3" />
|
||||
@@ -37,7 +37,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
|
||||
<>
|
||||
{link ? (
|
||||
<Link href={link}>
|
||||
<a className="border-r-2 border-brand-base px-3 text-sm">
|
||||
<a className="border-r-2 border-gray-300 px-3 text-sm">
|
||||
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
|
||||
{icon ?? null}
|
||||
{title}
|
||||
+48
-73
@@ -44,7 +44,6 @@ import {
|
||||
ChangeIssueState,
|
||||
ChangeIssuePriority,
|
||||
ChangeIssueAssignee,
|
||||
ChangeInterfaceTheme,
|
||||
} from "components/command-palette";
|
||||
import { BulkDeleteIssuesModal } from "components/core";
|
||||
import { CreateUpdateCycleModal } from "components/cycles";
|
||||
@@ -380,7 +379,7 @@ export const CommandPalette: React.FC = () => {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
@@ -393,7 +392,7 @@ export const CommandPalette: React.FC = () => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl bg-brand-surface-2 border-brand-base border shadow-2xl transition-all">
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||
@@ -416,7 +415,7 @@ export const CommandPalette: React.FC = () => {
|
||||
>
|
||||
{issueId && issueDetails && (
|
||||
<div className="flex p-3">
|
||||
<p className="overflow-hidden truncate rounded-md bg-brand-surface-1 p-1 px-2 text-xs font-medium text-brand-secondary">
|
||||
<p className="overflow-hidden truncate rounded-md bg-gray-100 p-1 px-2 text-xs font-medium text-gray-500">
|
||||
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
||||
{issueDetails?.name}
|
||||
</p>
|
||||
@@ -424,11 +423,11 @@ export const CommandPalette: React.FC = () => {
|
||||
)}
|
||||
<div className="relative">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-secondary"
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Command.Input
|
||||
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"
|
||||
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"
|
||||
placeholder={placeholder}
|
||||
value={searchTerm}
|
||||
onValueChange={(e) => {
|
||||
@@ -442,9 +441,7 @@ export const CommandPalette: React.FC = () => {
|
||||
resultsCount === 0 &&
|
||||
searchTerm !== "" &&
|
||||
debouncedSearchTerm !== "" && (
|
||||
<div className="my-4 text-center text-brand-secondary">
|
||||
No results found.
|
||||
</div>
|
||||
<div className="my-4 text-center text-gray-500">No results found.</div>
|
||||
)}
|
||||
|
||||
{(isLoading || isSearching) && (
|
||||
@@ -505,11 +502,8 @@ export const CommandPalette: React.FC = () => {
|
||||
value={value}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden text-brand-secondary">
|
||||
<Icon
|
||||
className="h-4 w-4 text-brand-secondary"
|
||||
color="#6b7280"
|
||||
/>
|
||||
<div className="flex items-center gap-2 overflow-hidden text-gray-700">
|
||||
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" />
|
||||
<p className="block flex-1 truncate">{item.name}</p>
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -534,8 +528,8 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<Squares2X2Icon className="h-4 w-4 text-gray-500" />
|
||||
Change state...
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -547,8 +541,8 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ChartBarIcon className="h-4 w-4 text-gray-500" />
|
||||
Change priority...
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -560,8 +554,8 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<UsersIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<UsersIcon className="h-4 w-4 text-gray-500" />
|
||||
Assign to...
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -572,15 +566,15 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
{issueDetails?.assignees.includes(user.id) ? (
|
||||
<>
|
||||
<UserMinusIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<UserMinusIcon className="h-4 w-4 text-gray-500" />
|
||||
Un-assign from me
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlusIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<UserPlusIcon className="h-4 w-4 text-gray-500" />
|
||||
Assign to me
|
||||
</>
|
||||
)}
|
||||
@@ -588,8 +582,8 @@ export const CommandPalette: React.FC = () => {
|
||||
</Command.Item>
|
||||
|
||||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<TrashIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<TrashIcon className="h-4 w-4 text-gray-500" />
|
||||
Delete issue
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -600,19 +594,16 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<LinkIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<LinkIcon className="h-4 w-4 text-gray-500" />
|
||||
Copy issue URL to clipboard
|
||||
</div>
|
||||
</Command.Item>
|
||||
</>
|
||||
)}
|
||||
<Command.Group heading="Issue">
|
||||
<Command.Item
|
||||
onSelect={createNewIssue}
|
||||
className="focus:bg-brand-surface-2"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<Command.Item onSelect={createNewIssue} className="focus:bg-gray-200">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new issue
|
||||
</div>
|
||||
@@ -626,7 +617,7 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={createNewProject}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new project
|
||||
</div>
|
||||
@@ -642,7 +633,7 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={createNewCycle}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ContrastIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new cycle
|
||||
</div>
|
||||
@@ -655,7 +646,7 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={createNewModule}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new module
|
||||
</div>
|
||||
@@ -665,7 +656,7 @@ export const CommandPalette: React.FC = () => {
|
||||
|
||||
<Command.Group heading="View">
|
||||
<Command.Item onSelect={createNewView} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ViewListIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new view
|
||||
</div>
|
||||
@@ -694,7 +685,7 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4" color="#6b7280" />
|
||||
Search settings...
|
||||
</div>
|
||||
@@ -705,24 +696,11 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={createNewWorkspace}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<FolderPlusIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<FolderPlusIcon className="h-4 w-4 text-gray-500" />
|
||||
Create new workspace
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Change interface theme...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-interface-theme"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||
Change interface theme...
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Help">
|
||||
<Command.Item
|
||||
@@ -735,8 +713,8 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<RocketLaunchIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
|
||||
Open keyboard shortcuts
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -747,8 +725,8 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<DocumentIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<DocumentIcon className="h-4 w-4 text-gray-500" />
|
||||
Open Plane documentation
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -759,7 +737,7 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
||||
Join our Discord
|
||||
</div>
|
||||
@@ -774,7 +752,7 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<GithubIcon className="h-4 w-4" color="#6b7280" />
|
||||
Report a bug
|
||||
</div>
|
||||
@@ -786,8 +764,8 @@ export const CommandPalette: React.FC = () => {
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" />
|
||||
Chat with us
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -801,8 +779,8 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={() => goToSettings()}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
General
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -810,8 +788,8 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={() => goToSettings("members")}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
Members
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -819,8 +797,8 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={() => goToSettings("billing")}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
Billings and Plans
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -828,8 +806,8 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={() => goToSettings("integrations")}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
Integrations
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -837,8 +815,8 @@ export const CommandPalette: React.FC = () => {
|
||||
onSelect={() => goToSettings("import-export")}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<SettingIcon className="h-4 w-4 text-gray-500" />
|
||||
Import/Export
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -864,9 +842,6 @@ export const CommandPalette: React.FC = () => {
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
/>
|
||||
)}
|
||||
{page === "change-interface-theme" && (
|
||||
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
|
||||
)}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</Dialog.Panel>
|
||||
@@ -3,4 +3,3 @@ export * from "./shortcuts-modal";
|
||||
export * from "./change-issue-state";
|
||||
export * from "./change-issue-priority";
|
||||
export * from "./change-issue-assignee";
|
||||
export * from "./change-interface-theme";
|
||||
+15
-15
@@ -71,7 +71,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||
<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">
|
||||
@@ -85,29 +85,29 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="bg-brand-surface-2 p-5">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="bg-white p-5">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="flex justify-between text-lg font-medium leading-6 text-brand-base"
|
||||
className="flex justify-between text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
<span>Keyboard Shortcuts</span>
|
||||
<span>
|
||||
<button type="button" onClick={() => setIsOpen(false)}>
|
||||
<XMarkIcon
|
||||
className="h-6 w-6 text-gray-400 hover:text-brand-secondary"
|
||||
className="h-6 w-6 text-gray-400 hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</Dialog.Title>
|
||||
<div>
|
||||
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-brand-base bg-brand-surface-1 px-3 py-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-brand-secondary" />
|
||||
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-gray-200 bg-gray-100 px-3 py-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-gray-500" />
|
||||
<Input
|
||||
className="w-full border-none bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none"
|
||||
className="w-full border-none bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none"
|
||||
id="search"
|
||||
name="search"
|
||||
type="text"
|
||||
@@ -123,16 +123,16 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<div key={shortcut.keys} className="flex w-full flex-col">
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-brand-secondary">{shortcut.description}</p>
|
||||
<p className="text-sm text-gray-500">{shortcut.description}</p>
|
||||
<div className="flex items-center gap-x-2.5">
|
||||
{shortcut.keys.split(",").map((key, index) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
{key === "Ctrl" ? (
|
||||
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-2">
|
||||
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
|
||||
<MacCommandIcon />
|
||||
</span>
|
||||
) : (
|
||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-gray-800">
|
||||
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
|
||||
{key === "Ctrl" ? <MacCommandIcon /> : key}
|
||||
</kbd>
|
||||
)}
|
||||
@@ -145,7 +145,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<p className="text-sm text-brand-secondary">
|
||||
<p className="text-sm text-gray-500">
|
||||
No shortcuts found for{" "}
|
||||
<span className="font-semibold italic">
|
||||
{`"`}
|
||||
@@ -162,16 +162,16 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{shortcuts.map(({ keys, description }, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<p className="text-sm text-brand-secondary">{description}</p>
|
||||
<p className="text-sm text-gray-500">{description}</p>
|
||||
<div className="flex items-center gap-x-2.5">
|
||||
{keys.split(",").map((key, index) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
{key === "Ctrl" ? (
|
||||
<span className="flex h-full items-center rounded-sm border border-brand-base text-brand-secondary bg-brand-surface-1 p-2">
|
||||
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
|
||||
<MacCommandIcon />
|
||||
</span>
|
||||
) : (
|
||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
|
||||
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
|
||||
{key === "Ctrl" ? <MacCommandIcon /> : key}
|
||||
</kbd>
|
||||
)}
|
||||
+2
-2
@@ -81,7 +81,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between gap-2 rounded bg-brand-surface-1 p-2 shadow"
|
||||
className="flex items-center justify-between gap-2 rounded bg-white p-2 shadow"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentState &&
|
||||
@@ -92,7 +92,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
: addSpaceIfCamelCase(singleGroup)}
|
||||
</h4>
|
||||
</div>
|
||||
<span className="text-xs text-brand-secondary">0</span>
|
||||
<span className="text-xs text-gray-500">0</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
+8
-10
@@ -96,17 +96,17 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
||||
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor);
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center");
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||
className="h-[18px] w-[18px] flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: labelColor }}
|
||||
/>
|
||||
);
|
||||
@@ -123,8 +123,8 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between px-1 ${
|
||||
!isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : ""
|
||||
className={`flex justify-between items-center px-1 ${
|
||||
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
||||
@@ -143,9 +143,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
<span
|
||||
className={`${
|
||||
isCollapsed ? "ml-0.5" : ""
|
||||
} min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs`}
|
||||
className={`${isCollapsed ? "ml-0.5" : ""} rounded-full bg-gray-100 py-1 px-3 text-sm`}
|
||||
>
|
||||
{groupedByIssues?.[groupTitle].length ?? 0}
|
||||
</span>
|
||||
@@ -155,7 +153,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
setIsCollapsed((prevData) => !prevData);
|
||||
}}
|
||||
@@ -169,7 +167,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
{!isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
|
||||
onClick={addIssueToState}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
+7
-9
@@ -66,7 +66,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
}, [currentState]);
|
||||
|
||||
return (
|
||||
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96"}`}>
|
||||
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
|
||||
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
|
||||
<BoardHeader
|
||||
addIssueToState={addIssueToState}
|
||||
@@ -81,7 +81,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`relative h-full overflow-y-auto p-1 ${
|
||||
snapshot.isDraggingOver ? "bg-brand-base/20" : ""
|
||||
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
||||
} ${!isCollapsed ? "hidden" : "block"}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
@@ -91,17 +91,15 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
<div
|
||||
className={`absolute ${
|
||||
snapshot.isDraggingOver ? "block" : "hidden"
|
||||
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-brand-surface-1 opacity-50`}
|
||||
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-gray-100 opacity-50`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute ${
|
||||
snapshot.isDraggingOver ? "block" : "hidden"
|
||||
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-brand-base 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-white p-2 text-xs`}
|
||||
>
|
||||
This board is ordered by{" "}
|
||||
{replaceUnderscoreIfSnakeCase(
|
||||
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
|
||||
)}
|
||||
{replaceUnderscoreIfSnakeCase(orderBy ?? "created_at")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -148,7 +146,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
|
||||
className="flex items-center gap-2 font-medium text-theme outline-none"
|
||||
onClick={addIssueToState}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
@@ -160,7 +158,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
|
||||
className="flex items-center gap-2 font-medium text-theme outline-none"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
+9
-9
@@ -261,8 +261,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</a>
|
||||
</ContextMenu>
|
||||
<div
|
||||
className={`mb-3 rounded bg-brand-base shadow ${
|
||||
snapshot.isDragging ? "border-2 border-brand-accent shadow-lg" : ""
|
||||
className={`mb-3 rounded bg-white shadow ${
|
||||
snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
|
||||
}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
@@ -312,12 +312,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a>
|
||||
{properties.key && (
|
||||
<div className="mb-2.5 text-xs font-medium text-brand-secondary">
|
||||
<div className="mb-2.5 text-xs font-medium text-gray-700">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
<h5
|
||||
className="break-all text-sm group-hover:text-brand-accent"
|
||||
className="break-all text-sm group-hover:text-theme"
|
||||
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
|
||||
>
|
||||
{truncateText(issue.name, 100)}
|
||||
@@ -349,7 +349,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm">
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
@@ -358,7 +358,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{issue.label_details.map((label) => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
@@ -389,7 +389,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.link && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
||||
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
|
||||
@@ -399,10 +399,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{properties.attachment_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
||||
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-gray-500" />
|
||||
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
|
||||
{issue.attachment_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
+8
-8
@@ -121,7 +121,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-brand-surface-2 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-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<form>
|
||||
<Combobox
|
||||
onChange={(val: string) => {
|
||||
@@ -136,12 +136,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
||||
>
|
||||
<div className="relative m-1">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
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"
|
||||
placeholder="Search..."
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
@@ -154,7 +154,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
||||
{filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base">
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||
Select issues to delete
|
||||
</h2>
|
||||
)}
|
||||
@@ -166,7 +166,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
||||
value={issue.id}
|
||||
className={({ active }) =>
|
||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-brand-base" : ""
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -182,7 +182,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
<span>{issue.name}</span>
|
||||
@@ -194,9 +194,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||
<LayerDiagonalIcon height="56" width="56" />
|
||||
<h3 className="text-brand-secondary">
|
||||
<h3 className="text-gray-500">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
|
||||
<pre className="inline rounded bg-gray-200 px-2 py-1">C</pre>.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
+34
-75
@@ -26,17 +26,14 @@ import {
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { CustomMenu, Spinner } from "components/ui";
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icon
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
@@ -52,16 +49,12 @@ import { IIssue } from "types";
|
||||
import { monthOptions, yearOptions } from "constants/calendar";
|
||||
import modulesService from "services/modules.service";
|
||||
|
||||
type Props = {
|
||||
addIssueToDate: (date: string) => void;
|
||||
};
|
||||
|
||||
interface ICalendarRange {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
|
||||
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
export const CalendarView = () => {
|
||||
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false);
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true);
|
||||
@@ -69,8 +62,6 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { params } = useIssuesView();
|
||||
|
||||
const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({
|
||||
startDate: startOfWeek(currentDate),
|
||||
endDate: lastDayOfWeek(currentDate),
|
||||
@@ -86,13 +77,11 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, {
|
||||
...params,
|
||||
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
|
||||
calendarDateRange.endDate
|
||||
)};before`,
|
||||
group_by: null,
|
||||
})
|
||||
issuesService.getIssuesWithParams(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
targetDateFilter
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
@@ -106,13 +95,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleId as string,
|
||||
{
|
||||
...params,
|
||||
target_date: `${renderDateFormat(
|
||||
calendarDateRange.startDate
|
||||
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
|
||||
group_by: null,
|
||||
}
|
||||
targetDateFilter
|
||||
)
|
||||
: null
|
||||
);
|
||||
@@ -127,13 +110,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
moduleId as string,
|
||||
{
|
||||
...params,
|
||||
target_date: `${renderDateFormat(
|
||||
calendarDateRange.startDate
|
||||
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
|
||||
group_by: null,
|
||||
}
|
||||
targetDateFilter
|
||||
)
|
||||
: null
|
||||
);
|
||||
@@ -150,11 +127,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
|
||||
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
||||
|
||||
const calendarIssues = cycleId
|
||||
? cycleCalendarIssues
|
||||
: moduleId
|
||||
? moduleCalendarIssues
|
||||
: projectCalendarIssues;
|
||||
const calendarIssues = cycleCalendarIssues ?? moduleCalendarIssues ?? projectCalendarIssues;
|
||||
|
||||
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
||||
const filterIssue =
|
||||
@@ -225,17 +198,17 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
});
|
||||
};
|
||||
|
||||
return calendarIssues ? (
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className="-m-2 h-full overflow-y-auto rounded-lg text-brand-secondary">
|
||||
<div className="h-full overflow-y-auto rounded-lg text-gray-600 -m-2">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
||||
<div className="relative flex h-full w-full gap-2 items-center justify-start text-sm ">
|
||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button className={`group flex h-full items-start gap-1 text-brand-base`}>
|
||||
<Popover.Button className={`group flex h-full items-start gap-1 text-gray-800`}>
|
||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold">
|
||||
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
||||
<span className="text-black">{formatDate(currentDate, "Month")}</span>{" "}
|
||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
@@ -249,30 +222,30 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
|
||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||
<Popover.Panel className="absolute top-10 left-0 z-20 w-full max-w-xs flex flex-col transform overflow-hidden bg-white shadow-lg rounded-[10px]">
|
||||
<div className="flex justify-center items-center text-sm gap-5 px-2 py-2">
|
||||
{yearOptions.map((year) => (
|
||||
<button
|
||||
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
||||
className={` ${
|
||||
isSameYear(year.value, currentDate)
|
||||
? "text-sm font-medium text-brand-base"
|
||||
: "text-xs text-brand-secondary "
|
||||
} hover:text-sm hover:font-medium hover:text-brand-base`}
|
||||
? "text-sm font-medium text-gray-800"
|
||||
: "text-xs text-gray-400 "
|
||||
} hover:text-sm hover:text-gray-800 hover:font-medium `}
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 border-t border-brand-base px-2">
|
||||
<div className="grid grid-cols-4 px-2 border-t border-gray-200">
|
||||
{monthOptions.map((month) => (
|
||||
<button
|
||||
onClick={() =>
|
||||
updateDate(updateDateWithMonth(month.value, currentDate))
|
||||
}
|
||||
className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${
|
||||
className={`text-gray-400 text-xs px-2 py-2 hover:font-medium hover:text-gray-800 ${
|
||||
isSameMonth(month.value, currentDate)
|
||||
? "font-medium text-brand-base"
|
||||
? "font-medium text-gray-800"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
@@ -322,9 +295,9 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<div className="flex w-full gap-2 items-center justify-end">
|
||||
<button
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-4 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none"
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border bg-white px-4 py-1.5 text-sm hover:bg-gray-100 hover:text-gray-900 focus:outline-none"
|
||||
onClick={() => {
|
||||
if (isMonthlyView) {
|
||||
updateDate(new Date());
|
||||
@@ -339,11 +312,10 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
>
|
||||
Today{" "}
|
||||
</button>
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div
|
||||
className={`group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none `}
|
||||
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"}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
@@ -358,7 +330,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
endDate: lastDayOfWeek(currentDate),
|
||||
});
|
||||
}}
|
||||
className="w-52 text-sm text-brand-secondary"
|
||||
className="w-52 text-sm text-gray-600"
|
||||
>
|
||||
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-2">Monthly View</span>
|
||||
@@ -377,7 +349,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
endDate: getCurrentWeekEndDate(currentDate),
|
||||
});
|
||||
}}
|
||||
className="w-52 text-sm text-brand-secondary"
|
||||
className="w-52 text-sm text-gray-600"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-2">Weekly View</span>
|
||||
@@ -388,12 +360,12 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
|
||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-gray-200 py-2 px-1 text-sm text-gray-600">
|
||||
<h4>Show weekends</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
showWeekEnds ? "bg-green-500" : "bg-brand-surface-2"
|
||||
showWeekEnds ? "bg-green-500" : "bg-gray-200"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showWeekEnds}
|
||||
@@ -402,7 +374,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
<span className="sr-only">Show weekends</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
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"
|
||||
}`}
|
||||
/>
|
||||
@@ -420,7 +392,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
{weeks.map((date, index) => (
|
||||
<div
|
||||
key={index}
|
||||
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 ${
|
||||
className={`flex items-center justify-start p-1.5 gap-2 border-gray-300 bg-gray-100 text-base font-medium text-gray-600 ${
|
||||
!isMonthlyView
|
||||
? showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
@@ -452,7 +424,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
key={index}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={`group flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
|
||||
className={`flex flex-col gap-1.5 border-t border-gray-300 p-2.5 text-left text-sm font-medium hover:bg-gray-100 ${
|
||||
showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
? ""
|
||||
@@ -472,7 +444,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={`w-full cursor-pointer truncate rounded bg-brand-surface-2 p-1.5 hover:scale-105 ${
|
||||
className={`w-full cursor-pointer truncate rounded bg-white p-1.5 hover:scale-105 ${
|
||||
snapshot.isDragging ? "shadow-lg" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -486,15 +458,6 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<div className="flex items-center justify-center p-1.5 text-sm text-brand-secondary opacity-0 group-hover:opacity-100">
|
||||
<button
|
||||
className="flex items-center justify-center gap-2 text-center"
|
||||
onClick={() => addIssueToDate(date.date)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 text-brand-secondary" />
|
||||
Add new issue
|
||||
</button>
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
@@ -503,9 +466,5 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+8
-8
@@ -130,7 +130,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-brand-surface-2 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-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<form>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -139,11 +139,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
<Combobox as="div" {...field} multiple>
|
||||
<div className="relative m-1">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
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"
|
||||
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"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
@@ -156,7 +156,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
{filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mb-2 px-3 text-xs font-semibold text-brand-base">
|
||||
<h2 className="mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||
Select issues to add
|
||||
</h2>
|
||||
)}
|
||||
@@ -169,7 +169,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
value={issue.id}
|
||||
className={({ active }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-brand-base" : ""
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -182,7 +182,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
{issue.name}
|
||||
@@ -195,9 +195,9 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||
<LayerDiagonalIcon height="56" width="56" />
|
||||
<h3 className="text-brand-secondary">
|
||||
<h3 className="text-gray-500">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
|
||||
<pre className="inline rounded bg-gray-200 px-2 py-1">C</pre>.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
@@ -55,23 +55,23 @@ const activityDetails: {
|
||||
},
|
||||
modules: {
|
||||
message: "set the module to",
|
||||
icon: <RectangleGroupIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <RectangleGroupIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: "set the state to",
|
||||
icon: <Squares2X2Icon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <Squares2X2Icon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: "set the priority to",
|
||||
icon: <ChartBarIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <ChartBarIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: "set the name to",
|
||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: "updated the description.",
|
||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: "set the estimate point to",
|
||||
@@ -79,15 +79,15 @@ const activityDetails: {
|
||||
},
|
||||
target_date: {
|
||||
message: "set the due date to",
|
||||
icon: <CalendarDaysIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <CalendarDaysIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: "set the parent to",
|
||||
icon: <UserIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <UserIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: "deleted the issue.",
|
||||
icon: <TrashIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: <TrashIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
estimate: {
|
||||
message: "updated the estimate",
|
||||
@@ -217,7 +217,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-white px-0.5 py-px">
|
||||
<ChatBubbleLeftEllipsisIcon
|
||||
className="h-3.5 w-3.5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
@@ -230,7 +230,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-brand-secondary">
|
||||
<p className="mt-0.5 text-xs text-gray-500">
|
||||
Commented {timeAgo(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -244,7 +244,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
editable={false}
|
||||
onBlur={() => ({})}
|
||||
noBorder
|
||||
customClassName="text-xs bg-brand-surface-1"
|
||||
customClassName="text-xs bg-gray-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,7 +259,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div className="relative pb-1">
|
||||
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-brand-surface-2"
|
||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
@@ -268,7 +268,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-1 ring-white">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 ring-white">
|
||||
{activity.field ? (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
) : activity.actor_detail.avatar &&
|
||||
@@ -292,7 +292,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="text-xs text-brand-secondary">
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="text-gray font-medium">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot
|
||||
@@ -300,7 +300,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
: " " + activity.actor_detail.last_name}
|
||||
</span>
|
||||
<span> {action} </span>
|
||||
<span className="text-xs font-medium text-brand-base"> {value} </span>
|
||||
<span className="text-xs font-medium text-gray-900"> {value} </span>
|
||||
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,9 +57,9 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
|
||||
className="flex items-center gap-x-2 rounded-full border bg-white px-2 py-1"
|
||||
>
|
||||
<span className="font-medium capitalize text-brand-secondary">
|
||||
<span className="font-medium capitalize text-gray-500">
|
||||
{replaceUnderscoreIfSnakeCase(key)}:
|
||||
</span>
|
||||
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||
@@ -131,7 +131,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
|
||||
: priority === "low"
|
||||
? "bg-green-100 text-green-500 hover:bg-green-100"
|
||||
: "bg-brand-surface-1 text-gray-700 hover:bg-brand-surface-1"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<span>{getPriorityIcon(priority)}</span>
|
||||
@@ -339,7 +339,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
created_by: null,
|
||||
})
|
||||
}
|
||||
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
|
||||
className="flex items-center gap-x-1 rounded-full border bg-white px-3 py-1.5 text-xs"
|
||||
>
|
||||
<span className="font-medium">Clear all filters</span>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
+1
-1
@@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-brand-base bg-brand-surface-2 p-4 shadow ${
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border bg-white p-4 shadow ${
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
+5
-5
@@ -65,7 +65,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
return (
|
||||
<Popover className="relative z-[2]" ref={ref}>
|
||||
<Popover.Button
|
||||
className="rounded-md border border-brand-base bg-brand-surface-2 px-2 py-1 text-xs text-gray-700"
|
||||
className="rounded border border-gray-500 bg-white px-2 py-1 text-xs text-gray-700"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
{label}
|
||||
@@ -79,16 +79,16 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-brand-surface-2 shadow-lg">
|
||||
<div className="h-96 w-80 overflow-auto rounded border border-brand-base bg-brand-surface-2 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-white shadow-lg">
|
||||
<div className="h-96 w-80 overflow-auto rounded border bg-white p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||
<Tab.Group>
|
||||
<Tab.List as="span" className="inline-block rounded bg-brand-surface-2 p-1">
|
||||
<Tab.List as="span" className="inline-block rounded bg-gray-200 p-1">
|
||||
{tabOptions.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
|
||||
selected ? "bg-brand-accent text-white" : "text-brand-base"
|
||||
selected ? "bg-theme text-white" : "text-black"
|
||||
}`
|
||||
}
|
||||
>
|
||||
+6
-6
@@ -110,7 +110,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
@@ -124,9 +124,9 @@ export const ImageUploadModal: React.FC<Props> = ({
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 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-white px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
Upload Image
|
||||
</Dialog.Title>
|
||||
<div className="space-y-3">
|
||||
@@ -135,7 +135,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
||||
{...getRootProps()}
|
||||
className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-brand-base hover:border-gray-400"
|
||||
? "border-2 border-dashed border-gray-300 hover:border-gray-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
@@ -143,7 +143,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-brand-surface-1 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-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -157,7 +157,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
||||
) : (
|
||||
<>
|
||||
<UserCircleIcon className="mx-auto h-16 w-16 text-gray-400" />
|
||||
<span className="mt-2 block text-sm font-medium text-brand-base">
|
||||
<span className="mt-2 block text-sm font-medium text-gray-900">
|
||||
{isDragActive
|
||||
? "Drop image here to upload"
|
||||
: "Drag & drop image here"}
|
||||
@@ -11,4 +11,3 @@ export * from "./link-modal";
|
||||
export * from "./image-picker-popover";
|
||||
export * from "./filter-list";
|
||||
export * from "./feeds";
|
||||
export * from "./theme-switch";
|
||||
@@ -0,0 +1,267 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { SelectFilters } from "components/views";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon, ListBulletIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { Properties } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
export const IssuesFilterView: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
} = useIssuesView();
|
||||
|
||||
const [properties, setProperties] = useIssuesProperties(
|
||||
workspaceSlug as string,
|
||||
projectId as string
|
||||
);
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||
issueView === "list" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => setIssueView("list")}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||
issueView === "kanban" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => setIssueView("kanban")}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||
issueView === "calendar" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => setIssueView("calendar")}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists) {
|
||||
setFilters(
|
||||
{
|
||||
...(filters ?? {}),
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
} else {
|
||||
setFilters(
|
||||
{
|
||||
...(filters ?? {}),
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center gap-2 rounded-md border bg-transparent px-3 py-1.5 text-xs hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${
|
||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
View
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
|
||||
<div className="relative divide-y-2">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-gray-600">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) =>
|
||||
issueView === "kanban" && option.key === null ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-gray-600">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-gray-600">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-gray-600">Show empty states</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
showEmptyGroups ? "bg-green-500" : "bg-gray-200"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showEmptyGroups}
|
||||
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
>
|
||||
<span className="sr-only">Show empty groups</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-theme"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-gray-600">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-theme bg-theme text-white"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -280,17 +280,6 @@ export const IssuesView: React.FC<Props> = ({
|
||||
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
||||
);
|
||||
|
||||
const addIssueToDate = useCallback(
|
||||
(date: string) => {
|
||||
setCreateIssueModal(true);
|
||||
setPreloadedData({
|
||||
target_date: date,
|
||||
actionType: "createIssue",
|
||||
});
|
||||
},
|
||||
[setCreateIssueModal, setPreloadedData]
|
||||
);
|
||||
|
||||
const makeIssueCopy = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setCreateIssueModal(true);
|
||||
@@ -326,6 +315,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
cycleId as string,
|
||||
bridgeId
|
||||
)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
@@ -346,6 +338,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
moduleId as string,
|
||||
bridgeId
|
||||
)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
@@ -396,48 +391,49 @@ export const IssuesView: React.FC<Props> = ({
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-2 ${
|
||||
issueView === "list" ? (areFiltersApplied ? "mt-6 px-8" : "") : "-mt-2"
|
||||
}`}
|
||||
>
|
||||
<FilterList filters={filters} setFilters={setFilters} />
|
||||
{issueView !== "calendar" && (
|
||||
<>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-2 ${
|
||||
issueView === "list" && areFiltersApplied ? "px-8 mt-6" : "-mt-2"
|
||||
}`}
|
||||
>
|
||||
<FilterList filters={filters} setFilters={setFilters} />
|
||||
{areFiltersApplied && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
setFilters({}, true);
|
||||
setToastAlert({
|
||||
title: "View updated",
|
||||
message: "Your view has been updated",
|
||||
type: "success",
|
||||
});
|
||||
} else
|
||||
setCreateViewModal({
|
||||
query: filters,
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
{!viewId && <PlusIcon className="h-4 w-4" />}
|
||||
{viewId ? "Update" : "Save"} view
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
{areFiltersApplied && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
setFilters({}, true);
|
||||
setToastAlert({
|
||||
title: "View updated",
|
||||
message: "Your view has been updated",
|
||||
type: "success",
|
||||
});
|
||||
} else
|
||||
setCreateViewModal({
|
||||
query: filters,
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
{!viewId && <PlusIcon className="h-4 w-4" />}
|
||||
{viewId ? "Update" : "Save"} view
|
||||
</PrimaryButton>
|
||||
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
|
||||
)}
|
||||
</div>
|
||||
{areFiltersApplied && (
|
||||
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
|
||||
)}
|
||||
</>
|
||||
|
||||
</>
|
||||
)}
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<StrictModeDroppable droppableId="trashBox">
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`${
|
||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||
} fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-red-500/20 p-3 text-xs font-medium italic text-red-500 ${
|
||||
snapshot.isDraggingOver ? "bg-red-500/100 text-white" : ""
|
||||
} fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${
|
||||
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
|
||||
} duration-200`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
@@ -491,7 +487,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : (
|
||||
<CalendarView addIssueToDate={addIssueToDate} />
|
||||
<CalendarView />
|
||||
)}
|
||||
</>
|
||||
) : type === "issue" ? (
|
||||
@@ -512,8 +508,8 @@ export const IssuesView: React.FC<Props> = ({
|
||||
title="Create a new issue"
|
||||
description={
|
||||
<span>
|
||||
Use <pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>{" "}
|
||||
shortcut to create a new issue
|
||||
Use <pre className="inline rounded bg-gray-200 px-2 py-1">C</pre> shortcut to
|
||||
create a new issue
|
||||
</span>
|
||||
}
|
||||
Icon={PlusIcon}
|
||||
@@ -56,7 +56,7 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
@@ -70,11 +70,11 @@ export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-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">
|
||||
<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">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
Add Link
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 space-y-3">
|
||||
+1
-1
@@ -36,7 +36,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
{groupedByIssues && (
|
||||
<div>
|
||||
<div className="flex flex-col space-y-5 bg-white">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
+139
-138
@@ -44,6 +44,7 @@ import {
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
} from "constants/fetch-keys";
|
||||
import { DIVIDER } from "@blueprintjs/core/lib/esm/common/classes";
|
||||
|
||||
type Props = {
|
||||
type?: string;
|
||||
@@ -215,149 +216,149 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</ContextMenu.Item>
|
||||
</a>
|
||||
</ContextMenu>
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 border-b border-brand-base bg-brand-base px-4 py-2.5 last:border-b-0"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
||||
}}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span className="text-[0.825rem] text-brand-base">
|
||||
{truncateText(issue.name, 50)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.due_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex items-center gap-1 rounded-md border border-brand-base px-3 py-1 text-xs text-brand-secondary shadow-sm">
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && issue.label_details.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
|
||||
<div className="border-b mx-6 border-gray-300 last:border-b-0">
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 py-3"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
||||
}}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-gray-400">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span className="text-sm text-gray-800">{truncateText(issue.name, 50)}</span>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.due_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && issue.label_details.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.link && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
|
||||
{issue.link_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.attachment_count && (
|
||||
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
|
||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-gray-500" />
|
||||
{issue.attachment_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{type !== "issue" && removeIssue && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.link && (
|
||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
||||
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
|
||||
{issue.link_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{properties.attachment_count && (
|
||||
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
|
||||
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
|
||||
{issue.attachment_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<span>Remove from {type}</span>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
{type !== "issue" && removeIssue && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<span>Remove from {type}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
+16
-14
@@ -104,17 +104,17 @@ export const SingleList: React.FC<Props> = ({
|
||||
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
|
||||
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor);
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center");
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||
className="h-[18px] w-[18px] flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: labelColor }}
|
||||
/>
|
||||
);
|
||||
@@ -130,23 +130,27 @@ export const SingleList: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure as="div" defaultOpen>
|
||||
<Disclosure key={groupTitle} as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-4 py-2.5">
|
||||
<div className="bg-white">
|
||||
<div
|
||||
className={`flex items-center justify-between bg-gray-100 px-5 py-3 ${
|
||||
open ? "" : "rounded-[10px]"
|
||||
}`}
|
||||
>
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-3">
|
||||
{selectedGroup !== null && (
|
||||
<div className="flex items-center">{getGroupIcon()}</div>
|
||||
<span className="flex items-center">{getGroupIcon()}</span>
|
||||
)}
|
||||
{selectedGroup !== null ? (
|
||||
<h2 className="text-sm font-semibold capitalize leading-6 text-brand-base">
|
||||
<h2 className="text-base font-semibold capitalize leading-6 text-gray-800">
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="font-medium leading-5">All Issues</h2>
|
||||
)}
|
||||
<span className="text-brand-2 min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs">
|
||||
<span className="rounded-full bg-gray-200 py-0.5 px-3 text-sm text-black">
|
||||
{groupedByIssues[groupTitle as keyof IIssue].length}
|
||||
</span>
|
||||
</div>
|
||||
@@ -154,7 +158,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-brand-secondary hover:bg-brand-surface-2"
|
||||
className="p-1 text-gray-500 hover:bg-gray-100"
|
||||
onClick={addIssueToState}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
@@ -164,7 +168,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
) : (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="flex cursor-pointer items-center">
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
}
|
||||
@@ -211,9 +215,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="bg-brand-base px-4 py-2.5 text-sm text-brand-secondary">
|
||||
No issues.
|
||||
</p>
|
||||
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user