fix:merge conflicts resolved

This commit is contained in:
sriram veeraghanta
2024-01-30 15:25:38 +05:30
116 changed files with 1504 additions and 777 deletions

View File

@@ -562,7 +562,7 @@ class IssueSerializer(DynamicBaseSerializer):
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.SerializerMethodField()
# Many to many
label_ids = serializers.PrimaryKeyRelatedField(
@@ -597,7 +597,7 @@ class IssueSerializer(DynamicBaseSerializer):
"project_id",
"parent_id",
"cycle_id",
"module_id",
"module_ids",
"label_ids",
"assignee_ids",
"sub_issues_count",
@@ -613,6 +613,10 @@ class IssueSerializer(DynamicBaseSerializer):
]
read_only_fields = fields
def get_module_ids(self, obj):
# Access the prefetched modules and extract module IDs
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
class IssueLiteSerializer(DynamicBaseSerializer):
workspace_detail = WorkspaceLiteSerializer(

View File

@@ -35,17 +35,26 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
}
),
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
"get": "list",
"post": "create",
}
),
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
ModuleIssueViewSet.as_view(
{
"get": "retrieve",

View File

@@ -600,16 +600,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.order_by(order_by)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()

View File

@@ -100,7 +100,7 @@ def dashboard_assigned_issues(self, request, slug):
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_relation",
@@ -110,7 +110,6 @@ def dashboard_assigned_issues(self, request, slug):
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -221,9 +220,8 @@ def dashboard_created_issues(self, request, slug):
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()

View File

@@ -95,7 +95,7 @@ class InboxIssueViewSet(BaseViewSet):
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("labels", "assignees")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_inbox",
@@ -105,7 +105,6 @@ class InboxIssueViewSet(BaseViewSet):
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()

View File

@@ -112,12 +112,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
project_id=self.kwargs.get("project_id")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_reactions",
@@ -125,7 +121,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -1087,12 +1082,31 @@ class IssueArchiveViewSet(BaseViewSet):
.filter(archived_at__isnull=False)
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
@method_decorator(gzip_page)
@@ -1120,22 +1134,6 @@ class IssueArchiveViewSet(BaseViewSet):
issue_queryset = (
self.get_queryset()
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
@@ -1681,18 +1679,37 @@ class IssueDraftViewSet(BaseViewSet):
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(is_draft=True)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
@method_decorator(gzip_page)
@@ -1719,22 +1736,6 @@ class IssueDraftViewSet(BaseViewSet):
issue_queryset = (
self.get_queryset()
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering

View File

@@ -7,6 +7,8 @@ from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
from django.core import serializers
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework.response import Response
@@ -296,23 +298,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(pk),
"module_name": str(module.name),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
_ = [
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps({"module_id": str(pk)}),
actor_id=str(request.user.id),
issue_id=str(issue),
project_id=project_id,
current_instance=json.dumps({"module_name": str(module.name)}),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for issue in module_issues
]
module.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -332,62 +331,18 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
ProjectEntityPermission,
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(module_id=self.kwargs.get("module_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.select_related("module")
.select_related("issue", "issue__state", "issue__project")
.prefetch_related("issue__assignees", "issue__labels")
.prefetch_related("module__members")
.distinct()
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id, module_id):
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
order_by = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
def get_queryset(self):
return (
Issue.objects.filter(
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
issue_module__module_id=self.kwargs.get("module_id")
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("labels", "assignees")
.prefetch_related('issue_module__module')
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -403,105 +358,118 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id, module_id):
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
filters = issue_filters(request.query_params, "GET")
issue_queryset = self.get_queryset().filter(**filters)
serializer = IssueSerializer(
issues, many=True, fields=fields if fields else None
issue_queryset, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, module_id):
# create multiple issues inside a module
def create_module_issues(self, request, slug, project_id, module_id):
issues = request.data.get("issues", [])
if not len(issues):
return Response(
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id
)
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
update_module_issue_activity = []
records_to_update = []
record_to_create = []
for issue in issues:
module_issue = [
module_issue
for module_issue in module_issues
if str(module_issue.issue_id) in issues
]
if len(module_issue):
if module_issue[0].module_id != module_id:
update_module_issue_activity.append(
{
"old_module_id": str(module_issue[0].module_id),
"new_module_id": str(module_id),
"issue_id": str(module_issue[0].issue_id),
}
)
module_issue[0].module_id = module_id
records_to_update.append(module_issue[0])
else:
record_to_create.append(
ModuleIssue(
module=module,
issue_id=issue,
project_id=project_id,
workspace=module.workspace,
created_by=request.user,
updated_by=request.user,
)
project = Project.objects.get(pk=project_id)
_ = ModuleIssue.objects.bulk_create(
[
ModuleIssue(
issue_id=str(issue),
module_id=module_id,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
ModuleIssue.objects.bulk_create(
record_to_create,
for issue in issues
],
batch_size=10,
ignore_conflicts=True,
)
# Bulk Update the activity
_ = [
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"module_id": str(module_id)}),
actor_id=str(request.user.id),
issue_id=str(issue),
project_id=project_id,
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for issue in issues
]
issues = (self.get_queryset().filter(pk__in=issues))
serializer = IssueSerializer(issues , many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
ModuleIssue.objects.bulk_update(
records_to_update,
["module"],
# create multiple module inside an issue
def create_issue_modules(self, request, slug, project_id, issue_id):
modules = request.data.get("modules", [])
if not len(modules):
return Response(
{"error": "Modules are required"},
status=status.HTTP_400_BAD_REQUEST,
)
project = Project.objects.get(pk=project_id)
_ = ModuleIssue.objects.bulk_create(
[
ModuleIssue(
issue_id=issue_id,
module_id=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module in modules
],
batch_size=10,
ignore_conflicts=True,
)
# Bulk Update the activity
_ = [
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"module_id": module}),
actor_id=str(request.user.id),
issue_id=issue_id,
project_id=project_id,
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for module in modules
]
# Capture Issue Activity
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"modules_list": issues}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue = (self.get_queryset().filter(pk=issue_id).first())
serializer = IssueSerializer(issue)
return Response(serializer.data, status=status.HTTP_201_CREATED)
issues = self.get_queryset().values_list("issue_id", flat=True)
return Response(
IssueSerializer(
Issue.objects.filter(pk__in=issues), many=True
).data,
status=status.HTTP_200_OK,
)
def destroy(self, request, slug, project_id, module_id, issue_id):
module_issue = ModuleIssue.objects.get(
@@ -512,16 +480,11 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(issue_id)],
}
),
requested_data=json.dumps({"module_id": str(module_id)}),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
current_instance=json.dumps({"module_name": module_issue.module.name}),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),

View File

@@ -228,7 +228,7 @@ class IssueSearchEndpoint(BaseAPIView):
parent = request.query_params.get("parent", "false")
issue_relation = request.query_params.get("issue_relation", "false")
cycle = request.query_params.get("cycle", "false")
module = request.query_params.get("module", "false")
module = request.query_params.get("module", False)
sub_issue = request.query_params.get("sub_issue", "false")
issue_id = request.query_params.get("issue_id", False)
@@ -269,8 +269,8 @@ class IssueSearchEndpoint(BaseAPIView):
if cycle == "true":
issues = issues.exclude(issue_cycle__isnull=False)
if module == "true":
issues = issues.exclude(issue_module__isnull=False)
if module:
issues = issues.exclude(issue_module__module=module)
return Response(
issues.values(

View File

@@ -87,12 +87,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_reactions",
@@ -127,7 +123,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.filter(**filters)
.filter(project__project_projectmember__member=self.request.user)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -150,13 +145,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
)
# Priority Ordering

View File

@@ -1346,9 +1346,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()

View File

@@ -30,6 +30,7 @@ from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
from plane.settings.redis import redis_instance
# Track Changes in name
def track_name(
requested_data,
@@ -852,70 +853,26 @@ def create_module_issue_activity(
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 = Issue.objects.filter(pk=updated_record.get("issue_id")).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor_id=actor_id,
verb="updated",
old_value=old_module.name,
new_value=new_module.name,
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"updated module to ",
old_identifier=old_module.id,
new_identifier=new_module.id,
epoch=epoch,
)
)
for created_record in created_records:
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).first()
issue = Issue.objects.filter(
pk=created_record.get("fields").get("issue")
).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor_id=actor_id,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"added module {module.name}",
new_identifier=module.id,
epoch=epoch,
)
module = Module.objects.filter(pk=requested_data.get("module_id")).first()
issue = Issue.objects.filter(pk=issue_id).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"added module {module.name}",
new_identifier=requested_data.get("module_id"),
epoch=epoch,
)
)
def delete_module_issue_activity(
@@ -934,32 +891,26 @@ def delete_module_issue_activity(
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
module_id = requested_data.get("module_id", "")
module_name = requested_data.get("module_name", "")
module = Module.objects.filter(pk=module_id).first()
issues = requested_data.get("issues")
for issue in issues:
current_issue = Issue.objects.filter(pk=issue).first()
if issue:
current_issue.updated_at = timezone.now()
current_issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=issue,
actor_id=actor_id,
verb="deleted",
old_value=module.name if module is not None else module_name,
new_value="",
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"removed this issue from {module.name if module is not None else module_name}",
old_identifier=module_id if module_id is not None else None,
epoch=epoch,
)
module_name = current_instance.get("module_name")
current_issue = Issue.objects.filter(pk=issue_id).first()
if current_issue:
current_issue.updated_at = timezone.now()
current_issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="deleted",
old_value=module_name,
new_value="",
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"removed this issue from {module_name}",
old_identifier=requested_data.get("module_id") if requested_data.get("module_id") is not None else None,
epoch=epoch,
)
)
def create_link_activity(
@@ -1648,7 +1599,6 @@ def issue_activity(
)
except Exception as e:
capture_exception(e)
if notification:
notifications.delay(

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-24 18:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('db', '0057_auto_20240122_0901'),
]
operations = [
migrations.AlterField(
model_name='moduleissue',
name='issue',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue'),
),
migrations.AlterUniqueTogether(
name='moduleissue',
unique_together={('issue', 'module')},
),
]

View File

@@ -134,11 +134,12 @@ class ModuleIssue(ProjectBaseModel):
module = models.ForeignKey(
"db.Module", on_delete=models.CASCADE, related_name="issue_module"
)
issue = models.OneToOneField(
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_module"
)
class Meta:
unique_together = ["issue", "module"]
verbose_name = "Module Issue"
verbose_name_plural = "Module Issues"
db_table = "module_issues"

View File

@@ -21,7 +21,7 @@ export type TIssue = {
project_id: string;
parent_id: string | null;
cycle_id: string | null;
module_id: string | null;
module_ids: string[] | null;
created_at: string;
updated_at: string;

View File

@@ -128,7 +128,7 @@ export type TProjectIssuesSearchParams = {
parent?: boolean;
issue_relation?: boolean;
cycle?: boolean;
module?: boolean;
module?: string[];
sub_issue?: boolean;
issue_id?: string;
workspace_search: boolean;

View File

@@ -64,8 +64,7 @@ export type TIssueParams =
| "order_by"
| "type"
| "sub_issue"
| "show_empty_groups"
| "start_target_date";
| "show_empty_groups";
export type TCalendarLayouts = "month" | "week";
@@ -93,7 +92,6 @@ export interface IIssueDisplayFilterOptions {
layout?: TIssueLayouts;
order_by?: TIssueOrderByOptions;
show_empty_groups?: boolean;
start_target_date?: boolean;
sub_issue?: boolean;
type?: TIssueTypeFilters;
}

View File

@@ -216,7 +216,7 @@ export const CommandPalette: FC = observer(() => {
<CreateUpdateIssueModal
isOpen={isCreateIssueModalOpen}
onClose={() => toggleCreateIssueModal(false)}
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_id: moduleId.toString() } : undefined}
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
storeType={createIssueStoreType}
/>

View File

@@ -40,9 +40,9 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
}`}`}
target={activity.issue === null ? "_self" : "_blank"}
rel={activity.issue === null ? "" : "noopener noreferrer"}
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline whitespace-nowrap"
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
>
{`${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`}{" "}
<span className="whitespace-nowrap">{`${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`}</span>{" "}
<span className="font-normal">{activity.issue_detail?.name}</span>
</a>
) : (
@@ -267,7 +267,7 @@ const activityDetails: {
<span className="flex-shrink truncate font-medium text-custom-text-100">{activity.new_value}</span>
</span>
{showIssue && (
<span>
<span className="">
{" "}
to <IssueLink activity={activity} />
</span>

View File

@@ -0,0 +1,16 @@
import { FC } from "react";
import { Menu } from "lucide-react";
import { useApplication } from "hooks/store";
import { observer } from "mobx-react";
export const SidebarHamburgerToggle: FC = observer (() => {
const { theme: themStore } = useApplication();
return (
<div
className="w-7 h-7 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
onClick={() => themStore.toggleSidebar()}
>
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
</div>
);
});

View File

@@ -164,7 +164,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
</Tooltip>
</span>
<span className="flex items-center gap-1 capitalize">
<span className="flex items-center gap-1">
<span className="flex gap-1 whitespace-nowrap rounded-sm text-sm px-3 py-0.5 bg-amber-500/10 text-amber-500">
{`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
</span>
@@ -254,7 +254,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex h-full w-full flex-col p-4 text-custom-text-200">
<div className="flex w-full items-center gap-2 py-1">
<span>Progress</span>
<LinearProgressIndicator data={progressIndicatorData} inPercentage />
<LinearProgressIndicator size="md" data={progressIndicatorData} inPercentage />
</div>
<div className="mt-2 flex flex-col items-center gap-1">
{Object.keys(groupedIssues).map((group, index) => (

View File

@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// components
import { DateDropdown, ProjectDropdown } from "components/dropdowns";
@@ -11,19 +12,28 @@ import { ICycle } from "@plane/types";
type Props = {
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
handleClose: () => void;
status: boolean;
projectId: string;
setActiveProject: (projectId: string) => void;
data?: ICycle | null;
};
const defaultValues: Partial<ICycle> = {
name: "",
description: "",
start_date: null,
end_date: null,
};
export const CycleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, projectId, setActiveProject, data } = props;
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
// form data
const {
formState: { errors, isSubmitting },
handleSubmit,
control,
watch,
reset,
} = useForm<ICycle>({
defaultValues: {
project: projectId,
@@ -34,6 +44,13 @@ export const CycleForm: React.FC<Props> = (props) => {
},
});
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, reset]);
const startDate = watch("start_date");
const endDate = watch("end_date");

View File

@@ -1,9 +1,9 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// services
import { CycleService } from "services/cycle.service";
// hooks
import { useApplication, useCycle } from "hooks/store";
import { useApplication, useCycle, useProject } from "hooks/store";
import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// components
@@ -25,11 +25,12 @@ const cycleService = new CycleService();
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
// states
const [activeProject, setActiveProject] = useState<string>(projectId);
const [activeProject, setActiveProject] = useState<string | null>(null);
// store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { workspaceProjectIds } = useProject();
const { createCycle, updateCycleDetails } = useCycle();
// toast alert
const { setToastAlert } = useToast();
@@ -134,6 +135,27 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
});
};
useEffect(() => {
// if modal is closed, reset active project to null
// and return to avoid activeProject being set to some other project
if (!isOpen) {
setActiveProject(null);
return;
}
// if data is present, set active project to the project of the
// issue. This has more priority than the project in the url.
if (data && data.project) {
setActiveProject(data.project);
return;
}
// if data is not present, set active project to the project
// in the url. This has the least priority.
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProject)
setActiveProject(projectId ?? workspaceProjectIds?.[0] ?? null);
}, [activeProject, data, projectId, workspaceProjectIds, isOpen]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@@ -164,7 +186,8 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
<CycleForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}
projectId={activeProject}
status={data ? true : false}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
data={data}
/>

View File

@@ -63,34 +63,27 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full grid grid-cols-4 p-0.5 hover:shadow-custom-shadow-4xl duration-300">
{STATS_LIST.map((stat, index) => {
const isFirst = index === 0;
const isLast = index === STATS_LIST.length - 1;
const isMiddle = !isFirst && !isLast;
return (
<div key={stat.key} className="flex relative">
{!isLast && (
<div className="absolute right-0 top-1/2 -translate-y-1/2 h-3/5 w-[0.5px] bg-custom-border-200" />
)}
<Link
href={stat.link}
className={cn(
`py-4 hover:bg-custom-background-80 duration-300 rounded-[10px] w-full break-words flex flex-col justify-center`,
{
"pl-11 pr-[4.725rem] mr-0.5": isFirst,
"px-[4.725rem] mx-0.5": isMiddle,
"px-[4.725rem] ml-0.5": isLast,
}
)}
>
<h5 className="font-semibold text-xl">{stat.count}</h5>
<p className="text-custom-text-300">{stat.title}</p>
</Link>
</div>
);
})}
<div
className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full grid lg:grid-cols-4 md:grid-cols-2 sm:grid-cols-2 grid-cols-2 p-0.5 hover:shadow-custom-shadow-4xl duration-300
[&>div>a>div]:border-r
[&>div:last-child>a>div]:border-0
[&>div>a>div]:border-custom-border-200
[&>div:nth-child(2)>a>div]:border-0
[&>div:nth-child(2)>a>div]:lg:border-r
"
>
{STATS_LIST.map((stat) => (
<div className="w-full flex flex-col gap-2 hover:bg-custom-background-80 rounded-[10px]">
<Link href={stat.link} className="py-4 duration-300 rounded-[10px] w-full ">
<div className={`relative flex justify-center items-center`}>
<div>
<h5 className="font-semibold text-xl">{stat.count}</h5>
<p className="text-custom-text-300 text-sm xl:text-base">{stat.title}</p>
</div>
</div>
</Link>
</div>
))}
</div>
);
});

View File

@@ -1,23 +0,0 @@
import React, { useState, useEffect } from "react";
// react beautiful dnd
import { Droppable, DroppableProps } from "@hello-pangea/dnd";
const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
if (!enabled) return null;
return <Droppable {...props}>{children}</Droppable>;
};
export default StrictModeDroppable;

View File

@@ -3,6 +3,7 @@ export * from "./cycle";
export * from "./date";
export * from "./estimate";
export * from "./module";
export * from "./module-select";
export * from "./priority";
export * from "./project";
export * from "./state";

View File

@@ -0,0 +1,114 @@
import { FC } from "react";
import { twMerge } from "tailwind-merge";
import { observer } from "mobx-react-lite";
import { ChevronDown, X } from "lucide-react";
// hooks
import { useModule } from "hooks/store";
// ui and components
import { DiceIcon, Tooltip } from "@plane/ui";
// types
import { TModuleSelectButton } from "./types";
export const ModuleSelectButton: FC<TModuleSelectButton> = observer((props) => {
const {
value,
onChange,
placeholder,
buttonClassName,
buttonVariant,
hideIcon,
hideText,
dropdownArrow,
dropdownArrowClassName,
showTooltip,
showCount,
} = props;
// hooks
const { getModuleById } = useModule();
return (
<div
className={twMerge(
`w-full h-full relative overflow-hidden flex justify-between items-center gap-1 rounded text-sm px-2`,
buttonVariant === "border-with-text"
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
: ``,
buttonVariant === "border-without-text"
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
: ``,
buttonVariant === "background-with-text" ? `bg-custom-background-80` : ``,
buttonVariant === "background-without-text" ? `bg-custom-background-80` : ``,
buttonVariant === "transparent-with-text" ? `hover:bg-custom-background-80` : ``,
buttonVariant === "transparent-without-text" ? `hover:bg-custom-background-80` : ``,
buttonClassName
)}
>
<div className="relative overflow-hidden h-full flex flex-wrap items-center gap-1">
{value && typeof value === "string" ? (
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
{getModuleById(value)?.name || placeholder}
</span>
)}
</div>
) : value && Array.isArray(value) && value.length > 0 ? (
showCount ? (
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
{value.length} Modules
</span>
)}
</div>
) : (
value.map((moduleId) => {
const _module = getModuleById(moduleId);
if (!_module) return <></>;
return (
<div className="relative flex justify-between items-center gap-1 min-w-[60px] max-w-[84px] overflow-hidden bg-custom-background-80 px-1.5 py-1 rounded">
<Tooltip tooltipContent={_module?.name} disabled={!showTooltip}>
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full truncate inline-block line-clamp-1 capitalize">{_module?.name}</span>
)}
</div>
</Tooltip>
<Tooltip tooltipContent="Remove" disabled={!showTooltip}>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onChange(_module.id);
}}
>
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
</span>
</Tooltip>
</div>
);
})
)
) : (
!hideText && (
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
{placeholder}
</span>
)}
</div>
)
)}
</div>
{dropdownArrow && (
<ChevronDown className={twMerge("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
);
});

View File

@@ -0,0 +1,2 @@
export * from "./button";
export * from "./select";

View File

@@ -0,0 +1,227 @@
import { FC, useEffect, useRef, useState, Fragment } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { twMerge } from "tailwind-merge";
// hooks
import { useModule } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { ModuleSelectButton } from "./";
// types
import { TModuleSelectDropdown, TModuleSelectDropdownOption } from "./types";
import { DiceIcon } from "@plane/ui";
export const ModuleSelectDropdown: FC<TModuleSelectDropdown> = observer((props) => {
// props
const {
workspaceSlug,
projectId,
value = undefined,
onChange,
placeholder = "Module",
multiple = false,
disabled = false,
className = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonVariant = "transparent-with-text",
hideIcon = false,
dropdownArrow = false,
dropdownArrowClassName = "",
showTooltip = false,
showCount = false,
placement,
tabIndex,
button,
} = props;
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// store hooks
const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
const moduleIds = getProjectModuleIds(projectId);
const options: TModuleSelectDropdownOption[] | undefined = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return {
value: moduleId,
query: `${moduleDetails?.name}`,
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{moduleDetails?.name}</span>
</div>
),
};
});
!multiple &&
options?.unshift({
value: undefined,
query: "No module",
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No module</span>
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch modules of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!moduleIds) fetchModules(workspaceSlug, projectId);
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
const openDropdown = () => {
if (isOpen) closeDropdown();
else {
setIsOpen(true);
if (referenceElement) referenceElement.focus();
}
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const comboboxProps: any = {};
if (multiple) comboboxProps.multiple = true;
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={twMerge("h-full", className)}
value={value}
onChange={onChange}
disabled={disabled}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={twMerge(
"block h-full max-w-full outline-none",
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
buttonContainerClassName
)}
onClick={openDropdown}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={twMerge(
"block h-full max-w-full outline-none ",
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
buttonContainerClassName
)}
onClick={openDropdown}
>
<ModuleSelectButton
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
buttonClassName={buttonClassName}
buttonVariant={buttonVariant}
hideIcon={hideIcon}
hideText={["border-without-text", "background-without-text", "transparent-without-text"].includes(
buttonVariant
)}
dropdownArrow={dropdownArrow}
dropdownArrowClassName={dropdownArrowClassName}
showTooltip={showTooltip}
showCount={showCount}
/>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(moduleIds: any) => {
const displayValueOptions: TModuleSelectDropdownOption[] | undefined = options?.filter((_module) =>
moduleIds.includes(_module.value)
);
return displayValueOptions?.map((_option) => _option.query).join(", ") || "Select Module";
}}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={() => !multiple && closeDropdown()}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
)}
</Combobox>
);
});

View File

@@ -0,0 +1,50 @@
import { ReactNode } from "react";
import { Placement } from "@popperjs/core";
import { TDropdownProps, TButtonVariants } from "../types";
type TModuleSelectDropdownRoot = Omit<
TDropdownProps,
"buttonClassName",
"buttonContainerClassName",
"buttonContainerClassName",
"className",
"disabled",
"hideIcon",
"placeholder",
"placement",
"tabIndex",
"tooltip"
>;
export type TModuleSelectDropdownBase = {
value: string | string[] | undefined;
onChange: (moduleIds: undefined | string | (string | undefined)[]) => void;
placeholder?: string;
disabled?: boolean;
buttonClassName?: string;
buttonVariant?: TButtonVariants;
hideIcon?: boolean;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
showTooltip?: boolean;
showCount?: boolean;
};
export type TModuleSelectButton = TModuleSelectDropdownBase & { hideText?: boolean };
export type TModuleSelectDropdown = TModuleSelectDropdownBase & {
workspaceSlug: string;
projectId: string;
multiple?: boolean;
className?: string;
buttonContainerClassName?: string;
placement?: Placement;
tabIndex?: number;
button?: ReactNode;
};
export type TModuleSelectDropdownOption = {
value: string | undefined;
query: string;
content: JSX.Element;
};

View File

@@ -2,7 +2,7 @@ import { FC } from "react";
// hooks
import { useChart } from "../hooks";
// helpers
import { ChartDraggable } from "../helpers/draggable";
import { ChartAddBlock, ChartDraggable } from "components/gantt-chart";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
@@ -15,6 +15,7 @@ export type GanttChartBlocksProps = {
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
showAllBlocks: boolean;
};
export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
@@ -26,6 +27,7 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
showAllBlocks,
} = props;
const { activeBlock, dispatch } = useChart();
@@ -45,6 +47,8 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
totalBlockShifts: number,
dragDirection: "left" | "right" | "move"
) => {
if (!block.start_date || !block.target_date) return;
const originalStartDate = new Date(block.start_date);
const updatedStartDate = new Date(originalStartDate);
@@ -75,27 +79,31 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
>
{blocks &&
blocks.length > 0 &&
blocks.map(
(block) =>
block.start_date &&
block.target_date && (
<div
key={`block-${block.id}`}
className={`h-11 ${activeBlock?.id === block.id ? "bg-custom-background-80" : ""}`}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
>
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
</div>
)
)}
blocks.map((block) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
const isBlockVisibleOnChart = block.start_date && block.target_date;
return (
<div
key={`block-${block.id}`}
className={`h-11 ${activeBlock?.id === block.id ? "bg-custom-background-80" : ""}`}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
>
{!isBlockVisibleOnChart && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />}
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
</div>
);
})}
</div>
);
};

View File

@@ -46,22 +46,25 @@ type ChartViewRootProps = {
enableBlockMove: boolean;
enableReorder: boolean;
bottomSpacing: boolean;
showAllBlocks: boolean;
};
export const ChartViewRoot: FC<ChartViewRootProps> = ({
border,
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
bottomSpacing,
}) => {
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const {
border,
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
bottomSpacing,
showAllBlocks,
} = props;
// states
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
@@ -311,6 +314,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
showAllBlocks={showAllBlocks}
/>
)}
</div>

View File

@@ -1,5 +1,4 @@
import { FC } from "react";
// hooks
import { useChart } from "../hooks";
// types

View File

@@ -0,0 +1,91 @@
import { useEffect, useRef, useState } from "react";
import { addDays } from "date-fns";
import { Plus } from "lucide-react";
// hooks
import { useChart } from "../hooks";
// ui
import { Tooltip } from "@plane/ui";
// helpers
import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
type Props = {
block: IGanttBlock;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
};
export const ChartAddBlock: React.FC<Props> = (props) => {
const { block, blockUpdateHandler } = props;
// states
const [isButtonVisible, setIsButtonVisible] = useState(false);
const [buttonXPosition, setButtonXPosition] = useState(0);
const [buttonStartDate, setButtonStartDate] = useState<Date | null>(null);
// refs
const containerRef = useRef<HTMLDivElement>(null);
// chart hook
const { currentViewData } = useChart();
const handleButtonClick = () => {
if (!currentViewData) return;
const { startDate: chartStartDate, width } = currentViewData.data;
const columnNumber = buttonXPosition / width;
const startDate = addDays(chartStartDate, columnNumber);
const endDate = addDays(startDate, 1);
blockUpdateHandler(block.data, {
start_date: renderFormattedPayloadDate(startDate) ?? undefined,
target_date: renderFormattedPayloadDate(endDate) ?? undefined,
});
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleMouseMove = (e: MouseEvent) => {
if (!currentViewData) return;
setButtonXPosition(e.offsetX);
const { startDate: chartStartDate, width } = currentViewData.data;
const columnNumber = buttonXPosition / width;
const startDate = addDays(chartStartDate, columnNumber);
setButtonStartDate(startDate);
};
container.addEventListener("mousemove", handleMouseMove);
return () => {
container?.removeEventListener("mousemove", handleMouseMove);
};
}, [buttonXPosition, currentViewData]);
return (
<div
className="relative h-full w-full"
onMouseEnter={() => setIsButtonVisible(true)}
onMouseLeave={() => setIsButtonVisible(false)}
>
<div ref={containerRef} className="h-full w-full" />
{isButtonVisible && (
<Tooltip tooltipContent={buttonStartDate && renderFormattedDate(buttonStartDate)}>
<button
type="button"
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-8 bg-custom-background-80 p-1.5 rounded border border-custom-border-300 grid place-items-center text-custom-text-200 hover:text-custom-text-100"
style={{
marginLeft: `${buttonXPosition}px`,
}}
onClick={handleButtonClick}
>
<Plus className="h-3.5 w-3.5" />
</button>
</Tooltip>
)}
</div>
);
};

View File

@@ -3,14 +3,11 @@ import { TIssue } from "@plane/types";
import { IGanttBlock } from "components/gantt-chart";
export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
blocks && blocks.length > 0
? blocks
.filter((b) => new Date(b?.start_date ?? "") <= new Date(b?.target_date ?? ""))
.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.target_date ?? ""),
}))
: [];
blocks &&
blocks.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
}));

View File

@@ -1,6 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
// icons
import { ArrowLeft, ArrowRight } from "lucide-react";
// hooks
import { useChart } from "../hooks";
@@ -16,23 +14,17 @@ type Props = {
enableBlockMove: boolean;
};
export const ChartDraggable: React.FC<Props> = ({
block,
blockToRender,
handleBlock,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
}) => {
export const ChartDraggable: React.FC<Props> = (props) => {
const { block, blockToRender, handleBlock, enableBlockLeftResize, enableBlockRightResize, enableBlockMove } = props;
// states
const [isLeftResizing, setIsLeftResizing] = useState(false);
const [isRightResizing, setIsRightResizing] = useState(false);
const [isMoving, setIsMoving] = useState(false);
const [posFromLeft, setPosFromLeft] = useState<number | null>(null);
// refs
const resizableRef = useRef<HTMLDivElement>(null);
// chart hook
const { currentViewData, scrollLeft } = useChart();
// check if cursor reaches either end while resizing/dragging
const checkScrollEnd = (e: MouseEvent): number => {
const SCROLL_THRESHOLD = 70;
@@ -68,7 +60,6 @@ export const ChartDraggable: React.FC<Props> = ({
return delWidth;
};
// handle block resize from the left end
const handleBlockLeftResize = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!currentViewData || !resizableRef.current || !block.position) return;
@@ -120,7 +111,6 @@ export const ChartDraggable: React.FC<Props> = ({
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
// handle block resize from the right end
const handleBlockRightResize = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!currentViewData || !resizableRef.current || !block.position) return;
@@ -163,7 +153,6 @@ export const ChartDraggable: React.FC<Props> = ({
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
// handle block x-axis move
const handleBlockMove = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!enableBlockMove || !currentViewData || !resizableRef.current || !block.position) return;
@@ -210,7 +199,6 @@ export const ChartDraggable: React.FC<Props> = ({
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
// scroll to a hidden block
const handleScrollToBlock = () => {
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
@@ -220,7 +208,6 @@ export const ChartDraggable: React.FC<Props> = ({
// update container's scroll position to the block's position
scrollContainer.scrollLeft = block.position.marginLeft - 4;
};
// update block position from viewport's left end on scroll
useEffect(() => {
const block = resizableRef.current;
@@ -229,7 +216,6 @@ export const ChartDraggable: React.FC<Props> = ({
setPosFromLeft(block.getBoundingClientRect().left);
}, [scrollLeft]);
// check if block is hidden on either side
const isBlockHiddenOnLeft =
block.position?.marginLeft &&

View File

@@ -1 +1,3 @@
export * from "./add-block";
export * from "./block-structure";
export * from "./draggable";

View File

@@ -19,36 +19,43 @@ type GanttChartRootProps = {
enableBlockMove?: boolean;
enableReorder?: boolean;
bottomSpacing?: boolean;
showAllBlocks?: boolean;
};
export const GanttChartRoot: FC<GanttChartRootProps> = ({
border = true,
title,
blocks,
loaderTitle = "blocks",
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize = true,
enableBlockRightResize = true,
enableBlockMove = true,
enableReorder = true,
bottomSpacing = false,
}) => (
<ChartContextProvider>
<ChartViewRoot
border={border}
title={title}
blocks={blocks}
loaderTitle={loaderTitle}
blockUpdateHandler={blockUpdateHandler}
sidebarToRender={sidebarToRender}
blockToRender={blockToRender}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableReorder={enableReorder}
bottomSpacing={bottomSpacing}
/>
</ChartContextProvider>
);
export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
const {
border = true,
title,
blocks,
loaderTitle = "blocks",
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize = true,
enableBlockRightResize = true,
enableBlockMove = true,
enableReorder = true,
bottomSpacing = false,
showAllBlocks = false,
} = props;
return (
<ChartContextProvider>
<ChartViewRoot
border={border}
title={title}
blocks={blocks}
loaderTitle={loaderTitle}
blockUpdateHandler={blockUpdateHandler}
sidebarToRender={sidebarToRender}
blockToRender={blockToRender}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableReorder={enableReorder}
bottomSpacing={bottomSpacing}
showAllBlocks={showAllBlocks}
/>
</ChartContextProvider>
);
};

View File

@@ -1,6 +1,5 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
@@ -83,7 +82,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
return (
<DragDropContext onDragEnd={handleOrderChange}>
<StrictModeDroppable droppableId="gantt-sidebar">
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
@@ -153,7 +152,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
</>
</div>
)}
</StrictModeDroppable>
</Droppable>
</DragDropContext>
);
};

View File

@@ -1,6 +1,5 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
@@ -83,7 +82,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
return (
<DragDropContext onDragEnd={handleOrderChange}>
<StrictModeDroppable droppableId="gantt-sidebar">
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
@@ -153,7 +152,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
</>
</div>
)}
</StrictModeDroppable>
</Droppable>
</DragDropContext>
);
};

View File

@@ -1,6 +1,5 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
@@ -84,7 +83,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
return (
<DragDropContext onDragEnd={handleOrderChange}>
<StrictModeDroppable droppableId="gantt-sidebar">
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
@@ -154,7 +153,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
</>
</div>
)}
</StrictModeDroppable>
</Droppable>
</DragDropContext>
);
};

View File

@@ -1,6 +1,5 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
@@ -27,10 +26,10 @@ type Props = {
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
showAllBlocks?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
blockUpdateHandler,
blocks,
@@ -39,6 +38,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
quickAddCallback,
viewId,
disableIssueCreation,
showAllBlocks = false,
} = props;
const router = useRouter();
@@ -100,7 +100,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
return (
<DragDropContext onDragEnd={handleOrderChange}>
<StrictModeDroppable droppableId="gantt-sidebar">
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
@@ -111,7 +111,15 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
<>
{blocks ? (
blocks.map((block, index) => {
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "");
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
const duration =
!block.start_date || !block.target_date
? null
: findTotalDaysInRange(block.start_date, block.target_date);
return (
<Draggable
@@ -149,7 +157,11 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
<IssueGanttSidebarBlock data={block.data} />
</div>
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
{duration && (
<span>
{duration} day{duration > 1 ? "s" : ""}
</span>
)}
</div>
</div>
</div>
@@ -173,7 +185,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
)}
</div>
)}
</StrictModeDroppable>
</Droppable>
</DragDropContext>
);
};

View File

@@ -13,8 +13,8 @@ export interface IGanttBlock {
width: number;
};
sort_order: number;
start_date: Date;
target_date: Date;
start_date: Date | null;
target_date: Date | null;
}
export interface IBlockUpdateData {

View File

@@ -167,6 +167,8 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType,
const { startDate } = chartData.data;
const { start_date: itemStartDate, target_date: itemTargetDate } = itemData;
if (!itemStartDate || !itemTargetDate) return null;
startDate.setHours(0, 0, 0, 0);
itemStartDate.setHours(0, 0, 0, 0);
itemTargetDate.setHours(0, 0, 0, 0);

View File

@@ -17,6 +17,7 @@ import useLocalStorage from "hooks/use-local-storage";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { ProjectAnalyticsModal } from "components/analytics";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui";
// icons
@@ -146,6 +147,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
/>
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<SidebarHamburgerToggle />
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"

View File

@@ -9,6 +9,8 @@ import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { EUserProjectRoles } from "constants/project";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const CyclesHeader: FC = observer(() => {
// router
@@ -30,6 +32,7 @@ export const CyclesHeader: FC = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -7,17 +7,13 @@ import { useLabel, useMember, useUser, useIssues } from "hooks/store";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues";
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui";
// icons
import { List, PlusIcon, Sheet } from "lucide-react";
// types
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
TStaticViewTypes,
} from "@plane/types";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserWorkspaceRoles } from "constants/workspace";
@@ -111,7 +107,8 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div>
<div className="relative flex gap-2">
<SidebarHamburgerToggle/>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"

View File

@@ -17,6 +17,7 @@ import useLocalStorage from "hooks/use-local-storage";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { ProjectAnalyticsModal } from "components/analytics";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
// icons
@@ -149,6 +150,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
/>
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<SidebarHamburgerToggle />
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"

View File

@@ -11,6 +11,8 @@ import { renderEmoji } from "helpers/emoji.helper";
// constants
import { MODULE_VIEW_LAYOUTS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const ModulesListHeader: React.FC = observer(() => {
// router
@@ -31,6 +33,7 @@ export const ModulesListHeader: React.FC = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -8,6 +8,8 @@ import { useApplication, usePage, useProject } from "hooks/store";
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export interface IPagesHeaderProps {
showButton?: boolean;
@@ -27,6 +29,7 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -9,6 +9,8 @@ import { Breadcrumbs, Button } from "@plane/ui";
import { renderEmoji } from "helpers/emoji.helper";
// constants
import { EUserProjectRoles } from "constants/project";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const PagesHeader = observer(() => {
// router
@@ -29,6 +31,7 @@ export const PagesHeader = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -14,6 +14,8 @@ import { ISSUE_DETAILS } from "constants/fetch-keys";
import { IssueArchiveService } from "services/issue";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
const issueArchiveService = new IssueArchiveService();
@@ -39,6 +41,7 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -10,6 +10,7 @@ import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } f
import { Breadcrumbs, LayersIcon } from "@plane/ui";
// components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// types
@@ -70,6 +71,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
return (
<div className="relative z-10 flex h-14 w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div className="block md:hidden">
<button
type="button"

View File

@@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, LayersIcon } from "@plane/ui";
// helper
@@ -74,6 +75,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -8,6 +8,7 @@ import { useProject } from "hooks/store";
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
// components
import { CreateInboxIssueModal } from "components/inbox";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// helper
import { renderEmoji } from "helpers/emoji.helper";
@@ -23,6 +24,7 @@ export const ProjectInboxHeader: FC = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -12,6 +12,8 @@ import { renderEmoji } from "helpers/emoji.helper";
import { IssueService } from "services/issue";
// constants
import { ISSUE_DETAILS } from "constants/fetch-keys";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// services
const issueService = new IssueService();
@@ -33,6 +35,7 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -8,6 +8,7 @@ import { useApplication, useLabel, useProject, useProjectState, useUser, useInbo
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { ProjectAnalyticsModal } from "components/analytics";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
// types
@@ -105,6 +106,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
/>
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div className="block md:hidden">
<button
type="button"

View File

@@ -9,6 +9,8 @@ import { renderEmoji } from "helpers/emoji.helper";
import { useProject, useUser } from "hooks/store";
// constants
import { EUserProjectRoles } from "constants/project";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export interface IProjectSettingHeader {
title: string;
@@ -30,6 +32,7 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -16,6 +16,7 @@ import {
} from "hooks/store";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
// helpers
@@ -107,6 +108,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<SidebarHamburgerToggle />
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"

View File

@@ -5,6 +5,7 @@ import { Plus } from "lucide-react";
import { useApplication, useProject, useUser } from "hooks/store";
// components
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// constants
@@ -30,6 +31,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
<>
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -6,6 +6,8 @@ import { useApplication, useProject, useUser } from "hooks/store";
import { Breadcrumbs, Button } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const ProjectsHeader = observer(() => {
// store hooks
@@ -23,6 +25,7 @@ export const ProjectsHeader = observer(() => {
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -1,9 +1,12 @@
// ui
import { Breadcrumbs } from "@plane/ui";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const UserProfileHeader = () => (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem type="text" label="Activity Overview" link="/profile" />

View File

@@ -1,10 +1,14 @@
import { observer } from "mobx-react-lite";
// ui
import { Breadcrumbs, ContrastIcon } from "@plane/ui";
// icons
import { Crown } from "lucide-react";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const WorkspaceActiveCycleHeader = observer(() => (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -2,6 +2,8 @@ import { useRouter } from "next/router";
import { ArrowLeft, BarChart2 } from "lucide-react";
// ui
import { Breadcrumbs } from "@plane/ui";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const WorkspaceAnalyticsHeader = () => {
const router = useRouter();
@@ -12,6 +14,7 @@ export const WorkspaceAnalyticsHeader = () => {
className={`relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div className="block md:hidden">
<button
type="button"

View File

@@ -8,10 +8,11 @@ import githubWhiteImage from "/public/logos/github-white.png";
// components
import { ProductUpdatesModal } from "components/common";
import { Breadcrumbs } from "@plane/ui";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export const WorkspaceDashboardHeader = () => {
const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false);
// theme
// hooks
const { resolvedTheme } = useTheme();
return (
@@ -19,6 +20,7 @@ export const WorkspaceDashboardHeader = () => {
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} setIsOpen={setIsProductUpdatesModalOpen} />
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -1,13 +1,12 @@
import { FC } from "react";
import { useRouter } from "next/router";
// ui
import { Breadcrumbs } from "@plane/ui";
import { Settings } from "lucide-react";
// hooks
import { observer } from "mobx-react-lite";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
export interface IWorkspaceSettingHeader {
title: string;
@@ -22,6 +21,7 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@@ -1,4 +1,4 @@
import { ChangeEvent, FC, useCallback, useContext, useEffect, useState } from "react";
import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// hooks
import useReloadConfirmations from "hooks/use-reload-confirmation";

View File

@@ -21,7 +21,7 @@ import {
CycleDropdown,
DateDropdown,
EstimateDropdown,
ModuleDropdown,
ModuleSelectDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
@@ -152,7 +152,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
project_id: watch("project_id"),
parent_id: watch("parent_id"),
cycle_id: watch("cycle_id"),
module_id: watch("module_id"),
module_ids: watch("module_ids"),
};
useEffect(() => {
@@ -570,15 +570,17 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
)}
/>
)}
{projectDetails?.module_view && (
{projectDetails?.module_view && workspaceSlug && (
<Controller
control={control}
name="module_id"
name="module_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ModuleDropdown
<ModuleSelectDropdown
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId}
value={value}
value={value || undefined}
onChange={(moduleId) => onChange(moduleId)}
buttonVariant="border-with-text"
/>
@@ -586,6 +588,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
areEstimatesEnabledForProject(projectId) && (
<Controller

View File

@@ -94,7 +94,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
cycle: cycleId.toString(),
}));
}
if (moduleId && !prePopulateDataProps?.module_id) {
if (moduleId && !prePopulateDataProps?.module_ids) {
setPreloadedData((prevData) => ({
...(prevData ?? {}),
...prePopulateDataProps,
@@ -123,7 +123,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
cycle: cycleId.toString(),
}));
}
if (moduleId && !prePopulateDataProps?.module_id) {
if (moduleId && !prePopulateDataProps?.module_ids) {
setPreloadedData((prevData) => ({
...(prevData ?? {}),
...prePopulateDataProps,
@@ -233,11 +233,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
});
};
const addIssueToModule = async (issueId: string, moduleId: string) => {
const addIssueToModule = async (issueId: string, moduleIds: string[]) => {
if (!workspaceSlug || !activeProject) return;
await moduleService.addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, {
issues: [issueId],
await moduleService.addModulesToIssue(workspaceSlug as string, activeProject ?? "", issueId as string, {
modules: moduleIds,
});
};
@@ -248,7 +248,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
.createIssue(workspaceSlug.toString(), activeProject, payload)
.then(async (res) => {
if (payload.cycle_id && payload.cycle_id !== "") await addIssueToCycle(res.id, payload.cycle_id);
if (payload.module_id && payload.module_id !== "") await addIssueToModule(res.id, payload.module_id);
if (payload.module_ids && payload.module_ids.length > 0) await addIssueToModule(res.id, payload.module_ids);
setToastAlert({
type: "success",

View File

@@ -59,7 +59,7 @@ export const IssueModuleActivity: FC<TIssueModuleActivity> = observer((props) =>
rel="noopener noreferrer"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="truncate"> {activity.new_value}</span>
<span className="truncate"> {activity.old_value}</span>
</a>
</>
)}

View File

@@ -1,9 +1,10 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import xor from "lodash/xor";
// hooks
import { useIssueDetail } from "hooks/store";
// components
import { ModuleDropdown } from "components/dropdowns";
import { ModuleSelectDropdown } from "components/dropdowns";
// ui
import { Spinner } from "@plane/ui";
// helpers
@@ -32,58 +33,56 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
const issue = getIssueById(issueId);
const disableSelect = disabled || isUpdating;
const handleIssueModuleChange = async (moduleId: string | null) => {
if (!issue || issue.module_id === moduleId) return;
const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => {
if (!issue) return;
setIsUpdating(true);
if (moduleId) await issueOperations.addIssueToModule?.(workspaceSlug, projectId, moduleId, [issueId]);
else await issueOperations.removeIssueFromModule?.(workspaceSlug, projectId, issue.module_id ?? "", issueId);
if (moduleIds === undefined && issue?.module_ids && issue?.module_ids.length > 0)
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue?.module_ids);
if (typeof moduleIds === "string" && moduleIds)
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [moduleIds]);
if (Array.isArray(moduleIds)) {
if (moduleIds.includes(undefined)) {
await issueOperations.removeModulesFromIssue?.(
workspaceSlug,
projectId,
issueId,
moduleIds.filter((x) => x != undefined) as string[]
);
} else {
const _moduleIds = xor(issue?.module_ids, moduleIds)[0];
if (_moduleIds) {
if (issue?.module_ids?.includes(_moduleIds))
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
else await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
}
}
}
setIsUpdating(false);
};
return (
<div className={cn("flex items-center gap-1 h-full", className)}>
<ModuleDropdown
value={issue?.module_id ?? null}
onChange={handleIssueModuleChange}
buttonVariant="transparent-with-text"
<div className={cn(`flex items-center gap-1 h-full`, className)}>
<ModuleSelectDropdown
workspaceSlug={workspaceSlug}
projectId={projectId}
disabled={disableSelect}
className="w-full group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.module_id ? "" : "text-custom-text-400"}`}
value={issue?.module_ids?.length ? issue?.module_ids : undefined}
onChange={handleIssueModuleChange}
multiple={true}
placeholder="No module"
hideIcon
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/>
{/* <CustomSearchSelect
value={issue?.module_id}
onChange={(value: any) => handleIssueModuleChange(value)}
options={options}
customButton={
<div>
<Tooltip position="left" tooltipContent={`${issueModule?.name ?? "No module"}`}>
<button
type="button"
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
disableSelect ? "cursor-not-allowed" : ""
} max-w-[10rem]`}
>
<span
className={`flex items-center gap-1.5 truncate ${
issueModule ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
<span className="truncate">{issueModule?.name ?? "No module"}</span>
</span>
</button>
</Tooltip>
</div>
}
noChevron
disabled={disableSelect}
/> */}
className={`w-full h-full group`}
buttonContainerClassName="w-full"
buttonClassName={`min-h-8 ${issue?.module_ids?.length ? `` : `text-custom-text-400`}`}
buttonVariant="transparent-with-text"
hideIcon={false}
dropdownArrow={true}
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
showTooltip={true}
/>
{isUpdating && <Spinner className="h-4 w-4" />}
</div>
);

View File

@@ -1,6 +1,5 @@
import { FC } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// components
import { IssueParentSiblingItem } from "./sibling-item";
// hooks

View File

@@ -29,13 +29,19 @@ export type TIssueOperations = {
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
addIssueToModule?: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
removeIssueFromModule?: (
workspaceSlug: string,
projectId: string,
moduleId: string,
issueId: string
) => Promise<void>;
removeModulesFromIssue?: (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleIds: string[]
) => Promise<void>;
};
export type TIssueDetailRoot = {
@@ -57,8 +63,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
removeIssue,
addIssueToCycle,
removeIssueFromCycle,
addIssueToModule,
addModulesToIssue,
removeIssueFromModule,
removeModulesFromIssue,
} = useIssueDetail();
const {
issues: { removeIssue: removeArchivedIssue },
@@ -150,9 +157,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
});
}
},
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
try {
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
setToastAlert({
title: "Module added to issue successfully",
type: "success",
@@ -182,6 +189,27 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
});
}
},
removeModulesFromIssue: async (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleIds: string[]
) => {
try {
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
setToastAlert({
title: "Module removed from issue successfully",
type: "success",
message: "Module removed from issue successfully",
});
} catch (error) {
setToastAlert({
title: "Module remove from issue failed",
type: "error",
message: "Module remove from issue failed",
});
}
},
}),
[
is_archived,
@@ -191,8 +219,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
removeArchivedIssue,
addIssueToCycle,
removeIssueFromCycle,
addIssueToModule,
addModulesToIssue,
removeIssueFromModule,
removeModulesFromIssue,
setToastAlert,
]
);

View File

@@ -286,7 +286,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
)}
{projectDetails?.module_view && (
<div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-2 min-h-8 h-full">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<DiceIcon className="h-4 w-4 flex-shrink-0" />
<span>Module</span>

View File

@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
// hooks
import { useProject, useWorkspace } from "hooks/store";
import { useProject } from "hooks/store";
import useToast from "hooks/use-toast";
import useKeypress from "hooks/use-keypress";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
@@ -57,14 +57,13 @@ const Inputs = (props: any) => {
};
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId, onOpen } = props;
const { formKey, prePopulatedData, quickAddCallback, viewId, onOpen } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { getProjectById } = useProject();
const { getWorkspaceBySlug } = useWorkspace();
// refs
const ref = useRef<HTMLDivElement>(null);
// states
@@ -73,7 +72,6 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast();
// derived values
const workspaceDetail = (workspaceSlug && getWorkspaceBySlug(workspaceSlug.toString())) || null;
const projectDetail = projectId ? getProjectById(projectId.toString()) : null;
const {

View File

@@ -45,7 +45,7 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0)
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);

View File

@@ -45,7 +45,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0)
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);

View File

@@ -46,11 +46,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const issueIds = data.map((i) => i.id);
await issues
.addIssueToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds)
.then((res) => {
updateIssue(workspaceSlug, projectId, res.id, res);
fetchIssue(workspaceSlug, projectId, res.id);
})
.addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds)
.catch(() =>
setToastAlert({
type: "error",
@@ -69,7 +65,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
projectId={projectId}
isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)}
searchParams={{ module: true }}
searchParams={{ module: moduleId != undefined ? [moduleId.toString()] : [] }}
handleOnSubmit={handleAddIssuesToModule}
/>
<div className="grid h-full w-full place-items-center">

View File

@@ -49,7 +49,7 @@ export const ProjectEmptyState: React.FC = observer(() => {
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0)
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);

View File

@@ -13,11 +13,12 @@ import {
} from "components/gantt-chart";
// types
import { TIssue, TUnGroupedIssues } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssueActions } from "../types";
interface IBaseGanttRoot {
@@ -76,12 +77,14 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
viewId={viewId}
enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isAllowed}
showAllBlocks
/>
)}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
showAllBlocks
/>
</div>
</>

View File

@@ -1,4 +1,3 @@
import { useRouter } from "next/router";
// ui
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
// helpers

View File

@@ -44,7 +44,7 @@ export interface IBaseKanBanLayout {
showLoader?: boolean;
viewId?: string;
storeType?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
addIssuesToView?: (issueIds: string[]) => Promise<any>;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
}

View File

@@ -56,7 +56,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
const { setToastAlert } = useToast();
const renderExistingIssueModal = moduleId || cycleId;
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true };
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;

View File

@@ -54,7 +54,7 @@ const defaultValues: Partial<TIssue> = {
};
export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = observer((props) => {
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
const { formKey, prePopulatedData, quickAddCallback, viewId } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

View File

@@ -53,7 +53,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
storeType={EIssuesStoreType.MODULE}
addIssuesToView={(issueIds: string[]) => {
if (!workspaceSlug || !projectId || !moduleId) throw new Error();
return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
}}
/>
);

View File

@@ -49,7 +49,7 @@ interface IBaseListRoot {
};
viewId?: string;
storeType: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
addIssuesToView?: (issueIds: string[]) => Promise<any>;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
}

View File

@@ -37,7 +37,7 @@ export const HeaderGroupByCard = observer(
const { setToastAlert } = useToast();
const renderExistingIssueModal = moduleId || cycleId;
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true };
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;

View File

@@ -51,7 +51,7 @@ export const ModuleListLayout: React.FC = observer(() => {
storeType={EIssuesStoreType.MODULE}
addIssuesToView={(issueIds: string[]) => {
if (!workspaceSlug || !projectId || !moduleId) throw new Error();
return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
}}
/>
);

View File

@@ -61,6 +61,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString())
) {
const routerQueryParams = { ...router.query };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ["workspaceSlug"]: _workspaceSlug, ["globalViewId"]: _globalViewId, ...filters } = routerQueryParams;
let issueFilters: any = {};

View File

@@ -2,7 +2,7 @@ import { FC, useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useIssues, useUser } from "hooks/store";
import { useUser } from "hooks/store";
// views
import { SpreadsheetView } from "./spreadsheet-view";
// types
@@ -36,7 +36,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
// store hooks
const { issueMap } = useIssues();
const {
membership: { currentProjectRole },
} = useUser();
@@ -55,7 +54,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
);
const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues;
const handleIssues = useCallback(

View File

@@ -20,7 +20,7 @@ import {
CycleDropdown,
DateDropdown,
EstimateDropdown,
ModuleDropdown,
ModuleSelectDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
@@ -44,7 +44,7 @@ const defaultValues: Partial<TIssue> = {
assignee_ids: [],
label_ids: [],
cycle_id: null,
module_id: null,
module_ids: null,
start_date: null,
target_date: null,
};
@@ -541,21 +541,24 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
)}
/>
)}
{projectDetails?.module_view && (
{projectDetails?.module_view && workspaceSlug && (
<Controller
control={control}
name="module_id"
name="module_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ModuleDropdown
<ModuleSelectDropdown
workspaceSlug={workspaceSlug.toString()}
projectId={projectId}
value={value}
value={value || undefined}
onChange={(moduleId) => {
onChange(moduleId);
handleFormChange();
}}
buttonVariant="border-with-text"
tabIndex={13}
multiple={true}
showCount={true}
/>
</div>
)}

View File

@@ -108,11 +108,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
fetchCycleDetails(workspaceSlug, activeProjectId, cycleId);
};
const addIssueToModule = async (issue: TIssue, moduleId: string) => {
const addIssueToModule = async (issue: TIssue, moduleIds: string[]) => {
if (!workspaceSlug || !activeProjectId) return;
await moduleIssues.addIssueToModule(workspaceSlug, activeProjectId, moduleId, [issue.id]);
fetchModuleDetails(workspaceSlug, activeProjectId, moduleId);
await moduleIssues.addModulesToIssue(workspaceSlug, activeProjectId, issue.id, moduleIds);
moduleIds.forEach((moduleId) => fetchModuleDetails(workspaceSlug, activeProjectId, moduleId));
};
const handleCreateMoreToggleChange = (value: boolean) => {
@@ -139,8 +139,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE)
await addIssueToCycle(response, payload.cycle_id);
if (payload.module_id && payload.module_id !== "" && storeType !== EIssuesStoreType.MODULE)
await addIssueToModule(response, payload.module_id);
if (payload.module_ids && payload.module_ids.length > 0 && storeType !== EIssuesStoreType.MODULE)
await addIssueToModule(response, payload.module_ids);
setToastAlert({
type: "success",
@@ -278,7 +278,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
data={{
...data,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_id: data?.module_id ? data?.module_id : moduleId ? moduleId : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
}}
onChange={handleFormChange}
onClose={handleClose}
@@ -292,7 +292,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
data={{
...data,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_id: data?.module_id ? data?.module_id : moduleId ? moduleId : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
}}
onClose={() => handleClose(false)}
isCreateMoreToggleEnabled={createMore}

View File

@@ -203,7 +203,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
)}
{projectDetails?.module_view && (
<div className="flex w-full items-center gap-3 h-8">
<div className="flex w-full items-center gap-3 min-h-8 h-full">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<DiceIcon className="h-4 w-4 flex-shrink-0" />
<span>Module</span>

View File

@@ -28,8 +28,19 @@ export type TIssuePeekOperations = {
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
removeIssueFromModule?: (
workspaceSlug: string,
projectId: string,
moduleId: string,
issueId: string
) => Promise<void>;
removeModulesFromIssue?: (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleIds: string[]
) => Promise<void>;
};
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
@@ -48,7 +59,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
removeIssue,
issue: { getIssueById, fetchIssue },
} = useIssueDetail();
const { addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule } = useIssueDetail();
const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } =
useIssueDetail();
// state
const [loader, setLoader] = useState(false);
@@ -143,9 +155,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
});
}
},
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
try {
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
setToastAlert({
title: "Module added to issue successfully",
type: "success",
@@ -175,6 +187,27 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
});
}
},
removeModulesFromIssue: async (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleIds: string[]
) => {
try {
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
setToastAlert({
title: "Module removed from issue successfully",
type: "success",
message: "Module removed from issue successfully",
});
} catch (error) {
setToastAlert({
title: "Module remove from issue failed",
type: "error",
message: "Module remove from issue failed",
});
}
},
}),
[
is_archived,
@@ -184,8 +217,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
removeArchivedIssue,
addIssueToCycle,
removeIssueFromCycle,
addIssueToModule,
addModulesToIssue,
removeIssueFromModule,
removeModulesFromIssue,
setToastAlert,
onIssueUpdate,
]

View File

@@ -1,6 +1,5 @@
import React, { Fragment, useRef, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { observer } from "mobx-react-lite";

View File

@@ -1,8 +1,6 @@
import React, { FC } from "react";
import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import { useApplication } from "hooks/store";
// components
import { PageForm } from "./page-form";
// types
@@ -25,10 +23,6 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
const { workspaceSlug } = router.query;
const { createPage } = useProjectPages();
// store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const createProjectPage = async (payload: IPage) => {
if (!workspaceSlug) return;

View File

@@ -131,6 +131,12 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
setLeaveProjectModal(false);
};
const handleProjectClick = () => {
if (window.innerWidth < 768) {
themeStore.toggleSidebar();
}
};
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
if (!project) return null;
@@ -143,9 +149,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
{({ open }) => (
<>
<div
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${
snapshot?.isDragging ? "opacity-60" : ""
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${snapshot?.isDragging ? "opacity-60" : ""
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
>
{provided && (
<Tooltip
@@ -154,11 +159,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
>
<button
type="button"
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${
isCollapsed ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${
isMenuActive ? "!flex" : ""
}`}
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${isCollapsed ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${isMenuActive ? "!flex" : ""
}`}
{...provided?.dragHandleProps}
>
<MoreVertical className="h-3.5" />
@@ -169,14 +172,12 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
<Disclosure.Button
as="div"
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
isCollapsed ? "justify-center" : `justify-between`
}`}
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${isCollapsed ? "justify-center" : `justify-between`
}`}
>
<div
className={`flex w-full flex-grow items-center gap-x-2 truncate ${
isCollapsed ? "justify-center" : ""
}`}
className={`flex w-full flex-grow items-center gap-x-2 truncate ${isCollapsed ? "justify-center" : ""
}`}
>
{project.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
@@ -196,9 +197,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</div>
{!isCollapsed && (
<ChevronDown
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${
isMenuActive ? "!block" : ""
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${isMenuActive ? "!block" : ""
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
/>
)}
</Disclosure.Button>
@@ -313,7 +313,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
return;
return (
<Link key={item.name} href={item.href}>
<Link key={item.name} href={item.href} onClick={handleProjectClick}>
<span className="block w-full">
<Tooltip
tooltipContent={`${project?.name}: ${item.name}`}
@@ -322,11 +322,10 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
disabled={!isCollapsed}
>
<div
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${isCollapsed ? "justify-center" : ""}`}
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${isCollapsed ? "justify-center" : ""}`}
>
<item.Icon className="h-4 w-4 stroke-[1.5]" />
{!isCollapsed && item.name}

View File

@@ -56,7 +56,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
const { workspaceSlug } = router.query;
// store hooks
const {
theme: { sidebarCollapsed },
theme: { sidebarCollapsed, toggleSidebar },
eventTracker: { setTrackElement },
} = useApplication();
const { currentUser, updateCurrentUser, isUserInstanceAdmin, signOut } = useUser();
@@ -86,6 +86,13 @@ export const WorkspaceSidebarDropdown = observer(() => {
);
};
const handleItemClick = () => {
console.log('CLICKED')
if (window.innerWidth < 768) {
toggleSidebar();
}
};
const workspacesList = Object.values(workspaces ?? {});
// TODO: fix workspaces list scroll
@@ -96,15 +103,13 @@ export const WorkspaceSidebarDropdown = observer(() => {
<>
<Menu.Button className="group/menu-button h-full w-full truncate rounded-md text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none">
<div
className={`flex items-center gap-x-2 truncate rounded p-1 ${
sidebarCollapsed ? "justify-center" : "justify-between"
}`}
className={`flex items-center gap-x-2 truncate rounded p-1 ${sidebarCollapsed ? "justify-center" : "justify-between"
}`}
>
<div className="flex items-center gap-2 truncate">
<div
className={`relative grid h-6 w-6 flex-shrink-0 place-items-center uppercase ${
!activeWorkspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
className={`relative grid h-6 w-6 flex-shrink-0 place-items-center uppercase ${!activeWorkspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
>
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
<img
@@ -126,9 +131,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
{!sidebarCollapsed && (
<ChevronDown
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${
open ? "rotate-180" : ""
} text-custom-sidebar-text-400 duration-300`}
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${open ? "rotate-180" : ""
} text-custom-sidebar-text-400 duration-300`}
/>
)}
</div>
@@ -156,7 +160,10 @@ export const WorkspaceSidebarDropdown = observer(() => {
<Link
key={workspace.id}
href={`/${workspace.slug}`}
onClick={() => handleWorkspaceNavigation(workspace)}
onClick={() => {
handleWorkspaceNavigation(workspace);
handleItemClick();
}}
className="w-full"
>
<Menu.Item
@@ -165,9 +172,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
>
<div className="flex items-center justify-start gap-2.5 truncate">
<span
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo && workspace.logo !== "" ? (
<img
@@ -181,9 +187,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
</span>
<h5
className={`truncate text-sm font-medium ${
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
className={`truncate text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
>
{workspace.name}
</h5>
@@ -220,8 +225,10 @@ export const WorkspaceSidebarDropdown = observer(() => {
Create workspace
</Menu.Item>
</Link>
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link) => (
<Link key={link.key} href={link.href} className="w-full">
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Link key={link.key} href={link.href} className="w-full" onClick={() => {
if (index > 0) handleItemClick();
}}>
<Menu.Item
as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 font-medium"
@@ -278,7 +285,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
{profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Link key={index} href={link.link}>
<Link key={index} href={link.link} onClick={() => { if (index == 0) handleItemClick(); }}>
<Menu.Item key={index} as="div">
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<link.icon className="h-4 w-4 stroke-[1.5]" />

View File

@@ -26,12 +26,21 @@ export const WorkspaceSidebarMenu = observer(() => {
// computed
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
const handleLinkClick = () => {
if (window.innerWidth < 768) {
themeStore.toggleSidebar();
}
};
return (
<div className="w-full cursor-pointer space-y-2 p-4">
{SIDEBAR_MENU_ITEMS.map(
(link) =>
workspaceMemberInfo >= link.access && (
<Link key={link.key} href={`/${workspaceSlug}${link.href}`}>
<Link key={link.key}
href={`/${workspaceSlug}${link.href}`}
onClick={handleLinkClick}
>
<span className="block w-full my-1">
<Tooltip
tooltipContent={link.label}
@@ -40,11 +49,10 @@ export const WorkspaceSidebarMenu = observer(() => {
disabled={!themeStore?.sidebarCollapsed}
>
<div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
link.highlight(router.asPath, `/${workspaceSlug}`)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.asPath, `/${workspaceSlug}`)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
>
{
<link.Icon

View File

@@ -13,7 +13,6 @@ const paramsToKey = (params: any) => {
start_date,
target_date,
sub_issue,
start_target_date,
project,
layout,
subscriber,
@@ -28,7 +27,6 @@ const paramsToKey = (params: any) => {
let createdByKey = created_by ? created_by.split(",") : [];
let labelsKey = labels ? labels.split(",") : [];
let subscriberKey = subscriber ? subscriber.split(",") : [];
const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE";
const startDateKey = start_date ?? "";
const targetDateKey = target_date ?? "";
const type = params.type ? params.type.toUpperCase() : "NULL";
@@ -47,7 +45,7 @@ const paramsToKey = (params: any) => {
labelsKey = labelsKey.sort().join("_");
subscriberKey = subscriberKey.sort().join("_");
return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${mentionsKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}_${subscriberKey}`;
return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${mentionsKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${subscriberKey}`;
};
const myIssuesParamsToKey = (params: any) => {

View File

@@ -105,9 +105,6 @@ export const handleIssueQueryParamsByLayout = (
});
}
// add start_target_date query param for the gantt_chart layout
if (layout === "gantt_chart") queryParams.push("start_target_date");
return queryParams;
};

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