Compare commits

..

2 Commits

Author SHA1 Message Date
sriram veeraghanta 621fcfc041 removing trubo 2023-04-21 19:37:13 -04:00
sriram veeraghanta 1538b99a28 removing trubo 2023-04-21 19:30:36 -04:00
603 changed files with 10247 additions and 4121 deletions
+1 -3
View File
@@ -1,10 +1,8 @@
module.exports = {
root: true,
// This tells ESLint to load the config from the package `eslint-config-custom`
extends: ["custom"],
settings: {
next: {
rootDir: ["apps/*"],
rootDir: ["app/"],
},
},
};
+1 -35
View File
@@ -3,15 +3,7 @@ import uuid
import random
from django.contrib.auth.hashers import make_password
from plane.db.models import ProjectIdentifier
from plane.db.models import (
Issue,
IssueComment,
User,
Project,
ProjectMember,
Label,
Integration,
)
from plane.db.models import Issue, IssueComment, User, Project, ProjectMember, Label
# Update description and description html values for old descriptions
@@ -182,29 +174,3 @@ def update_label_color():
except Exception as e:
print(e)
print("Failed")
def create_slack_integration():
try:
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_integration_verified():
try:
integrations = Integration.objects.all()
updated_integrations = []
for integration in integrations:
integration.verified = True
updated_integrations.append(integration)
Integration.objects.bulk_update(
updated_integrations, ["verified"], batch_size=10
)
print("Sucess")
except Exception as e:
print(e)
print("Failed")
@@ -62,7 +62,6 @@ from .integration import (
GithubRepositorySerializer,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
SlackProjectSyncSerializer,
)
from .importer import ImporterSerializer
@@ -5,4 +5,3 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
from .slack import SlackProjectSyncSerializer
@@ -1,14 +0,0 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.db.models import SlackProjectSync
class SlackProjectSyncSerializer(BaseSerializer):
class Meta:
model = SlackProjectSync
fields = "__all__"
read_only_fields = [
"project",
"workspace",
"workspace_integration",
]
+6 -21
View File
@@ -79,6 +79,7 @@ from plane.api.views import (
## End Issues
# States
StateViewSet,
StateDeleteIssueCheckEndpoint,
## End States
# Estimates
ProjectEstimatePointEndpoint,
@@ -131,7 +132,6 @@ from plane.api.views import (
GithubIssueSyncViewSet,
GithubCommentSyncViewSet,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
## End Integrations
# Importer
ServiceIssueImportSummaryEndpoint,
@@ -508,6 +508,11 @@ 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
path(
@@ -1211,26 +1216,6 @@ urlpatterns = [
),
),
## End Github Integrations
# Slack Integration
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
SlackProjectSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
SlackProjectSyncViewSet.as_view(
{
"delete": "destroy",
"get": "retrieve",
}
),
),
## End Slack Integration
## End Integrations
# Importer
path(
+1 -2
View File
@@ -42,7 +42,7 @@ from .workspace import (
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
)
from .state import StateViewSet
from .state import StateViewSet, StateDeleteIssueCheckEndpoint
from .shortcut import ShortCutViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import (
@@ -106,7 +106,6 @@ from .integration import (
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
)
from .importer import (
+2 -40
View File
@@ -48,28 +48,6 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
)
def perform_destroy(self, instance):
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in cycle_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
subquery = CycleFavorite.objects.filter(
user=self.request.user,
@@ -203,22 +181,6 @@ class CycleIssueViewSet(BaseViewSet):
cycle_id=self.kwargs.get("cycle_id"),
)
def perform_destroy(self, instance):
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
return self.filter_queryset(
super()
@@ -324,9 +286,9 @@ class CycleIssueViewSet(BaseViewSet):
# Get all CycleIssues already created
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
records_to_update = []
update_cycle_issue_activity = []
record_to_create = []
records_to_update = []
for issue in issues:
cycle_issue = [
@@ -371,7 +333,7 @@ class CycleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
type="cycle.activity.created",
type="issue.activity.updated",
requested_data=json.dumps({"cycles_list": issues}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
+40 -84
View File
@@ -28,7 +28,6 @@ from plane.db.models import (
Module,
ModuleLink,
ModuleIssue,
Label,
)
from plane.api.serializers import (
ImporterSerializer,
@@ -236,20 +235,9 @@ class ImportServiceEndpoint(BaseAPIView):
def delete(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
importer = Importer.objects.filter(
pk=pk, service=service, workspace__slug=slug
)
# Delete all imported Issues
imported_issues = importer.imported_data.get("issues", [])
Issue.objects.filter(id__in=imported_issues).delete()
# Delete all imported Labels
imported_labels = importer.imported_data.get("labels", [])
Label.objects.filter(id__in=imported_labels).delete()
if importer.service == "jira":
imported_modules = importer.imported_data.get("modules", [])
Module.objects.filter(id__in=imported_modules).delete()
importer.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
@@ -259,27 +247,6 @@ class ImportServiceEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
def patch(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
serializer = ImporterSerializer(importer, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Importer.DoesNotExist:
return Response(
{"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UpdateServiceImportStatusEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service, importer_id):
@@ -520,59 +487,48 @@ class BulkImportModulesEndpoint(BaseAPIView):
ignore_conflicts=True,
)
modules = Module.objects.filter(id__in=[module.id for module in modules])
_ = ModuleLink.objects.bulk_create(
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get("url", "https://plane.so"),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
if len(modules) == len(modules_data):
_ = ModuleLink.objects.bulk_create(
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get(
"url", "https://plane.so"
),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
bulk_module_issues = []
for module, module_data in zip(modules, modules_data):
module_issues_list = module_data.get("module_issues_list", [])
bulk_module_issues = bulk_module_issues + [
ModuleIssue(
issue_id=issue,
module=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for issue in module_issues_list
]
bulk_module_issues = []
for module, module_data in zip(modules, modules_data):
module_issues_list = module_data.get("module_issues_list", [])
bulk_module_issues = bulk_module_issues + [
ModuleIssue(
issue_id=issue,
module=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for issue in module_issues_list
]
_ = ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=100, ignore_conflicts=True
)
_ = ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=100, ignore_conflicts=True
)
serializer = ModuleSerializer(modules, many=True)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
else:
return Response(
{"message": "Modules created but issues could not be imported"},
status=status.HTTP_200_OK,
)
serializer = ModuleSerializer(modules, many=True)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
@@ -6,4 +6,3 @@ from .github import (
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
)
from .slack import SlackProjectSyncViewSet
+9 -18
View File
@@ -27,7 +27,6 @@ from plane.utils.integrations.github import (
)
from plane.api.permissions import WorkSpaceAdminPermission
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
model = Integration
@@ -102,6 +101,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
WorkSpaceAdminPermission,
]
def get_queryset(self):
return (
super()
@@ -112,30 +112,21 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
def create(self, request, slug, provider):
try:
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
integration = Integration.objects.get(provider=provider)
config = {}
if provider == "github":
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
metadata = get_github_metadata(installation_id)
config = {"installation_id": installation_id}
if provider == "slack":
metadata = request.data.get("metadata", {})
access_token = metadata.get("access_token", False)
team_id = metadata.get("team", {}).get("id", False)
if not metadata or not access_token or not team_id:
return Response(
{"error": "Access token and team id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
config = {"team_id": team_id, "access_token": access_token}
# Create a bot user
bot_user = User.objects.create(
email=f"{uuid.uuid4().hex}@plane.so",
@@ -1,59 +0,0 @@
# Django import
from django.db import IntegrityError
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
from plane.api.serializers import SlackProjectSyncSerializer
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
class SlackProjectSyncViewSet(BaseViewSet):
permission_classes = [
ProjectBasePermission,
]
serializer_class = SlackProjectSyncSerializer
model = SlackProjectSync
def create(self, request, slug, project_id, workspace_integration_id):
try:
serializer = SlackProjectSyncSerializer(data=request.data)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if serializer.is_valid():
serializer.save(
project_id=project_id,
workspace_integration_id=workspace_integration_id,
)
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id, workspace__slug=slug
)
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+1 -39
View File
@@ -109,28 +109,6 @@ class ModuleViewSet(BaseViewSet):
.order_by("-is_favorite", "name")
)
def perform_destroy(self, instance):
module_issues = list(
ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def create(self, request, slug, project_id):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id)
@@ -180,22 +158,6 @@ class ModuleIssueViewSet(BaseViewSet):
module_id=self.kwargs.get("module_id"),
)
def perform_destroy(self, instance):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("module_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
return self.filter_queryset(
super()
@@ -340,7 +302,7 @@ class ModuleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
type="module.activity.created",
type="issue.activity.updated",
requested_data=json.dumps({"modules_list": issues}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
-30
View File
@@ -96,36 +96,6 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, pk):
try:
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# Only update access if the page owner is the requesting user
if (
page.access != request.data.get("access", page.access)
and page.owned_by_id != request.user.id
):
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = PageSerializer(page, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Page.DoesNotExist:
return Response(
{"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class PageBlockViewSet(BaseViewSet):
serializer_class = PageBlockSerializer
+19
View File
@@ -103,3 +103,22 @@ class StateViewSet(BaseViewSet):
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,
)
+1 -11
View File
@@ -27,7 +27,6 @@ from plane.db.models import (
User,
)
from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_email
@shared_task
@@ -41,7 +40,7 @@ def service_importer(service, importer_id):
# Check if we need to import users as well
if len(users):
# For all invited users create the users
# For all invited users create the uers
new_users = User.objects.bulk_create(
[
User(
@@ -57,15 +56,6 @@ def service_importer(service, importer_id):
ignore_conflicts=True,
)
[
send_welcome_email.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
+115 -175
View File
@@ -506,6 +506,119 @@ def track_blockings(
)
def track_cycles(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
# Updated Records:
updated_records = current_instance.get("updated_cycle_issues", [])
created_records = json.loads(current_instance.get("created_cycle_issues", []))
for updated_record in updated_records:
old_cycle = Cycle.objects.filter(
pk=updated_record.get("old_cycle_id", None)
).first()
new_cycle = Cycle.objects.filter(
pk=updated_record.get("new_cycle_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_cycle.name,
new_value=new_cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
)
)
for created_record in created_records:
cycle = Cycle.objects.filter(
pk=created_record.get("fields").get("cycle")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added cycle {cycle.name}",
new_identifier=cycle.id,
)
)
def track_modules(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
# Updated Records:
updated_records = current_instance.get("updated_module_issues", [])
created_records = json.loads(current_instance.get("created_module_issues", []))
for updated_record in updated_records:
old_module = Module.objects.filter(
pk=updated_record.get("old_module_id", None)
).first()
new_module = Module.objects.filter(
pk=updated_record.get("new_module_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_module.name,
new_value=new_module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}",
old_identifier=old_module.id,
new_identifier=new_module.id,
)
)
for created_record in created_records:
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added module {module.name}",
new_identifier=module.id,
)
)
def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
@@ -570,6 +683,8 @@ def update_issue_activity(
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"cycles_list": track_cycles,
"modules_list": track_modules,
"estimate_point": track_estimate_points,
}
@@ -673,177 +788,6 @@ def delete_comment_activity(
)
def create_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_cycle_issues", [])
created_records = json.loads(current_instance.get("created_cycle_issues", []))
for updated_record in updated_records:
old_cycle = Cycle.objects.filter(
pk=updated_record.get("old_cycle_id", None)
).first()
new_cycle = Cycle.objects.filter(
pk=updated_record.get("new_cycle_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_cycle.name,
new_value=new_cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
)
)
for created_record in created_records:
cycle = Cycle.objects.filter(
pk=created_record.get("fields").get("cycle")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=cycle.name,
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added cycle {cycle.name}",
new_identifier=cycle.id,
)
)
def delete_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
cycle_id = requested_data.get("cycle_id", "")
cycle = Cycle.objects.filter(pk=cycle_id).first()
issues = requested_data.get("issues")
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=cycle.name if cycle is not None else "",
new_value="",
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {cycle.name if cycle is not None else None}",
old_identifier=cycle.id if cycle is not None else None,
)
)
def create_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_module_issues", [])
created_records = json.loads(current_instance.get("created_module_issues", []))
for updated_record in updated_records:
old_module = Module.objects.filter(
pk=updated_record.get("old_module_id", None)
).first()
new_module = Module.objects.filter(
pk=updated_record.get("new_module_id", None)
).first()
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor=actor,
verb="updated",
old_value=old_module.name,
new_value=new_module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}",
old_identifier=old_module.id,
new_identifier=new_module.id,
)
)
for created_record in created_records:
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).first()
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} added module {module.name}",
new_identifier=module.id,
)
)
def delete_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
module_id = requested_data.get("module_id", "")
module = Module.objects.filter(pk=module_id).first()
issues = requested_data.get("issues")
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=module.name if module is not None else "",
new_value="",
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {module.name if module is not None else None}",
old_identifier=module.id if module is not None else None,
)
)
def create_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
@@ -966,10 +910,6 @@ def issue_activity(
"comment.activity.created": create_comment_activity,
"comment.activity.updated": update_comment_activity,
"comment.activity.deleted": delete_comment_activity,
"cycle.activity.created": create_cycle_issue_activity,
"cycle.activity.deleted": delete_cycle_issue_activity,
"module.activity.created": create_module_issue_activity,
"module.activity.deleted": delete_module_issue_activity,
"link.activity.created": create_link_activity,
"link.activity.updated": update_link_activity,
"link.activity.deleted": delete_link_activity,
@@ -1,56 +0,0 @@
# Django imports
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import User
@shared_task
def send_welcome_email(user_id, created, message):
try:
instance = User.objects.get(pk=user_id)
if created and not instance.is_bot:
first_name = instance.first_name.capitalize()
to_email = instance.email
from_email_string = settings.EMAIL_FROM
subject = f"Welcome to Plane ✈️!"
context = {"first_name": first_name, "email": instance.email}
html_content = render_to_string(
"emails/auth/user_welcome_email.html", context
)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(
subject, text_content, from_email_string, [to_email]
)
msg.attach_alternative(html_content, "text/html")
msg.send()
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=message,
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
capture_exception(e)
return
@@ -1,58 +0,0 @@
# Generated by Django 3.2.18 on 2023-05-01 19:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0028_auto_20230414_1703'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='importer',
name='imported_data',
field=models.JSONField(null=True),
),
migrations.AddField(
model_name='module',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.CreateModel(
name='SlackProjectSync',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('access_token', models.CharField(max_length=300)),
('scopes', models.TextField()),
('bot_user_id', models.CharField(max_length=50)),
('webhook_url', models.URLField(max_length=1000)),
('data', models.JSONField(default=dict)),
('team_id', models.CharField(max_length=30)),
('team_name', models.CharField(max_length=300)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_slackprojectsync', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_slackprojectsync', to='db.workspace')),
('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slack_syncs', to='db.workspaceintegration')),
],
options={
'verbose_name': 'Slack Project Sync',
'verbose_name_plural': 'Slack Project Syncs',
'db_table': 'slack_project_syncs',
'ordering': ('-created_at',),
'unique_together': {('team_id', 'project')},
},
),
]
-1
View File
@@ -59,7 +59,6 @@ from .integration import (
GithubRepositorySync,
GithubIssueSync,
GithubCommentSync,
SlackProjectSync,
)
from .importer import Importer
-1
View File
@@ -16,7 +16,6 @@ class Cycle(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="owned_by_cycle",
)
view_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"
-1
View File
@@ -33,7 +33,6 @@ class Importer(ProjectBaseModel):
token = models.ForeignKey(
"db.APIToken", on_delete=models.CASCADE, related_name="importer"
)
imported_data = models.JSONField(null=True)
class Meta:
verbose_name = "Importer"
@@ -1,3 +1,2 @@
from .base import Integration, WorkspaceIntegration
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
from .slack import SlackProjectSync
@@ -1,32 +0,0 @@
# Python imports
import uuid
# Django imports
from django.db import models
# Module imports
from plane.db.models import ProjectBaseModel
class SlackProjectSync(ProjectBaseModel):
access_token = models.CharField(max_length=300)
scopes = models.TextField()
bot_user_id = models.CharField(max_length=50)
webhook_url = models.URLField(max_length=1000)
data = models.JSONField(default=dict)
team_id = models.CharField(max_length=30)
team_name = models.CharField(max_length=300)
workspace_integration = models.ForeignKey(
"db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE
)
def __str__(self):
"""Return the repo name"""
return f"{self.project.name}"
class Meta:
unique_together = ["team_id", "project"]
verbose_name = "Slack Project Sync"
verbose_name_plural = "Slack Project Syncs"
db_table = "slack_project_syncs"
ordering = ("-created_at",)
-1
View File
@@ -39,7 +39,6 @@ class Module(ProjectBaseModel):
through="ModuleMember",
through_fields=("module", "member"),
)
view_props = models.JSONField(default=dict)
class Meta:
unique_together = ["name", "project"]
+1 -1
View File
@@ -80,7 +80,7 @@ AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = 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 = ""
-12
View File
@@ -13,17 +13,6 @@ def filter_state(params, filter, method):
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
if len(estimate_points) and "" not in estimate_points:
filter["estimate_point__in"] = estimate_points
else:
if params.get("estimate_point", None) and len(params.get("estimate_point")):
filter["estimate_point__in"] = params.get("estimate_point")
return filter
def filter_priority(params, filter, method):
if method == "GET":
priorties = params.get("priority").split(",")
@@ -203,7 +192,6 @@ def issue_filters(query_params, method):
ISSUE_FILTER = {
"state": filter_state,
"estimate_point": filter_estimate_point,
"priority": filter_priority,
"parent": filter_parent,
"labels": filter_labels,
@@ -9,4 +9,3 @@ NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_SENTRY=0
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
NEXT_PUBLIC_TRACK_EVENTS=0
NEXT_PUBLIC_SLACK_CLIENT_ID=""
@@ -1,12 +1,7 @@
module.exports = {
extends: ["next", "turbo", "prettier"],
extends: ["next", "prettier"],
parser: "@typescript-eslint/parser",
plugins: ["react", "@typescript-eslint"],
settings: {
next: {
rootDir: ["app/", "docs/", "packages/*/"],
},
},
rules: {
"@next/next/no-html-link-for-pages": "off",
"react/jsx-key": "off",
@@ -4,9 +4,10 @@ RUN apk update
# Set working directory
WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
EXPOSE 3000
CMD ["yarn","dev"]
+30
View File
@@ -0,0 +1,30 @@
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
@@ -92,13 +92,13 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
<>
<form className="space-y-5 py-5 px-5">
{(codeSent || codeResent) && (
<div className="rounded-md bg-green-500/20 p-4">
<div className="rounded-md bg-green-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
<CheckCircleIcon className="h-5 w-5 text-green-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-500">
<p className="text-sm font-medium text-green-800">
{codeResent
? "Please check your mail for new code."
: "Please check your mail for code."}
@@ -141,9 +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-brand-secondary"
: "cursor-pointer text-brand-accent"
isResendDisabled ? "cursor-default text-gray-400" : "cursor-pointer text-brand-accent"
} `}
onClick={() => {
setIsCodeResending(true);
@@ -176,8 +174,7 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
className="w-full text-center"
size="md"
onClick={handleSubmit(handleSignin)}
disabled={!isValid && isDirty}
loading={isSubmitting}
loading={isSubmitting || (!isValid && isDirty)}
>
{isSubmitting ? "Signing in..." : "Sign in"}
</PrimaryButton>
@@ -94,9 +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-brand-accent">
Forgot your password?
</a>
<a className="font-medium text-brand-accent hover:text-indigo-500">Forgot your password?</a>
</Link>
</div>
</div>
@@ -104,8 +102,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<SecondaryButton
type="submit"
className="w-full text-center"
disabled={!isValid && isDirty}
loading={isSubmitting}
loading={isSubmitting || (!isValid && isDirty)}
>
{isSubmitting ? "Signing in..." : "Sign In"}
</SecondaryButton>
@@ -33,11 +33,11 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
}, []);
return (
<div className="w-full px-1">
<div className="px-1 w-full">
<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-brand-secondary duration-300 hover:bg-brand-surface-2">
<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">
<Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" />
<span>Sign In with Github</span>
</button>
@@ -47,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
return (
<>
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
<div className="overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
<div className="h-12" id="googleSignInButton" ref={googleSignInButton} />
</>
);
};
@@ -36,14 +36,16 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
alt="ProjectSettingImg"
/>
</div>
<h1 className="text-xl font-medium">Oops! You are not authorized to view this page</h1>
<h1 className="text-xl font-medium text-brand-base">
Oops! You are not authorized to view this page
</h1>
<div className="w-full max-w-md text-base text-brand-secondary">
<div className="w-full text-base text-brand-secondary max-w-md ">
{user ? (
<p>
You have signed in as {user.email}. <br />
<Link href={`/signin?next=${currentPath}`}>
<a className="font-medium text-brand-base">Sign in</a>
<a className="text-brand-base font-medium">Sign in</a>
</Link>{" "}
with different account that has access to this page.
</p>
@@ -51,7 +53,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
<p>
You need to{" "}
<Link href={`/signin?next=${currentPath}`}>
<a className="font-medium text-brand-base">Sign in</a>
<a className="text-brand-base font-medium">Sign in</a>
</Link>{" "}
with an account that has access to this page.
</p>
@@ -45,9 +45,9 @@ export const JoinProject: React.FC = () => {
<div className="h-44 w-72">
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
</div>
<h1 className="text-xl font-medium">You are not a member of this project</h1>
<h1 className="text-xl font-medium text-gray-900">You are not a member of this project</h1>
<div className="w-full max-w-md text-base text-brand-secondary">
<div className="w-full max-w-md text-base text-gray-500 ">
<p className="mx-auto w-full text-sm md:w-3/4">
You are not a member of this project, but you can join this project by clicking the button
below.
@@ -20,12 +20,12 @@ export const NotAWorkspaceMember = () => {
<div className="space-y-8 text-center">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Not Authorized!</h3>
<p className="mx-auto w-1/2 text-sm text-brand-secondary">
<p className="text-sm text-gray-500 w-1/2 mx-auto">
You{"'"}re not a member of this workspace. Please contact the workspace admin to get
an invitation or check your pending invitations.
</p>
</div>
<div className="flex items-center justify-center gap-2">
<div className="flex items-center gap-2 justify-center">
<Link href="/invitations">
<a>
<SecondaryButton>Check pending invites</SecondaryButton>
@@ -14,7 +14,7 @@ import stateService from "services/state.service";
// types
import { IIssue } from "types";
// fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATE_LIST } from "constants/fetch-keys";
// icons
import { CheckIcon, getStateGroupIcon } from "components/icons";
@@ -28,7 +28,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
const { workspaceSlug, projectId, issueId } = router.query;
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
@@ -393,7 +393,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-surface-2 shadow-2xl transition-all">
<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">
<Command
filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
@@ -675,7 +675,7 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="Page">
<Command.Item onSelect={createNewPage} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary">
<div className="flex items-center gap-2 text-gray-700">
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
Create new page
</div>
@@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { CommandIcon } from "components/icons";
import { MacCommandIcon } from "components/icons";
// ui
import { Input } from "components/ui";
@@ -123,23 +123,17 @@ 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-brand-secondary">{shortcut.description}</p>
<div className="flex items-center gap-x-2.5">
{shortcut.keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-2">
<MacCommandIcon />
</span>
) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</kbd>
) : (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key}
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-gray-800">
{key === "Ctrl" ? <MacCommandIcon /> : key}
</kbd>
)}
</span>
@@ -173,16 +167,12 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
{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-1.5 text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
<span className="flex h-full items-center rounded-sm border border-brand-base text-brand-secondary bg-brand-surface-1 p-2">
<MacCommandIcon />
</span>
) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</kbd>
) : (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key}
{key === "Ctrl" ? <MacCommandIcon /> : key}
</kbd>
)}
</span>
@@ -17,7 +17,7 @@ type Props = {
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
removeIssue: ((bridgeId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@@ -29,7 +29,7 @@ type Props = {
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
removeIssue: ((bridgeId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@@ -130,8 +130,7 @@ export const SingleBoard: React.FC<Props> = ({
handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox}
removeIssue={() => {
if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id);
}}
isCompleted={isCompleted}
userAuth={userAuth}
@@ -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-2 py-1 text-xs text-brand-secondary shadow-sm">
<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">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
@@ -391,8 +391,8 @@ 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">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
<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>
@@ -401,8 +401,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
{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="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
<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>
@@ -26,7 +26,7 @@ import {
import { Popover, Transition } from "@headlessui/react";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
import { CustomMenu, Spinner } from "components/ui";
// icon
import {
CheckIcon,
@@ -51,7 +51,6 @@ import { IIssue } from "types";
// constant
import { monthOptions, yearOptions } from "constants/calendar";
import modulesService from "services/modules.service";
import { getStateGroupIcon } from "components/icons";
type Props = {
addIssueToDate: (date: string) => void;
@@ -63,10 +62,9 @@ interface ICalendarRange {
}
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
const [showWeekEnds, setShowWeekEnds] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const [isMonthlyView, setIsMonthlyView] = useState(true);
const [showAllIssues, setShowAllIssues] = useState(false);
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false);
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -153,15 +151,15 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
const calendarIssues = cycleId
? (cycleCalendarIssues as IIssue[])
? cycleCalendarIssues
: moduleId
? (moduleCalendarIssues as IIssue[])
: (projectCalendarIssues as IIssue[]);
? moduleCalendarIssues
: projectCalendarIssues;
const currentViewDaysData = currentViewDays.map((date: Date) => {
const filterIssue =
calendarIssues && calendarIssues.length > 0
? calendarIssues.filter(
? (calendarIssues as IIssue[]).filter(
(issue) =>
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
)
@@ -326,7 +324,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
<div className="flex w-full items-center justify-end gap-2">
<button
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-4 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none"
onClick={() => {
if (isMonthlyView) {
updateDate(new Date());
@@ -339,12 +337,14 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
}
}}
>
Today
Today{" "}
</button>
<CustomMenu
customButton={
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
<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 `}
>
{isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
@@ -390,10 +390,23 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
</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">
<h4>Show weekends</h4>
<ToggleSwitch
value={showWeekEnds}
onChange={() => setShowWeekEnds(!showWeekEnds)}
/>
<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"
}`}
role="switch"
aria-checked={showWeekEnds}
onClick={() => setShowWeekEnds(!showWeekEnds)}
>
<span className="sr-only">Show weekends</span>
<span
aria-hidden="true"
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
showWeekEnds ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
</div>
</CustomMenu>
</div>
@@ -432,87 +445,61 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `}
>
{currentViewDaysData.map((date, index) => {
const totalIssues = date.issues.length;
return (
<StrictModeDroppable droppableId={date.date}>
{(provided) => (
<div
key={index}
ref={provided.innerRef}
{...provided.droppableProps}
className={`group relative flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
{currentViewDaysData.map((date, index) => (
<StrictModeDroppable droppableId={date.date}>
{(provided, snapshot) => (
<div
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 ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{totalIssues > 0 &&
date.issues
.slice(0, showAllIssues ? totalIssues : 4)
.map((issue: IIssue, index) => (
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full cursor-pointer truncate rounded border border-brand-base px-1.5 py-1 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
}`}
>
<Link
href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
>
<a className="flex w-full items-center gap-2">
{getStateGroupIcon(
issue.state_detail.group,
"12",
"12",
issue.state_detail.color
)}
{issue.name}
</a>
</Link>
</div>
)}
</Draggable>
))}
{totalIssues > 4 && (
<button
type="button"
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
onClick={() => setShowAllIssues((prevData) => !prevData)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
</button>
)}
<div
className={`absolute ${
isMonthlyView ? "bottom-2" : "top-2"
} right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
: (index + 1) % 5 === 0
? ""
: "border-r"
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{date.issues.length > 0 &&
date.issues.map((issue: IIssue, index) => (
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full cursor-pointer truncate rounded bg-brand-surface-2 p-1.5 hover:scale-105 ${
snapshot.isDragging ? "shadow-lg" : ""
}`}
>
<Link
href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}
className="w-full"
>
{issue.name}
</Link>
</div>
)}
</Draggable>
))}
<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)}
>
<button
className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)}
>
<PlusIcon className="h-3 w-3 text-brand-secondary" />
Add issue
</button>
</div>
{provided.placeholder}
<PlusIcon className="h-4 w-4 text-brand-secondary" />
Add new issue
</button>
</div>
)}
</StrictModeDroppable>
);
})}
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
))}
</div>
</div>
</DragDropContext>
@@ -117,7 +117,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-brand-backdrop 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-10 overflow-y-auto p-4 sm:p-6 md:p-20">
@@ -130,7 +130,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-brand-base bg-brand-base 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-brand-surface-2 shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<form>
<Controller
control={control}
@@ -151,26 +151,26 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
<Combobox.Options
static
className="max-h-80 scroll-py-2 divide-y divide-brand-base overflow-y-auto"
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mb-2 px-3 text-xs font-medium text-brand-base">
<h2 className="mb-2 px-3 text-xs font-semibold text-brand-base">
Select issues to add
</h2>
)}
<ul className="text-sm text-brand-base">
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue.id}
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
active ? "bg-brand-surface-2 text-brand-base" : ""
} ${selected ? "text-brand-base" : ""}`
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" : ""
}`
}
>
{({ selected }) => (
@@ -182,7 +182,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs">
<span className="flex-shrink-0 text-xs text-brand-secondary">
{issue.project_detail.identifier}-{issue.sequence_id}
</span>
{issue.name}
@@ -194,11 +194,10 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
</li>
) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="52" width="52" />
<h3 className="text-sm text-brand-secondary">
<LayerDiagonalIcon height="56" width="56" />
<h3 className="text-brand-secondary">
No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>
.
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
</h3>
</div>
)}
@@ -67,19 +67,15 @@ const activityDetails: {
},
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-brand-secondary" 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-brand-secondary" aria-hidden="true" />,
},
estimate_point: {
message: "set the estimate point to",
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
},
target_date: {
message: "set the due date to",
@@ -95,7 +91,7 @@ const activityDetails: {
},
estimate: {
message: "updated the estimate",
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
},
link: {
message: "updated the link",
@@ -157,11 +153,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
) {
const { workspace_detail, project, issue } = activity;
value = (
<span className="text-brand-secondary">
<span className="text-gray-600">
created{" "}
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
<a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
this issue. <ArrowTopRightOnSquareIcon className="h-3.5 w-3.5 ml-1" />
</a>
</Link>
</span>
@@ -202,7 +198,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
if (activity.field === "comment") {
return (
<div key={activity.id} className="mt-2">
<div key={activity.id}>
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
@@ -223,7 +219,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
<ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-brand-secondary"
className="h-3.5 w-3.5 text-gray-400"
aria-hidden="true"
/>
</span>
@@ -246,8 +242,9 @@ export const Feeds: React.FC<any> = ({ activities }) => (
: activity.old_value
}
editable={false}
onBlur={() => ({})}
noBorder
customClassName="text-xs border border-brand-base bg-brand-base"
customClassName="text-xs bg-brand-surface-1"
/>
</div>
</div>
@@ -271,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-2 ring-white">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-1 ring-white">
{activity.field ? (
activityDetails[activity.field as keyof typeof activityDetails]?.icon
) : activity.actor_detail.avatar &&
@@ -15,7 +15,7 @@ import issuesService from "services/issues.service";
import projectService from "services/project.service";
import stateService from "services/state.service";
// types
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
import { IIssueFilterOptions } from "types";
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
@@ -37,7 +37,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
);
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
@@ -59,7 +59,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
key={key}
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
>
<span className="capitalize text-brand-secondary">
<span className="font-medium capitalize text-brand-secondary">
{replaceUnderscoreIfSnakeCase(key)}:
</span>
{filters[key as keyof IIssueFilterOptions] === null ||
@@ -75,7 +75,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return (
<p
key={state?.id}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium text-white"
style={{
color: state?.color,
backgroundColor: `${state?.color}20`,
@@ -122,16 +122,16 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
{filters.priority?.map((priority: any) => (
<p
key={priority}
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium capitalize text-white ${
priority === "urgent"
? "bg-red-500/20 text-red-500"
? "bg-red-100 text-red-600 hover:bg-red-100"
: priority === "high"
? "bg-orange-500/20 text-orange-500"
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
: priority === "medium"
? "bg-yellow-500/20 text-yellow-500"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
: priority === "low"
? "bg-green-500/20 text-green-500"
: "bg-brand-surface-1 text-brand-secondary"
? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-brand-surface-1 text-gray-700 hover:bg-brand-surface-1"
}`}
>
<span>{getPriorityIcon(priority)}</span>
@@ -170,7 +170,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return (
<div
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize"
>
<Avatar user={member} />
<span>{member?.first_name}</span>
@@ -203,7 +203,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
<XMarkIcon className="h-3 w-3" />
</button>
</div>
) : key === "created_by" ? (
) : (key as keyof IIssueFilterOptions) === "created_by" ? (
<div className="flex flex-wrap items-center gap-1">
{filters.created_by?.map((memberId: string) => {
const member = members?.find((m) => m.member.id === memberId)?.member;
@@ -211,7 +211,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
return (
<div
key={`${memberId}-${key}`}
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize"
>
<Avatar user={member} />
<span>{member?.first_name}</span>
@@ -253,20 +253,25 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
const color = label.color !== "" ? label.color : "#0f172a";
return (
<div
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
style={{
color: color,
backgroundColor: `${color}20`, // add 20% opacity
background: `${color}33`, // add 20% opacity
}}
key={labelId}
>
<div
className="h-1.5 w-1.5 rounded-full"
className="h-2 w-2 rounded-full"
style={{
backgroundColor: color,
}}
/>
<span>{label.name}</span>
<span
style={{
color: color,
}}
>
{label.name}
</span>
<span
className="cursor-pointer"
onClick={() =>
@@ -336,8 +341,8 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
}
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
>
<span>Clear all filters</span>
<XMarkIcon className="h-3 w-3" />
<span className="font-medium">Clear all filters</span>
<XMarkIcon className="h-4 w-4" />
</button>
)}
</div>
@@ -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-brand-secondary"
className="rounded-md border border-brand-base bg-brand-surface-2 px-2 py-1 text-xs text-gray-700"
onClick={() => setIsOpen((prev) => !prev)}
>
{label}
@@ -79,7 +79,7 @@ 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 border border-brand-base bg-brand-surface-2 shadow-lg">
<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]">
<Tab.Group>
<Tab.List as="span" className="inline-block rounded bg-brand-surface-2 p-1">
@@ -10,7 +10,7 @@ import { Popover, Transition } from "@headlessui/react";
// components
import { SelectFilters } from "components/views";
// ui
import { CustomMenu, ToggleSwitch } from "components/ui";
import { CustomMenu } from "components/ui";
// icons
import {
ChevronDownIcon,
@@ -213,10 +213,23 @@ export const IssuesFilterView: React.FC = () => {
<>
<div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Show empty states</h4>
<ToggleSwitch
value={showEmptyGroups}
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
/>
<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()}>
@@ -246,7 +259,7 @@ export const IssuesFilterView: React.FC = () => {
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-brand-accent bg-brand-accent text-white"
? "border-brand-accent bg-brand-accent text-brand-base"
: "border-brand-base"
}`}
onClick={() => setProperties(key as keyof Properties)}
@@ -46,7 +46,7 @@ import {
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST,
STATE_LIST,
} from "constants/fetch-keys";
// image
@@ -103,7 +103,7 @@ export const IssuesView: React.FC<Props> = ({
} = useIssuesView();
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
@@ -314,26 +314,10 @@ export const IssuesView: React.FC<Props> = ({
);
const removeIssueFromCycle = useCallback(
(bridgeId: string, issueId: string) => {
(bridgeId: string) => {
if (!workspaceSlug || !projectId || !cycleId) return;
mutate(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
}
return filteredData;
} else {
const filteredData = prevData.filter((i: any) => i.id !== issueId);
return filteredData;
}
},
false
);
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
issuesService
.removeIssueFromCycle(
@@ -342,13 +326,6 @@ export const IssuesView: React.FC<Props> = ({
cycleId as string,
bridgeId
)
.then(() => {
setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
})
.catch((e) => {
console.log(e);
});
@@ -357,26 +334,10 @@ export const IssuesView: React.FC<Props> = ({
);
const removeIssueFromModule = useCallback(
(bridgeId: string, issueId: string) => {
(bridgeId: string) => {
if (!workspaceSlug || !projectId || !moduleId) return;
mutate(
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
}
return filteredData;
} else {
const filteredData = prevData.filter((item: any) => item.id !== issueId);
return filteredData;
}
},
false
);
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
modulesService
.removeIssueFromModule(
@@ -385,13 +346,6 @@ export const IssuesView: React.FC<Props> = ({
moduleId as string,
bridgeId
)
.then(() => {
setToastAlert({
title: "Success",
message: "Issue removed successfully.",
type: "success",
});
})
.catch((e) => {
console.log(e);
});
@@ -472,7 +426,7 @@ export const IssuesView: React.FC<Props> = ({
)}
</div>
{areFiltersApplied && (
<div className={`${issueView === "list" ? "mt-4" : "my-4"} border-t border-brand-base`} />
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
)}
</>
@@ -14,7 +14,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
removeIssue: ((bridgeId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@@ -216,7 +216,7 @@ export const SingleListIssue: React.FC<Props> = ({
</a>
</ContextMenu>
<div
className="flex flex-wrap items-center justify-between gap-2 border-b border-brand-base bg-brand-base last:border-b-0"
className="flex items-center justify-between gap-2 border-b border-brand-base bg-brand-base px-4 py-2.5 last:border-b-0"
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
@@ -224,28 +224,26 @@ export const SingleListIssue: React.FC<Props> = ({
}}
>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<div className="flex-grow cursor-pointer px-4 pt-2.5 md:py-2.5">
<a className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-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)}
<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>
</a>
</div>
)}
<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 w-full flex-shrink flex-wrap items-center gap-2 px-4 pb-2.5 text-xs sm:w-auto md:px-0 md:py-2.5 md:pr-4">
<div className="flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
@@ -270,7 +268,7 @@ export const SingleListIssue: React.FC<Props> = ({
/>
)}
{properties.sub_issue_count && (
<div className="flex items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
<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>
)}
@@ -313,8 +311,8 @@ export const SingleListIssue: 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">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
<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>
@@ -323,8 +321,8 @@ export const SingleListIssue: React.FC<Props> = ({
{properties.attachment_count && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
<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>
@@ -37,7 +37,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
removeIssue: ((bridgeId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@@ -204,7 +204,7 @@ export const SingleList: React.FC<Props> = ({
makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue}
removeIssue={() => {
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id);
}}
isCompleted={isCompleted}
userAuth={userAuth}
@@ -78,7 +78,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
return 2;
default:
return 0;
return 3;
}
};
return (
@@ -94,7 +94,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
return setTab("States");
default:
return setTab("Assignees");
return setTab("States");
}
}}
>
@@ -25,6 +25,7 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
const [showNoCurrentCycleMessage, setShowNoCurrentCycleMessage] = useState(true);
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
@@ -60,9 +61,14 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
))}
</div>
) : type === "current" ? (
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
<h3 className="text-base font-medium text-brand-base ">No current cycle is present.</h3>
</div>
showNoCurrentCycleMessage && (
<div className="flex items-center justify-between bg-brand-surface-2 w-full px-6 py-4 rounded-[10px]">
<h3 className="text-base font-medium text-brand-base "> No current cycle is present.</h3>
<button onClick={() => setShowNoCurrentCycleMessage(false)}>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
)
) : (
<EmptyState
type="cycle"
@@ -139,7 +139,7 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
@@ -153,36 +153,30 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-brand-surface-2 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-brand-base"
>
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
Delete Cycle
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-brand-secondary">
Are you sure you want to delete cycle-{" "}
<span className="break-all font-medium text-brand-base">
{data?.name}
</span>
? All of the data related to the cycle will be permanently removed. This
action cannot be undone.
<span className="font-bold">{data?.name}</span>? All of the data related
to the cycle will be permanently removed. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}
@@ -11,11 +11,7 @@ import useToast from "hooks/use-toast";
// ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import {
getDateRangeStatus,
isDateGreaterThanToday,
isDateRangeValid,
} from "helpers/date-time.helper";
import { getDateRangeStatus, isDateRangeValid } from "helpers/date-time.helper";
// types
import { ICycle } from "types";
@@ -64,33 +60,24 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
data?.start_date && data?.end_date ? getDateRangeStatus(data?.start_date, data?.end_date) : "";
const dateChecker = async (payload: any) => {
if (isDateGreaterThanToday(payload.end_date)) {
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
})
.catch((err) => {
console.log(err);
});
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
})
.catch((err) => {
console.log(err);
});
}
};
const checkEmptyDate =
@@ -113,6 +100,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<div className="space-y-3">
<div>
<Input
mode="transparent"
autoComplete="off"
id="name"
name="name"
@@ -136,6 +124,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
name="description"
placeholder="Description"
className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description}
register={register}
/>
@@ -164,8 +153,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
setToastAlert({
type: "error",
title: "Error!",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
message: "The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
@@ -196,8 +184,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
setToastAlert({
type: "error",
title: "Error!",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
message: "The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
@@ -220,8 +207,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
? "cursor-pointer"
: "cursor-not-allowed"
}
disabled={checkEmptyDate ? false : isDateValid ? false : true}
loading={isSubmitting}
loading={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true}
>
{status
? isSubmitting
@@ -151,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
@@ -164,7 +164,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<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">
<CycleForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}
@@ -141,25 +141,25 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<>
<div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm">
<div className="flex items-center">
<span className="flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-center text-xs capitalize">
<div className="flex items-center ">
<span
className={`flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-center text-sm capitalize text-brand-muted-1 `}
>
{capitalizeFirstLetter(cycleStatus)}
</span>
</div>
<div className="relative flex h-full w-52 items-center gap-2">
<Popover className="flex h-full items-center justify-center rounded-lg">
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-brand-muted-1">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
cycle.start_date ? "" : "text-brand-secondary"
className={`group flex h-full items-center gap-1 rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-brand-muted-1 ${
open ? "bg-brand-surface-1" : ""
}`}
>
<CalendarDaysIcon className="h-3 w-3" />
<span>
{renderShortDate(new Date(`${cycle?.start_date}`), "Start date")}
</span>
<span>{renderShortDate(new Date(`${cycle?.start_date}`))}</span>
</Popover.Button>
<Transition
@@ -209,20 +209,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
)}
</Popover>
<span>
<ArrowLongRightIcon className="h-3 w-3 text-brand-secondary" />
<ArrowLongRightIcon className="h-3 w-3" />
</span>
<Popover className="flex h-full items-center justify-center rounded-lg">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
cycle.end_date ? "" : "text-brand-secondary"
className={`group flex items-center gap-1 rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-brand-muted-1 ${
open ? "bg-brand-surface-1" : ""
}`}
>
<CalendarDaysIcon className="h-3 w-3" />
<CalendarDaysIcon className="h-3 w-3 " />
<span>{renderShortDate(new Date(`${cycle?.end_date}`), "End date")}</span>
<span>{renderShortDate(new Date(`${cycle?.end_date}`))}</span>
</Popover.Button>
<Transition
@@ -234,7 +234,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker
selected={
watch("end_date") ? new Date(`${watch("end_date")}`) : new Date()
@@ -275,9 +275,9 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
</div>
<div className="flex w-full flex-col gap-6 px-6 py-6">
<div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2">
<div className="flex flex-col gap-6 px-6 py-6 w-full">
<div className="flex flex-col items-start justify-start gap-2 w-full">
<div className="flex items-start justify-between gap-2 w-full">
<h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4>
<CustomMenu width="lg" ellipsis>
{!isCompleted && (
@@ -238,7 +238,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
return (
<div>
<div className="flex flex-col rounded-[10px] bg-brand-base text-xs shadow">
<div className="flex flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
@@ -269,20 +269,20 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
)}
</div>
<div className="flex items-center justify-start gap-5 text-brand-secondary">
<div className="flex items-center justify-start gap-5">
<div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4" />
<span>Start :</span>
<CalendarDaysIcon className="h-4 w-4 text-brand-base" />
<span className="text-brand-secondary">Start :</span>
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<div className="flex items-start gap-1 ">
<TargetIcon className="h-4 w-4" />
<span>End :</span>
<TargetIcon className="h-4 w-4 text-brand-base" />
<span className="text-brand-secondary">End :</span>
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</div>
<div className="mt-4 flex items-center justify-between text-brand-secondary">
<div className="flex items-center justify-between mt-4">
<div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
@@ -293,11 +293,11 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
alt={cycle.owned_by.first_name}
/>
) : (
<span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full capitalize">
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-brand-base capitalize bg-brand-secondary">
{cycle.owned_by.first_name.charAt(0)}
</span>
)}
<span>{cycle.owned_by.first_name}</span>
<span className="text-brand-base">{cycle.owned_by.first_name}</span>
</div>
<div className="flex items-center">
{!isCompleted && (
@@ -350,7 +350,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
<Disclosure>
{({ open }) => (
<div
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-brand-base bg-brand-surface-2 text-brand-secondary ${
className={`flex h-full w-full flex-col border-t border-brand-base bg-brand-surface-1 ${
open ? "" : "flex-row"
}`}
>
@@ -106,30 +106,30 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
<Dialog.Panel className="relative transform rounded-lg bg-brand-surface-1 py-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between px-5">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<TransferIcon className="h-4 w-5" color="#495057" />
<h4 className="text-xl font-medium text-brand-base">Transfer Issues</h4>
<h4 className="text-gray-700 font-medium text-[1.50rem]">Transfer Issues</h4>
</div>
<button onClick={handleClose}>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
<div className="flex items-center gap-2 border-b border-brand-base px-5 pb-3">
<div className="flex items-center gap-2 pb-3 px-5 border-b border-brand-base">
<MagnifyingGlassIcon className="h-4 w-4 text-brand-secondary" />
<input
className="bg-brand-surface-1 outline-none"
className="outline-none"
placeholder="Search for a cycle..."
onChange={(e) => setQuery(e.target.value)}
value={query}
/>
</div>
<div className="flex w-full flex-col items-start gap-2 px-5">
<div className="flex flex-col items-start w-full gap-2 px-5">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option: ICycle) => (
<button
key={option.id}
className="flex w-full items-center gap-4 rounded px-4 py-3 text-sm text-brand-secondary hover:bg-brand-surface-1"
className="flex items-center gap-4 px-4 py-3 text-gray-600 text-sm rounded w-full hover:bg-brand-surface-1"
onClick={() => {
transferIssue({
new_cycle_id: option?.id,
@@ -138,16 +138,16 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
}}
>
<ContrastIcon className="h-5 w-5" />
<div className="flex w-full justify-between">
<div className="flex justify-between w-full">
<span>{option?.name}</span>
<span className=" flex items-center rounded-full bg-brand-surface-2 px-2 capitalize">
<span className=" flex bg-gray-200 capitalize px-2 rounded-full items-center">
{getDateRangeStatus(option?.start_date, option?.end_date)}
</span>
</div>
</button>
))
) : (
<div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
<div className="flex items-center justify-center gap-4 p-5 text-sm w-full">
<ExclamationIcon height={14} width={14} />
<span className="text-center text-brand-secondary">
You dont have any current cycle. Please create one to transfer the
@@ -37,7 +37,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
: 0;
return (
<div className="-mt-2 mb-4 flex items-center justify-between">
<div className="flex items-center justify-between -mt-4 mb-4">
<div className="flex items-center gap-2 text-sm text-brand-secondary">
<ExclamationIcon height={14} width={14} />
<span>Completed cycles are not editable.</span>
@@ -46,7 +46,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
{transferableIssuesCount > 0 && (
<div>
<PrimaryButton onClick={handleClick} className="flex items-center gap-3 rounded-lg">
<TransferIcon className="h-4 w-4" color="white" />
<TransferIcon className="h-4 w-4" color="white"/>
<span className="text-white">Transfer Issues</span>
</PrimaryButton>
</div>
@@ -36,7 +36,7 @@ const EmojiIconPicker: React.FC<Props> = ({
const [isOpen, setIsOpen] = useState(false);
const [openColorPicker, setOpenColorPicker] = useState(false);
const [activeColor, setActiveColor] = useState<string>("#858e96");
const [activeColor, setActiveColor] = useState<string>("#020617");
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
@@ -69,8 +69,8 @@ const EmojiIconPicker: React.FC<Props> = ({
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-brand-base bg-brand-surface-2 shadow-lg">
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-brand-base bg-brand-surface-2 p-2 shadow-xl">
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] bg-brand-surface-2 shadow-lg">
<div className="h-[230px] w-[250px] overflow-auto border border-brand-base rounded-[4px] bg-brand-surface-2 p-2 shadow-xl">
<Tab.Group as="div" className="flex h-full w-full flex-col">
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
{tabOptions.map((tab) => (
@@ -82,7 +82,7 @@ const EmojiIconPicker: React.FC<Props> = ({
setOpenColorPicker(false);
}}
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
selected ? "" : "border-transparent text-brand-secondary"
selected ? "border-theme text-theme" : "border-transparent text-gray-500"
}`}
>
{tab.title}
@@ -95,12 +95,12 @@ const EmojiIconPicker: React.FC<Props> = ({
<Tab.Panel>
{recentEmojis.length > 0 && (
<div className="py-2">
<h3 className="mb-2 text-xs text-brand-secondary">Recent</h3>
<h3 className="mb-2 ml-1 text-xs text-gray-400">Recent</h3>
<div className="grid grid-cols-8 gap-2">
{recentEmojis.map((emoji) => (
<button
type="button"
className="flex h-4 w-4 select-none items-center justify-between text-sm"
className="h-4 w-4 select-none text-sm hover:bg-brand-surface-2 flex items-center justify-between"
key={emoji}
onClick={() => {
onChange(emoji);
@@ -113,13 +113,13 @@ const EmojiIconPicker: React.FC<Props> = ({
</div>
</div>
)}
<hr className="mb-2 h-[1px] w-full border-brand-base" />
<hr className="w-full h-[1px] mb-2" />
<div>
<div className="grid grid-cols-8 gap-x-2 gap-y-3">
{emojis.map((emoji) => (
<button
type="button"
className="mb-1 flex h-4 w-4 select-none items-center text-sm"
className="h-4 w-4 mb-1 select-none text-sm hover:bg-brand-surface-2 flex items-center"
key={emoji}
onClick={() => {
onChange(emoji);
@@ -136,7 +136,7 @@ const EmojiIconPicker: React.FC<Props> = ({
<div className="py-2">
<Tab.Panel className="flex h-full w-full flex-col justify-center">
<div className="relative">
<div className="flex items-center justify-between px-1 pb-2">
<div className="pb-2 px-1 flex items-center justify-between">
{[
"#FF6B00",
"#8CC1FF",
@@ -147,7 +147,7 @@ const EmojiIconPicker: React.FC<Props> = ({
"#000000",
].map((curCol) => (
<span
className="h-4 w-4 cursor-pointer rounded-full"
className="w-4 h-4 rounded-full cursor-pointer"
style={{ backgroundColor: curCol }}
onClick={() => setActiveColor(curCol)}
/>
@@ -158,14 +158,14 @@ const EmojiIconPicker: React.FC<Props> = ({
className="flex items-center gap-1"
>
<span
className="conical-gradient h-4 w-4 rounded-full"
className="w-4 h-4 rounded-full conical-gradient"
style={{ backgroundColor: activeColor }}
/>
</button>
</div>
<div>
<TwitterPicker
className={`!absolute top-4 left-4 z-10 m-2 ${
className={`m-2 !absolute top-4 left-4 z-10 ${
openColorPicker ? "block" : "hidden"
}`}
color={activeColor}
@@ -178,12 +178,13 @@ const EmojiIconPicker: React.FC<Props> = ({
/>
</div>
</div>
<hr className="mb-1 h-[1px] w-full border-brand-base" />
<div className="mt-1 ml-1 grid grid-cols-8 gap-x-2 gap-y-3">
<hr className="w-full h-[1px] mb-1" />
<div className="grid grid-cols-8 mt-1 ml-1 gap-x-2 gap-y-3">
{icons.material_rounded.map((icon) => (
<button
type="button"
className="mb-1 flex h-4 w-4 select-none items-center text-lg"
className="h-4 w-4 mb-1 select-none text-lg hover:bg-brand-surface-2 flex items-center"
key={icon.name}
onClick={() => {
if (onIconsClick) onIconsClick(icon.name);

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