mirror of
https://github.com/makeplane/plane.git
synced 2026-01-27 16:49:09 -06:00
[WEB-2442] feat: Revamp Timeline Layout (#5915)
* chore: added issue relations in issue listing * chore: added pagination for issue detail endpoint * chore: bulk date update endpoint * chore: appended the target date * chore: issue relation new types defined * fix: order by and issue filters * fix: passed order by in pagination * chore: changed the key for issue dates * Revamp Timeline Layout * fix block dragging * minor ui fixes * improve auto scroll UX * remove unused import * fix timeline layout heights * modify base timeline store * Segregate issue relation types --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
@@ -49,44 +49,47 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
allowed.append(list(item.keys())[0])
|
||||
|
||||
for field in allowed:
|
||||
from . import (
|
||||
WorkspaceLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
UserLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueLiteSerializer,
|
||||
IssueRelationSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
IssueReactionLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
)
|
||||
if field not in self.fields:
|
||||
from . import (
|
||||
WorkspaceLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
UserLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueLiteSerializer,
|
||||
IssueRelationSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
IssueReactionLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
expansion = {
|
||||
"user": UserLiteSerializer,
|
||||
"workspace": WorkspaceLiteSerializer,
|
||||
"project": ProjectLiteSerializer,
|
||||
"default_assignee": UserLiteSerializer,
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
"members": UserLiteSerializer,
|
||||
"assignees": UserLiteSerializer,
|
||||
"labels": LabelSerializer,
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueLiteSerializer,
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_inbox": InboxIssueLiteSerializer,
|
||||
"issue_reactions": IssueReactionLiteSerializer,
|
||||
"issue_link": IssueLinkLiteSerializer,
|
||||
"sub_issues": IssueLiteSerializer,
|
||||
}
|
||||
# Expansion mapper
|
||||
expansion = {
|
||||
"user": UserLiteSerializer,
|
||||
"workspace": WorkspaceLiteSerializer,
|
||||
"project": ProjectLiteSerializer,
|
||||
"default_assignee": UserLiteSerializer,
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
"members": UserLiteSerializer,
|
||||
"assignees": UserLiteSerializer,
|
||||
"labels": LabelSerializer,
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueLiteSerializer,
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_related": RelatedIssueSerializer,
|
||||
"issue_inbox": InboxIssueLiteSerializer,
|
||||
"issue_reactions": IssueReactionLiteSerializer,
|
||||
"issue_link": IssueLinkLiteSerializer,
|
||||
"sub_issues": IssueLiteSerializer,
|
||||
}
|
||||
|
||||
if field not in self.fields and field in expansion:
|
||||
self.fields[field] = expansion[field](
|
||||
@@ -104,6 +107,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"issue_attachment",
|
||||
"issue_link",
|
||||
"sub_issues",
|
||||
"issue_related",
|
||||
]
|
||||
else False
|
||||
)
|
||||
@@ -133,6 +137,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
IssueReactionLiteSerializer,
|
||||
IssueAttachmentLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@@ -153,6 +158,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueLiteSerializer,
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_related": RelatedIssueSerializer,
|
||||
"issue_inbox": InboxIssueLiteSerializer,
|
||||
"issue_reactions": IssueReactionLiteSerializer,
|
||||
"issue_attachment": IssueAttachmentLiteSerializer,
|
||||
|
||||
@@ -21,7 +21,9 @@ from plane.app.views import (
|
||||
BulkArchiveIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
IssueDetailEndpoint,
|
||||
IssueAttachmentV2Endpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -40,6 +42,12 @@ urlpatterns = [
|
||||
),
|
||||
name="project-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues-detail/",
|
||||
IssueDetailEndpoint.as_view(),
|
||||
name="project-issue-detail",
|
||||
),
|
||||
# updated v1 paginated issues
|
||||
# updated v2 paginated issues
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
|
||||
@@ -307,4 +315,9 @@ urlpatterns = [
|
||||
DeletedIssuesListViewSet.as_view(),
|
||||
name="deleted-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-dates/",
|
||||
IssueBulkUpdateDateEndpoint.as_view(),
|
||||
name="project-issue-dates",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -130,6 +130,8 @@ from .issue.base import (
|
||||
BulkDeleteIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
IssueDetailEndpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
)
|
||||
|
||||
from .issue.activity import (
|
||||
|
||||
@@ -976,3 +976,194 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueDetailEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
.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")
|
||||
)
|
||||
)
|
||||
issue = issue.filter(**filters)
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
# Issue queryset
|
||||
issue, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=(issue),
|
||||
on_results=lambda issue: IssueSerializer(
|
||||
issue,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
|
||||
def validate_dates(
|
||||
self, current_start, current_target, new_start, new_target
|
||||
):
|
||||
"""
|
||||
Validate that start date is before target date.
|
||||
"""
|
||||
start = new_start or current_start
|
||||
target = new_target or current_target
|
||||
|
||||
if start and target and start > target:
|
||||
return False
|
||||
return True
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id):
|
||||
|
||||
updates = request.data.get("updates", [])
|
||||
|
||||
issue_ids = [update["id"] for update in updates]
|
||||
epoch = int(timezone.now().timestamp())
|
||||
|
||||
# Fetch all relevant issues in a single query
|
||||
issues = list(Issue.objects.filter(id__in=issue_ids))
|
||||
issues_dict = {str(issue.id): issue for issue in issues}
|
||||
issues_to_update = []
|
||||
|
||||
for update in updates:
|
||||
issue_id = update["id"]
|
||||
issue = issues_dict.get(issue_id)
|
||||
|
||||
if not issue:
|
||||
continue
|
||||
|
||||
start_date = update.get("start_date")
|
||||
target_date = update.get("target_date")
|
||||
validate_dates = self.validate_dates(
|
||||
issue.start_date, issue.target_date, start_date, target_date
|
||||
)
|
||||
if not validate_dates:
|
||||
return Response(
|
||||
{
|
||||
"message": "Start date cannot exceed target date",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if start_date:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"start_date": update.get("start_date")}
|
||||
),
|
||||
current_instance=json.dumps(
|
||||
{"start_date": str(issue.start_date)}
|
||||
),
|
||||
issue_id=str(issue_id),
|
||||
actor_id=str(request.user.id),
|
||||
project_id=str(project_id),
|
||||
epoch=epoch,
|
||||
)
|
||||
issue.start_date = start_date
|
||||
issues_to_update.append(issue)
|
||||
|
||||
if target_date:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"target_date": update.get("target_date")}
|
||||
),
|
||||
current_instance=json.dumps(
|
||||
{"target_date": str(issue.target_date)}
|
||||
),
|
||||
issue_id=str(issue_id),
|
||||
actor_id=str(request.user.id),
|
||||
project_id=str(project_id),
|
||||
epoch=epoch,
|
||||
)
|
||||
issue.target_date = target_date
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk update issues
|
||||
Issue.objects.bulk_update(
|
||||
issues_to_update, ["start_date", "target_date"]
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Issues updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ from plane.db.models import (
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_relation_mapper import get_actual_relation
|
||||
|
||||
|
||||
class IssueRelationViewSet(BaseViewSet):
|
||||
@@ -89,6 +90,26 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
related_issue_id=issue_id, relation_type="relates_to"
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all start after issues
|
||||
start_after_issues = issue_relations.filter(
|
||||
relation_type="start_before", related_issue_id=issue_id
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all start_before issues
|
||||
start_before_issues = issue_relations.filter(
|
||||
relation_type="start_before", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all finish after issues
|
||||
finish_after_issues = issue_relations.filter(
|
||||
relation_type="finish_before", related_issue_id=issue_id
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all finish before issues
|
||||
finish_before_issues = issue_relations.filter(
|
||||
relation_type="finish_before", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
@@ -211,12 +232,50 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"start_after": queryset.filter(pk__in=start_after_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"start_after",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"start_before": queryset.filter(pk__in=start_before_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"start_before",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"finish_after": queryset.filter(pk__in=finish_after_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"finish_after",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"finish_before": queryset.filter(pk__in=finish_before_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"finish_before",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
relation_type = request.data.get("relation_type", None)
|
||||
if relation_type is None:
|
||||
return Response(
|
||||
{"message": "Issue relation type is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
issues = request.data.get("issues", [])
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
@@ -224,16 +283,18 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=(
|
||||
issue if relation_type == "blocking" else issue_id
|
||||
issue
|
||||
if relation_type
|
||||
in ["blocking", "start_after", "finish_after"]
|
||||
else issue_id
|
||||
),
|
||||
related_issue_id=(
|
||||
issue_id if relation_type == "blocking" else issue
|
||||
),
|
||||
relation_type=(
|
||||
"blocked_by"
|
||||
if relation_type == "blocking"
|
||||
else relation_type
|
||||
issue_id
|
||||
if relation_type
|
||||
in ["blocking", "start_after", "finish_after"]
|
||||
else issue
|
||||
),
|
||||
relation_type=(get_actual_relation(relation_type)),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
|
||||
@@ -31,6 +31,7 @@ from plane.db.models import (
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.bgtasks.webhook_task import webhook_activity
|
||||
from plane.utils.issue_relation_mapper import get_inverse_relation
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
@@ -1394,6 +1395,9 @@ def create_issue_relation_activity(
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
inverse_relation = get_inverse_relation(
|
||||
requested_data.get("relation_type")
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1402,19 +1406,10 @@ def create_issue_relation_activity(
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
field=(
|
||||
"blocking"
|
||||
if requested_data.get("relation_type") == "blocked_by"
|
||||
else (
|
||||
"blocked_by"
|
||||
if requested_data.get("relation_type")
|
||||
== "blocking"
|
||||
else requested_data.get("relation_type")
|
||||
)
|
||||
),
|
||||
field=inverse_relation,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f'added {"blocking" if requested_data.get("relation_type") == "blocked_by" else ("blocked_by" if requested_data.get("relation_type") == "blocking" else requested_data.get("relation_type")),} relation',
|
||||
comment=f"added {inverse_relation} relation",
|
||||
old_identifier=issue_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
|
||||
@@ -279,6 +279,8 @@ class IssueRelation(ProjectBaseModel):
|
||||
("duplicate", "Duplicate"),
|
||||
("relates_to", "Relates To"),
|
||||
("blocked_by", "Blocked By"),
|
||||
("start_before", "Start Before"),
|
||||
("finish_before", "Finish Before"),
|
||||
)
|
||||
|
||||
issue = models.ForeignKey(
|
||||
|
||||
24
apiserver/plane/utils/issue_relation_mapper.py
Normal file
24
apiserver/plane/utils/issue_relation_mapper.py
Normal file
@@ -0,0 +1,24 @@
|
||||
def get_inverse_relation(relation_type):
|
||||
relation_mapping = {
|
||||
"start_after": "start_before",
|
||||
"finish_after": "finish_before",
|
||||
"blocked_by": "blocking",
|
||||
"blocking": "blocked_by",
|
||||
"start_before": "start_after",
|
||||
"finish_before": "finish_after",
|
||||
}
|
||||
return relation_mapping.get(relation_type, relation_type)
|
||||
|
||||
|
||||
def get_actual_relation(relation_type):
|
||||
# This function is used to get the actual relation type which is store in database
|
||||
actual_relation = {
|
||||
"start_after": "start_before",
|
||||
"finish_after": "finish_before",
|
||||
"blocking": "blocked_by",
|
||||
"blocked_by": "blocked_by",
|
||||
"start_before": "start_before",
|
||||
"finish_before": "finish_before",
|
||||
}
|
||||
|
||||
return actual_relation.get(relation_type, relation_type)
|
||||
@@ -289,6 +289,7 @@ module.exports = {
|
||||
},
|
||||
// scale down font sizes to 90% of default
|
||||
fontSize: {
|
||||
"2xs": "0.5625rem",
|
||||
xs: "0.675rem",
|
||||
sm: "0.7875rem",
|
||||
base: "0.9rem",
|
||||
|
||||
2
packages/types/src/dashboard.d.ts
vendored
2
packages/types/src/dashboard.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
import { EDurationFilters } from "./enums";
|
||||
import { IIssueActivity, TIssuePriorities } from "./issues";
|
||||
import { TIssue } from "./issues/issue";
|
||||
import { TIssueRelationTypes } from "./issues/issue_relation";
|
||||
import { TStateGroups } from "./state";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
|
||||
export type TWidgetKeys =
|
||||
| "overview_stats"
|
||||
|
||||
11
packages/types/src/issues/issue.d.ts
vendored
11
packages/types/src/issues/issue.d.ts
vendored
@@ -2,6 +2,7 @@ import { TIssuePriorities } from "../issues";
|
||||
import { TIssueAttachment } from "./issue_attachment";
|
||||
import { TIssueLink } from "./issue_link";
|
||||
import { TIssueReaction } from "./issue_reaction";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
|
||||
// new issue structure types
|
||||
|
||||
@@ -40,6 +41,14 @@ export type TBaseIssue = {
|
||||
is_draft: boolean;
|
||||
};
|
||||
|
||||
export type IssueRelation = {
|
||||
id: string;
|
||||
name: string;
|
||||
project_id: string;
|
||||
relation_type: TIssueRelationTypes;
|
||||
sequence_id: number;
|
||||
};
|
||||
|
||||
export type TIssue = TBaseIssue & {
|
||||
description_html?: string;
|
||||
is_subscribed?: boolean;
|
||||
@@ -47,6 +56,8 @@ export type TIssue = TBaseIssue & {
|
||||
issue_reactions?: TIssueReaction[];
|
||||
issue_attachments?: TIssueAttachment[];
|
||||
issue_link?: TIssueLink[];
|
||||
issue_relation?: IssueRelation[];
|
||||
issue_related?: IssueRelation[];
|
||||
// tempId is used for optimistic updates. It is not a part of the API response.
|
||||
tempId?: string;
|
||||
// sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response.
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
import { TIssue } from "./issues";
|
||||
|
||||
export type TIssueRelationTypes =
|
||||
| "blocking"
|
||||
| "blocked_by"
|
||||
| "duplicate"
|
||||
| "relates_to";
|
||||
|
||||
export type TIssueRelation = Record<TIssueRelationTypes, TIssue[]>;
|
||||
|
||||
export type TIssueRelationMap = {
|
||||
|
||||
3
packages/types/src/view-props.d.ts
vendored
3
packages/types/src/view-props.d.ts
vendored
@@ -78,7 +78,8 @@ export type TIssueParams =
|
||||
| "cursor"
|
||||
| "per_page"
|
||||
| "issue_type"
|
||||
| "layout";
|
||||
| "layout"
|
||||
| "expand";
|
||||
|
||||
export type TCalendarLayouts = "month" | "week";
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./left-draggable";
|
||||
export * from "./right-draggable";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { RefObject } from "react";
|
||||
import { IGanttBlock } from "@/components/gantt-chart";
|
||||
|
||||
type LeftDependencyDraggableProps = {
|
||||
block: IGanttBlock;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const LeftDependencyDraggable = (props: LeftDependencyDraggableProps) => <></>;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RefObject } from "react";
|
||||
import { IGanttBlock } from "@/components/gantt-chart";
|
||||
|
||||
type RightDependencyDraggableProps = {
|
||||
block: IGanttBlock;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
export const RightDependencyDraggable = (props: RightDependencyDraggableProps) => <></>;
|
||||
@@ -0,0 +1 @@
|
||||
export const TimelineDependencyPaths = () => <></>;
|
||||
@@ -0,0 +1 @@
|
||||
export const TimelineDraggablePath = () => <></>;
|
||||
3
web/ce/components/gantt-chart/dependency/index.ts
Normal file
3
web/ce/components/gantt-chart/dependency/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./blockDraggables";
|
||||
export * from "./dependency-paths";
|
||||
export * from "./draggable-dependency-path";
|
||||
1
web/ce/components/gantt-chart/index.ts
Normal file
1
web/ce/components/gantt-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./dependency";
|
||||
35
web/ce/components/relations/index.tsx
Normal file
35
web/ce/components/relations/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { CircleDot, CopyPlus, XCircle } from "lucide-react";
|
||||
import { RelatedIcon } from "@plane/ui";
|
||||
import { TRelationObject } from "@/components/issues";
|
||||
import { TIssueRelationTypes } from "../../types";
|
||||
|
||||
export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject> = {
|
||||
relates_to: {
|
||||
key: "relates_to",
|
||||
label: "Relates to",
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
icon: (size) => <RelatedIcon height={size} width={size} className="text-custom-text-200" />,
|
||||
placeholder: "Add related issues",
|
||||
},
|
||||
duplicate: {
|
||||
key: "duplicate",
|
||||
label: "Duplicate of",
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
icon: (size) => <CopyPlus size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
blocked_by: {
|
||||
key: "blocked_by",
|
||||
label: "Blocked by",
|
||||
className: "bg-red-500/20 text-red-700",
|
||||
icon: (size) => <CircleDot size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
blocking: {
|
||||
key: "blocking",
|
||||
label: "Blocking",
|
||||
className: "bg-yellow-500/20 text-yellow-700",
|
||||
icon: (size) => <XCircle size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
};
|
||||
8
web/ce/constants/gantt-chart.ts
Normal file
8
web/ce/constants/gantt-chart.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { TIssueRelationTypes } from "../types";
|
||||
|
||||
export const REVERSE_RELATIONS: { [key in TIssueRelationTypes]: TIssueRelationTypes } = {
|
||||
blocked_by: "blocking",
|
||||
blocking: "blocked_by",
|
||||
relates_to: "relates_to",
|
||||
duplicate: "duplicate",
|
||||
};
|
||||
7
web/ce/constants/index.ts
Normal file
7
web/ce/constants/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./ai";
|
||||
export * from "./estimates";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./issues";
|
||||
export * from "./project";
|
||||
export * from "./user-permissions";
|
||||
export * from "./workspace";
|
||||
@@ -33,3 +33,5 @@ export const filterActivityOnSelectedFilters = (
|
||||
|
||||
// boolean to decide if the local db cache is enabled
|
||||
export const ENABLE_LOCAL_DB_CACHE = false;
|
||||
|
||||
export const ENABLE_ISSUE_DEPENDENCIES = false;
|
||||
|
||||
283
web/ce/store/timeline/base-timeline.store.ts
Normal file
283
web/ce/store/timeline/base-timeline.store.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import set from "lodash/set";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// components
|
||||
import { ChartDataType, IBlockUpdateDependencyData, IGanttBlock, TGanttViews } from "@/components/gantt-chart";
|
||||
import { currentViewDataWithView } from "@/components/gantt-chart/data";
|
||||
import { getDateFromPositionOnGantt, getItemPositionWidth } from "@/components/gantt-chart/views/helpers";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
// types
|
||||
type BlockData = {
|
||||
id: string;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
start_date: string | undefined | null;
|
||||
target_date: string | undefined | null;
|
||||
};
|
||||
|
||||
export interface IBaseTimelineStore {
|
||||
// observables
|
||||
currentView: TGanttViews;
|
||||
currentViewData: ChartDataType | undefined;
|
||||
activeBlockId: string | null;
|
||||
renderView: any;
|
||||
isDragging: boolean;
|
||||
//
|
||||
setBlockIds: (ids: string[]) => void;
|
||||
getBlockById: (blockId: string) => IGanttBlock;
|
||||
// computed functions
|
||||
getIsCurrentDependencyDragging: (blockId: string) => boolean;
|
||||
isBlockActive: (blockId: string) => boolean;
|
||||
// actions
|
||||
updateCurrentView: (view: TGanttViews) => void;
|
||||
updateCurrentViewData: (data: ChartDataType | undefined) => void;
|
||||
updateActiveBlockId: (blockId: string | null) => void;
|
||||
updateRenderView: (data: any) => void;
|
||||
getUpdatedPositionAfterDrag: (id: string, ignoreDependencies?: boolean) => IBlockUpdateDependencyData[];
|
||||
updateBlockPosition: (id: string, deltaLeft: number, deltaWidth: number, ignoreDependencies?: boolean) => void;
|
||||
getNumberOfDaysFromPosition: (position: number | undefined) => number | undefined;
|
||||
setIsDragging: (isDragging: boolean) => void;
|
||||
initGantt: () => void;
|
||||
|
||||
getDateFromPositionOnGantt: (position: number, offsetDays: number) => Date | undefined;
|
||||
}
|
||||
|
||||
export class BaseTimeLineStore implements IBaseTimelineStore {
|
||||
blocksMap: Record<string, IGanttBlock> = {};
|
||||
blockIds: string[] | undefined = undefined;
|
||||
|
||||
isDragging: boolean = false;
|
||||
currentView: TGanttViews = "week";
|
||||
currentViewData: ChartDataType | undefined = undefined;
|
||||
activeBlockId: string | null = null;
|
||||
renderView: any = [];
|
||||
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
blocksMap: observable,
|
||||
blockIds: observable,
|
||||
isDragging: observable.ref,
|
||||
currentView: observable.ref,
|
||||
currentViewData: observable,
|
||||
activeBlockId: observable.ref,
|
||||
renderView: observable,
|
||||
// actions
|
||||
setIsDragging: action,
|
||||
setBlockIds: action.bound,
|
||||
initGantt: action.bound,
|
||||
updateCurrentView: action.bound,
|
||||
updateCurrentViewData: action.bound,
|
||||
updateActiveBlockId: action.bound,
|
||||
updateRenderView: action.bound,
|
||||
});
|
||||
|
||||
this.initGantt();
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Block Ids to derive blocks from
|
||||
* @param ids
|
||||
*/
|
||||
setBlockIds = (ids: string[]) => {
|
||||
this.blockIds = ids;
|
||||
};
|
||||
|
||||
/**
|
||||
* setIsDragging
|
||||
* @param isDragging
|
||||
*/
|
||||
setIsDragging = (isDragging: boolean) => {
|
||||
runInAction(() => {
|
||||
this.isDragging = isDragging;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description check if block is active
|
||||
* @param {string} blockId
|
||||
*/
|
||||
isBlockActive = computedFn((blockId: string): boolean => this.activeBlockId === blockId);
|
||||
|
||||
/**
|
||||
* @description update current view
|
||||
* @param {TGanttViews} view
|
||||
*/
|
||||
updateCurrentView = (view: TGanttViews) => {
|
||||
this.currentView = view;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update current view data
|
||||
* @param {ChartDataType | undefined} data
|
||||
*/
|
||||
updateCurrentViewData = (data: ChartDataType | undefined) => {
|
||||
runInAction(() => {
|
||||
this.currentViewData = data;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update active block
|
||||
* @param {string | null} block
|
||||
*/
|
||||
updateActiveBlockId = (blockId: string | null) => {
|
||||
this.activeBlockId = blockId;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update render view
|
||||
* @param {any[]} data
|
||||
*/
|
||||
updateRenderView = (data: any[]) => {
|
||||
this.renderView = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description initialize gantt chart with month view
|
||||
*/
|
||||
initGantt = () => {
|
||||
const newCurrentViewData = currentViewDataWithView(this.currentView);
|
||||
|
||||
runInAction(() => {
|
||||
this.currentViewData = newCurrentViewData;
|
||||
this.blocksMap = {};
|
||||
this.blockIds = undefined;
|
||||
});
|
||||
};
|
||||
|
||||
/** Gets Block from Id */
|
||||
getBlockById = computedFn((blockId: string) => this.blocksMap[blockId]);
|
||||
|
||||
/**
|
||||
* updates the BlocksMap from blockIds
|
||||
* @param getDataById
|
||||
* @returns
|
||||
*/
|
||||
updateBlocks(getDataById: (id: string) => BlockData | undefined | null) {
|
||||
if (!this.blockIds || !Array.isArray(this.blockIds) || this.isDragging) return true;
|
||||
|
||||
const updatedBlockMaps: { path: string[]; value: any }[] = [];
|
||||
const newBlocks: IGanttBlock[] = [];
|
||||
|
||||
// Loop through blockIds to generate blocks Data
|
||||
for (const blockId of this.blockIds) {
|
||||
const blockData = getDataById(blockId);
|
||||
if (!blockData) continue;
|
||||
|
||||
const block: IGanttBlock = {
|
||||
data: blockData,
|
||||
id: blockData?.id,
|
||||
name: blockData.name,
|
||||
sort_order: blockData?.sort_order,
|
||||
start_date: blockData?.start_date ?? undefined,
|
||||
target_date: blockData?.target_date ?? undefined,
|
||||
};
|
||||
if (this.currentViewData && this.currentViewData?.data?.startDate && this.currentViewData?.data?.dayWidth) {
|
||||
block.position = getItemPositionWidth(this.currentViewData, block);
|
||||
}
|
||||
|
||||
// create block updates if the block already exists, or push them to newBlocks
|
||||
if (this.blocksMap[blockId]) {
|
||||
for (const key of Object.keys(block)) {
|
||||
const currValue = this.blocksMap[blockId][key as keyof IGanttBlock];
|
||||
const nextValue = block[key as keyof IGanttBlock];
|
||||
if (!isEqual(currValue, nextValue)) {
|
||||
updatedBlockMaps.push({ path: [blockId, key], value: nextValue });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newBlocks.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
// update the store with the block updates
|
||||
runInAction(() => {
|
||||
for (const updatedBlock of updatedBlockMaps) {
|
||||
set(this.blocksMap, updatedBlock.path, updatedBlock.value);
|
||||
}
|
||||
|
||||
for (const newBlock of newBlocks) {
|
||||
set(this.blocksMap, [newBlock.id], newBlock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* returns number of days that the position pixels span across the timeline chart
|
||||
* @param position
|
||||
* @returns
|
||||
*/
|
||||
getNumberOfDaysFromPosition = (position: number | undefined) => {
|
||||
if (!this.currentViewData || !position) return;
|
||||
|
||||
return Math.round(position / this.currentViewData.data.dayWidth);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the date at which the position corresponds to on the timeline chart
|
||||
*/
|
||||
getDateFromPositionOnGantt = computedFn((position: number, offsetDays: number) => {
|
||||
if (!this.currentViewData) return;
|
||||
|
||||
return getDateFromPositionOnGantt(position, this.currentViewData, offsetDays);
|
||||
});
|
||||
|
||||
/**
|
||||
* returns updates dates of blocks post drag.
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
getUpdatedPositionAfterDrag = action((id: string) => {
|
||||
const currBlock = this.blocksMap[id];
|
||||
|
||||
if (!currBlock?.position || !this.currentViewData) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id,
|
||||
start_date: renderFormattedPayloadDate(
|
||||
getDateFromPositionOnGantt(currBlock.position.marginLeft, this.currentViewData)
|
||||
),
|
||||
target_date: renderFormattedPayloadDate(
|
||||
getDateFromPositionOnGantt(currBlock.position.marginLeft + currBlock.position.width, this.currentViewData, -1)
|
||||
),
|
||||
},
|
||||
] as IBlockUpdateDependencyData[];
|
||||
});
|
||||
|
||||
/**
|
||||
* updates the block's position such as marginLeft and width wile dragging
|
||||
* @param id
|
||||
* @param deltaLeft
|
||||
* @param deltaWidth
|
||||
* @returns
|
||||
*/
|
||||
updateBlockPosition = action((id: string, deltaLeft: number, deltaWidth: number) => {
|
||||
const currBlock = this.blocksMap[id];
|
||||
|
||||
if (!currBlock?.position) return;
|
||||
|
||||
const newMarginLeft = currBlock.position.marginLeft + deltaLeft;
|
||||
const newWidth = currBlock.position.width + deltaWidth;
|
||||
|
||||
runInAction(() => {
|
||||
set(this.blocksMap, [id, "position"], {
|
||||
marginLeft: newMarginLeft ?? currBlock.position?.marginLeft,
|
||||
width: newWidth ?? currBlock.position?.width,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Dummy method to return if the current Block's dependency is being dragged
|
||||
getIsCurrentDependencyDragging = computedFn((blockId: string) => false);
|
||||
}
|
||||
1
web/ce/types/gantt-chart.ts
Normal file
1
web/ce/types/gantt-chart.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type TIssueRelationTypes = "blocking" | "blocked_by" | "duplicate" | "relates_to";
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./projects";
|
||||
export * from "./issue-types";
|
||||
export * from "./gantt-chart";
|
||||
|
||||
@@ -50,7 +50,7 @@ const defaultValues: Partial<IApiToken> = {
|
||||
expired_at: null,
|
||||
};
|
||||
|
||||
const getExpiryDate = (val: string): string | null => {
|
||||
const getExpiryDate = (val: string): string | null | undefined => {
|
||||
const today = new Date();
|
||||
|
||||
const dateToAdd = EXPIRY_DATE_OPTIONS.find((option) => option.key === val)?.value;
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
defaultValue?: boolean;
|
||||
shouldRecordHeights?: boolean;
|
||||
useIdletime?: boolean;
|
||||
forceRender?: boolean;
|
||||
};
|
||||
|
||||
const RenderIfVisible: React.FC<Props> = (props) => {
|
||||
@@ -29,12 +30,13 @@ const RenderIfVisible: React.FC<Props> = (props) => {
|
||||
placeholderChildren = null, //placeholder children
|
||||
defaultValue = false,
|
||||
useIdletime = false,
|
||||
forceRender = false,
|
||||
} = props;
|
||||
const [shouldVisible, setShouldVisible] = useState<boolean>(defaultValue);
|
||||
const placeholderHeight = useRef<string>(defaultHeight);
|
||||
const intersectionRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const isVisible = shouldVisible;
|
||||
const isVisible = shouldVisible || forceRender;
|
||||
|
||||
// Set visibility with intersection observer
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Tooltip, ContrastIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type Props = {
|
||||
cycleId: string;
|
||||
};
|
||||
|
||||
export const CycleGanttBlock: React.FC<Props> = observer((props) => {
|
||||
const { cycleId } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { workspaceSlug } = useParams();
|
||||
const { getCycleById } = useCycle();
|
||||
// derived values
|
||||
const cycleDetails = getCycleById(cycleId);
|
||||
const { isMobile } = usePlatformOS();
|
||||
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-full w-full items-center rounded"
|
||||
style={{
|
||||
backgroundColor:
|
||||
cycleStatus === "current"
|
||||
? "#09a953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#f7ae59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3f76ff"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: "",
|
||||
}}
|
||||
onClick={() =>
|
||||
router.push(`/${workspaceSlug?.toString()}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)
|
||||
}
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{cycleDetails?.name}</h5>
|
||||
<div>
|
||||
{renderFormattedDate(cycleDetails?.start_date ?? "")} to{" "}
|
||||
{renderFormattedDate(cycleDetails?.end_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
position="top-left"
|
||||
>
|
||||
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{cycleDetails?.name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const { cycleId } = props;
|
||||
// store hooks
|
||||
const { workspaceSlug } = useParams();
|
||||
const { getCycleById } = useCycle();
|
||||
// derived values
|
||||
const cycleDetails = getCycleById(cycleId);
|
||||
|
||||
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="relative flex h-full w-full items-center gap-2"
|
||||
href={`/${workspaceSlug?.toString()}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`}
|
||||
draggable={false}
|
||||
>
|
||||
<ContrastIcon
|
||||
className="h-5 w-5 flex-shrink-0"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09a953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#f7ae59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3f76ff"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ICycle } from "@plane/types";
|
||||
// components
|
||||
import { CycleGanttBlock } from "@/components/cycles";
|
||||
import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar, ChartDataType } from "@/components/gantt-chart";
|
||||
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
||||
// helpers
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
cycleIds: string[];
|
||||
};
|
||||
|
||||
export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||
const { cycleIds } = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { getCycleById, updateCycleDetails } = useCycle();
|
||||
|
||||
const getBlockById = useCallback(
|
||||
(id: string, currentViewData?: ChartDataType | undefined) => {
|
||||
const cycle = getCycleById(id);
|
||||
const block = {
|
||||
data: cycle,
|
||||
id: cycle?.id ?? "",
|
||||
sort_order: cycle?.sort_order ?? 0,
|
||||
start_date: getDate(cycle?.start_date),
|
||||
target_date: getDate(cycle?.end_date),
|
||||
};
|
||||
|
||||
if (currentViewData) {
|
||||
return {
|
||||
...block,
|
||||
position: getMonthChartItemPositionWidthInMonth(currentViewData, block),
|
||||
};
|
||||
}
|
||||
return block;
|
||||
},
|
||||
[getCycleById]
|
||||
);
|
||||
|
||||
const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !cycle) return;
|
||||
|
||||
const payload: any = { ...data };
|
||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||
|
||||
await updateCycleDetails(workspaceSlug.toString(), cycle.project_id, cycle.id, payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<GanttChartRoot
|
||||
title="Cycles"
|
||||
loaderTitle="Cycles"
|
||||
blockIds={cycleIds}
|
||||
getBlockById={getBlockById}
|
||||
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
|
||||
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
|
||||
blockToRender={(data: ICycle) => <CycleGanttBlock cycleId={data.id} />}
|
||||
enableBlockLeftResize={false}
|
||||
enableBlockRightResize={false}
|
||||
enableBlockMove={false}
|
||||
enableReorder
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./blocks";
|
||||
export * from "./cycles-list-layout";
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from "./active-cycle";
|
||||
export * from "./applied-filters";
|
||||
export * from "./dropdowns";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./list";
|
||||
export * from "./cycle-peek-overview";
|
||||
export * from "./cycles-view-header";
|
||||
|
||||
@@ -72,7 +72,6 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
|
||||
projectIdentifier={blockedByIssueProjectDetails?.identifier}
|
||||
projectId={blockedByIssueProjectDetails?.id}
|
||||
issueSequenceId={blockedByIssues[0]?.sequence_id}
|
||||
issueTypeId={blockedByIssues[0]?.type_id}
|
||||
textContainerClassName="text-xs text-custom-text-200 font-medium"
|
||||
/>
|
||||
)
|
||||
|
||||
49
web/core/components/gantt-chart/blocks/block-row-list.tsx
Normal file
49
web/core/components/gantt-chart/blocks/block-row-list.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
// hooks
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
// types
|
||||
import { BLOCK_HEIGHT } from "../constants";
|
||||
import { IBlockUpdateData } from "../types";
|
||||
import { BlockRow } from "./block-row";
|
||||
|
||||
export type GanttChartBlocksProps = {
|
||||
blockIds: string[];
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||
showAllBlocks: boolean;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const GanttChartRowList: FC<GanttChartBlocksProps> = (props) => {
|
||||
const { blockIds, blockUpdateHandler, enableAddBlock, showAllBlocks, selectionHelpers, ganttContainerRef } = props;
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 min-w-full w-max">
|
||||
{blockIds?.map((blockId) => (
|
||||
<>
|
||||
<RenderIfVisible
|
||||
root={ganttContainerRef}
|
||||
horizontalOffset={100}
|
||||
verticalOffset={200}
|
||||
classNames="relative min-w-full w-max"
|
||||
placeholderChildren={<div className="w-full pointer-events-none" style={{ height: `${BLOCK_HEIGHT}px` }} />}
|
||||
shouldRecordHeights={false}
|
||||
>
|
||||
<BlockRow
|
||||
key={blockId}
|
||||
blockId={blockId}
|
||||
showAllBlocks={showAllBlocks}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock}
|
||||
selectionHelpers={selectionHelpers}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
</RenderIfVisible>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
122
web/core/components/gantt-chart/blocks/block-row.tsx
Normal file
122
web/core/components/gantt-chart/blocks/block-row.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { BLOCK_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
||||
import { ChartAddBlock } from "../helpers";
|
||||
import { IBlockUpdateData } from "../types";
|
||||
|
||||
type Props = {
|
||||
blockId: string;
|
||||
showAllBlocks: boolean;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
enableAddBlock: boolean;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const BlockRow: React.FC<Props> = observer((props) => {
|
||||
const { blockId, showAllBlocks, blockUpdateHandler, enableAddBlock, selectionHelpers } = props;
|
||||
// states
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
const [isBlockHiddenOnLeft, setIsBlockHiddenOnLeft] = useState(false);
|
||||
// store hooks
|
||||
const { getBlockById, updateActiveBlockId, isBlockActive } = useTimeLineChartStore();
|
||||
const { getIsIssuePeeked } = useIssueDetail();
|
||||
|
||||
const block = getBlockById(blockId);
|
||||
|
||||
useEffect(() => {
|
||||
const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
const timelineBlock = document.getElementById(`gantt-block-${block?.id}`);
|
||||
if (!timelineBlock || !intersectionRoot) return;
|
||||
|
||||
setIsBlockHiddenOnLeft(
|
||||
!!block.position?.marginLeft &&
|
||||
!!block.position?.width &&
|
||||
intersectionRoot.scrollLeft > block.position.marginLeft + block.position.width
|
||||
);
|
||||
|
||||
// Observe if the block is visible on the chart
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
setIsHidden(!entry.isIntersecting);
|
||||
setIsBlockHiddenOnLeft(entry.boundingClientRect.left < 0);
|
||||
});
|
||||
},
|
||||
{
|
||||
root: intersectionRoot,
|
||||
rootMargin: `0px 0px 0px -${SIDEBAR_WIDTH}px`,
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(timelineBlock);
|
||||
|
||||
return () => {
|
||||
observer.unobserve(timelineBlock);
|
||||
};
|
||||
}, [block]);
|
||||
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || !block.data || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
|
||||
|
||||
// scroll to a hidden block
|
||||
const handleScrollToBlock = () => {
|
||||
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
if (!scrollContainer || !block.position) return;
|
||||
// update container's scroll position to the block's position
|
||||
scrollContainer.scrollLeft = block.position.marginLeft - 4;
|
||||
};
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
|
||||
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
|
||||
const isBlockHoveredOn = isBlockActive(block.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative min-w-full w-max"
|
||||
onMouseEnter={() => updateActiveBlockId(blockId)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn("relative h-full", {
|
||||
"rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
|
||||
"bg-custom-background-90": isBlockHoveredOn,
|
||||
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isBlockSelected,
|
||||
"bg-custom-primary-100/10": isBlockSelected && isBlockHoveredOn,
|
||||
"border border-r-0 border-custom-border-400": isBlockFocused,
|
||||
})}
|
||||
>
|
||||
{isBlockVisibleOnChart
|
||||
? isHidden && (
|
||||
<button
|
||||
type="button"
|
||||
className="sticky z-[5] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH + 4}px`,
|
||||
}}
|
||||
onClick={handleScrollToBlock}
|
||||
>
|
||||
<ArrowRight
|
||||
className={cn("h-3.5 w-3.5", {
|
||||
"rotate-180": isBlockHiddenOnLeft,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
: enableAddBlock && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,124 +1,100 @@
|
||||
import { RefObject, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// constants
|
||||
import { BLOCK_HEIGHT } from "../constants";
|
||||
// components
|
||||
import { ChartAddBlock, ChartDraggable } from "../helpers";
|
||||
import { useGanttChart } from "../hooks";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
|
||||
import { ChartDraggable } from "../helpers";
|
||||
import { useGanttResizable } from "../helpers/blockResizables/use-gantt-resizable";
|
||||
import { IBlockUpdateDependencyData } from "../types";
|
||||
|
||||
type Props = {
|
||||
blockId: string;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
showAllBlocks: boolean;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
enableBlockLeftResize: boolean;
|
||||
enableBlockRightResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
enableAddBlock: boolean;
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blockId,
|
||||
getBlockById,
|
||||
showAllBlocks,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
enableAddBlock,
|
||||
ganttContainerRef,
|
||||
selectionHelpers,
|
||||
updateBlockDates,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { currentViewData, updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
const { getIsIssuePeeked } = useIssueDetail();
|
||||
const { updateActiveBlockId, getBlockById, getIsCurrentDependencyDragging, currentView } = useTimeLineChartStore();
|
||||
// refs
|
||||
const resizableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const block = getBlockById(blockId, currentViewData);
|
||||
const block = getBlockById(blockId);
|
||||
|
||||
const isCurrentDependencyDragging = getIsCurrentDependencyDragging(blockId);
|
||||
|
||||
const { isMoving, handleBlockDrag } = useGanttResizable(block, resizableRef, ganttContainerRef, updateBlockDates);
|
||||
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
|
||||
const handleChartBlockPosition = (
|
||||
block: IGanttBlock,
|
||||
totalBlockShifts: number,
|
||||
dragDirection: "left" | "right" | "move"
|
||||
) => {
|
||||
const originalStartDate = getDate(block.start_date);
|
||||
const originalTargetDate = getDate(block.target_date);
|
||||
|
||||
if (!originalStartDate || !originalTargetDate) return;
|
||||
|
||||
const updatedStartDate = new Date(originalStartDate);
|
||||
const updatedTargetDate = new Date(originalTargetDate);
|
||||
|
||||
// update the start date on left resize
|
||||
if (dragDirection === "left") updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts);
|
||||
// update the target date on right resize
|
||||
else if (dragDirection === "right") updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
|
||||
// update both the dates on x-axis move
|
||||
else if (dragDirection === "move") {
|
||||
updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts);
|
||||
updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
|
||||
}
|
||||
|
||||
// call the block update handler with the updated dates
|
||||
blockUpdateHandler(block.data, {
|
||||
start_date: renderFormattedPayloadDate(updatedStartDate) ?? undefined,
|
||||
target_date: renderFormattedPayloadDate(updatedTargetDate) ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
if (!block.data) return null;
|
||||
|
||||
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
|
||||
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
|
||||
const isBlockHoveredOn = isBlockActive(block.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative min-w-full w-max"
|
||||
className={cn("relative z-[5]", {
|
||||
"transition-all": !!isMoving && currentView === "week",
|
||||
"pointer-events-none": !isBlockVisibleOnChart,
|
||||
})}
|
||||
id={`gantt-block-${block.id}`}
|
||||
ref={resizableRef}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
transform: `translateX(${block.position?.marginLeft}px)`,
|
||||
width: `${block.position?.width}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn("relative h-full", {
|
||||
"rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
|
||||
"bg-custom-background-90": isBlockHoveredOn,
|
||||
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isBlockSelected,
|
||||
"bg-custom-primary-100/10": isBlockSelected && isBlockHoveredOn,
|
||||
"border border-r-0 border-custom-border-400": isBlockFocused,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(blockId)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
>
|
||||
{isBlockVisibleOnChart ? (
|
||||
<ChartDraggable
|
||||
block={block}
|
||||
blockToRender={blockToRender}
|
||||
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
) : (
|
||||
enableAddBlock && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
|
||||
)}
|
||||
</div>
|
||||
{isBlockVisibleOnChart && (
|
||||
<RenderIfVisible
|
||||
root={ganttContainerRef}
|
||||
horizontalOffset={100}
|
||||
verticalOffset={200}
|
||||
classNames="flex h-full w-full items-center"
|
||||
placeholderChildren={<div className="h-8 w-full bg-custom-background-80 rounded" />}
|
||||
shouldRecordHeights={false}
|
||||
forceRender={isCurrentDependencyDragging}
|
||||
>
|
||||
<div
|
||||
className={cn("relative h-full w-full")}
|
||||
onMouseEnter={() => updateActiveBlockId(blockId)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
>
|
||||
<ChartDraggable
|
||||
block={block}
|
||||
blockToRender={blockToRender}
|
||||
handleBlockDrag={handleBlockDrag}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
isMoving={isMoving}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
</div>
|
||||
</RenderIfVisible>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,60 +1,39 @@
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
// constants
|
||||
import { HEADER_HEIGHT } from "../constants";
|
||||
// types
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
|
||||
// components
|
||||
//
|
||||
import { IBlockUpdateDependencyData } from "../types";
|
||||
import { GanttChartBlock } from "./block";
|
||||
|
||||
export type GanttChartBlocksProps = {
|
||||
itemsContainerWidth: number;
|
||||
blockIds: string[];
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
|
||||
enableBlockRightResize: boolean | ((blockId: string) => boolean);
|
||||
enableBlockMove: boolean | ((blockId: string) => boolean);
|
||||
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
showAllBlocks: boolean;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||
const {
|
||||
itemsContainerWidth,
|
||||
blockIds,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
getBlockById,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
enableAddBlock,
|
||||
ganttContainerRef,
|
||||
showAllBlocks,
|
||||
selectionHelpers,
|
||||
updateBlockDates,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full"
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{blockIds?.map((blockId) => (
|
||||
<GanttChartBlock
|
||||
key={blockId}
|
||||
blockId={blockId}
|
||||
getBlockById={getBlockById}
|
||||
showAllBlocks={showAllBlocks}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={
|
||||
typeof enableBlockLeftResize === "function" ? enableBlockLeftResize(blockId) : enableBlockLeftResize
|
||||
}
|
||||
@@ -62,11 +41,10 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||
typeof enableBlockRightResize === "function" ? enableBlockRightResize(blockId) : enableBlockRightResize
|
||||
}
|
||||
enableBlockMove={typeof enableBlockMove === "function" ? enableBlockMove(blockId) : enableBlockMove}
|
||||
enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
selectionHelpers={selectionHelpers}
|
||||
updateBlockDates={updateBlockDates}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
// hooks
|
||||
// helpers
|
||||
// plane
|
||||
import { Row } from "@plane/ui";
|
||||
// components
|
||||
import { VIEWS_LIST } from "@/components/gantt-chart/data";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// types
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { GANTT_BREADCRUMBS_HEIGHT } from "../constants";
|
||||
import { TGanttViews } from "../types";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
blockIds: string[];
|
||||
@@ -24,10 +26,13 @@ export const GanttChartHeader: React.FC<Props> = observer((props) => {
|
||||
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } =
|
||||
props;
|
||||
// chart hook
|
||||
const { currentView } = useGanttChart();
|
||||
const { currentView } = useTimeLineChartStore();
|
||||
|
||||
return (
|
||||
<Row className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap py-2">
|
||||
<Row
|
||||
className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap py-2"
|
||||
style={{ height: `${GANTT_BREADCRUMBS_HEIGHT}px` }}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto text-sm font-medium">
|
||||
{blockIds ? `${blockIds.length} ${loaderTitle}` : "Loading..."}
|
||||
|
||||
@@ -5,36 +5,34 @@ import { observer } from "mobx-react";
|
||||
// components
|
||||
import { MultipleSelectGroup } from "@/components/core";
|
||||
import {
|
||||
BiWeekChartView,
|
||||
ChartDataType,
|
||||
DayChartView,
|
||||
GanttChartBlocksList,
|
||||
GanttChartSidebar,
|
||||
HourChartView,
|
||||
IBlockUpdateData,
|
||||
IGanttBlock,
|
||||
IBlockUpdateDependencyData,
|
||||
MonthChartView,
|
||||
QuarterChartView,
|
||||
TGanttViews,
|
||||
WeekChartView,
|
||||
YearChartView,
|
||||
} from "@/components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// plane web components
|
||||
import { TimelineDependencyPaths, TimelineDraggablePath } from "@/plane-web/components/gantt-chart";
|
||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues";
|
||||
// plane web hooks
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
// constants
|
||||
import { GANTT_SELECT_GROUP } from "../constants";
|
||||
// hooks
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
//
|
||||
import { GanttChartRowList } from "../blocks/block-row-list";
|
||||
import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { TimelineDragHelper } from "./timeline-drag-helper";
|
||||
|
||||
type Props = {
|
||||
blockIds: string[];
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
bottomSpacing: boolean;
|
||||
@@ -55,7 +53,6 @@ type Props = {
|
||||
export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blockIds,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
@@ -73,11 +70,12 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
canLoadMoreBlocks,
|
||||
updateCurrentViewRenderPayload,
|
||||
quickAdd,
|
||||
updateBlockDates,
|
||||
} = props;
|
||||
// refs
|
||||
const ganttContainerRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
const { currentView, currentViewData } = useGanttChart();
|
||||
const { currentView, currentViewData } = useTimeLineChartStore();
|
||||
// plane web hooks
|
||||
const isBulkOperationsEnabled = useBulkOperationStatus();
|
||||
|
||||
@@ -91,15 +89,15 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
autoScrollForElements({
|
||||
element,
|
||||
getAllowedAxis: () => "vertical",
|
||||
canScroll: ({ source }) => source.data.dragInstanceId === "GANTT_REORDER",
|
||||
})
|
||||
);
|
||||
}, [ganttContainerRef?.current]);
|
||||
|
||||
// handling scroll functionality
|
||||
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
|
||||
|
||||
// updateScrollLeft(scrollLeft);
|
||||
|
||||
const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth;
|
||||
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
|
||||
|
||||
@@ -110,77 +108,89 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const CHART_VIEW_COMPONENTS: {
|
||||
[key in TGanttViews]: React.FC;
|
||||
} = {
|
||||
hours: HourChartView,
|
||||
day: DayChartView,
|
||||
week: WeekChartView,
|
||||
bi_week: BiWeekChartView,
|
||||
month: MonthChartView,
|
||||
quarter: QuarterChartView,
|
||||
year: YearChartView,
|
||||
};
|
||||
|
||||
if (!currentView) return null;
|
||||
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
|
||||
|
||||
return (
|
||||
<MultipleSelectGroup
|
||||
containerRef={ganttContainerRef}
|
||||
entities={{
|
||||
[GANTT_SELECT_GROUP]: blockIds ?? [],
|
||||
}}
|
||||
disabled={!isBulkOperationsEnabled}
|
||||
>
|
||||
{(helpers) => (
|
||||
<>
|
||||
<div
|
||||
// DO NOT REMOVE THE ID
|
||||
id="gantt-container"
|
||||
className={cn(
|
||||
"h-full w-full overflow-auto vertical-scrollbar horizontal-scrollbar scrollbar-lg flex border-t-[0.5px] border-custom-border-200",
|
||||
{
|
||||
"mb-8": bottomSpacing,
|
||||
}
|
||||
)}
|
||||
ref={ganttContainerRef}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<GanttChartSidebar
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
quickAdd={quickAdd}
|
||||
selectionHelpers={helpers}
|
||||
/>
|
||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||
<ActiveChartView />
|
||||
{currentViewData && (
|
||||
<GanttChartBlocksList
|
||||
itemsContainerWidth={itemsContainerWidth}
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableAddBlock={enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
showAllBlocks={showAllBlocks}
|
||||
selectionHelpers={helpers}
|
||||
/>
|
||||
<>
|
||||
<TimelineDragHelper ganttContainerRef={ganttContainerRef} />
|
||||
<MultipleSelectGroup
|
||||
containerRef={ganttContainerRef}
|
||||
entities={{
|
||||
[GANTT_SELECT_GROUP]: blockIds ?? [],
|
||||
}}
|
||||
disabled={!isBulkOperationsEnabled}
|
||||
>
|
||||
{(helpers) => (
|
||||
<>
|
||||
<div
|
||||
// DO NOT REMOVE THE ID
|
||||
id="gantt-container"
|
||||
className={cn(
|
||||
"h-full w-full overflow-auto vertical-scrollbar horizontal-scrollbar scrollbar-lg flex border-t-[0.5px] border-custom-border-200",
|
||||
{
|
||||
"mb-8": bottomSpacing,
|
||||
}
|
||||
)}
|
||||
ref={ganttContainerRef}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<GanttChartSidebar
|
||||
blockIds={blockIds}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
quickAdd={quickAdd}
|
||||
selectionHelpers={helpers}
|
||||
/>
|
||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||
<ActiveChartView />
|
||||
{currentViewData && (
|
||||
<div
|
||||
className="relative h-full"
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
}}
|
||||
>
|
||||
<GanttChartRowList
|
||||
blockIds={blockIds}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableAddBlock={enableAddBlock}
|
||||
showAllBlocks={showAllBlocks}
|
||||
selectionHelpers={helpers}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
<TimelineDependencyPaths />
|
||||
<TimelineDraggablePath />
|
||||
<GanttChartBlocksList
|
||||
blockIds={blockIds}
|
||||
blockToRender={blockToRender}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
showAllBlocks={showAllBlocks}
|
||||
updateBlockDates={updateBlockDates}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
)}
|
||||
</MultipleSelectGroup>
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
)}
|
||||
</MultipleSelectGroup>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
// components
|
||||
import { GanttChartHeader, GanttChartMainContent } from "@/components/gantt-chart";
|
||||
// views
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// types
|
||||
// data
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { SIDEBAR_WIDTH } from "../constants";
|
||||
import { currentViewDataWithView } from "../data";
|
||||
// constants
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
||||
import { generateMonthChart, getNumberOfDaysBetweenTwoDatesInMonth } from "../views";
|
||||
import { ChartDataType, IBlockUpdateData, IBlockUpdateDependencyData, TGanttViews } from "../types";
|
||||
import { getNumberOfDaysBetweenTwoDates, IMonthBlock, IMonthView, IWeekBlock, timelineViewHelpers } from "../views";
|
||||
|
||||
type ChartViewRootProps = {
|
||||
border: boolean;
|
||||
@@ -31,8 +28,8 @@ type ChartViewRootProps = {
|
||||
enableSelection: boolean | ((blockId: string) => boolean);
|
||||
bottomSpacing: boolean;
|
||||
showAllBlocks: boolean;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
loadMoreBlocks?: () => void;
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
showToday: boolean;
|
||||
@@ -43,7 +40,6 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
border,
|
||||
title,
|
||||
blockIds,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
loaderTitle,
|
||||
blockUpdateHandler,
|
||||
@@ -60,13 +56,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
showAllBlocks,
|
||||
quickAdd,
|
||||
showToday,
|
||||
updateBlockDates,
|
||||
} = props;
|
||||
// states
|
||||
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
|
||||
const [fullScreenMode, setFullScreenMode] = useState(false);
|
||||
// hooks
|
||||
const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } =
|
||||
useGanttChart();
|
||||
useTimeLineChartStore();
|
||||
|
||||
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
|
||||
const selectedCurrentView: TGanttViews = view;
|
||||
@@ -77,21 +74,25 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
|
||||
if (selectedCurrentViewData === undefined) return;
|
||||
|
||||
let currentRender: any;
|
||||
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
|
||||
const currentViewHelpers = timelineViewHelpers[selectedCurrentView];
|
||||
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side);
|
||||
const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as (
|
||||
a: IWeekBlock[] | IMonthView | IMonthBlock[],
|
||||
b: IWeekBlock[] | IMonthView | IMonthBlock[]
|
||||
) => IWeekBlock[] | IMonthView | IMonthBlock[];
|
||||
|
||||
// updating the prevData, currentData and nextData
|
||||
if (currentRender.payload.length > 0) {
|
||||
if (currentRender.payload) {
|
||||
updateCurrentViewData(currentRender.state);
|
||||
|
||||
if (side === "left") {
|
||||
updateCurrentView(selectedCurrentView);
|
||||
updateRenderView([...currentRender.payload, ...renderView]);
|
||||
updateRenderView(mergeRenderPayloads(currentRender.payload, renderView));
|
||||
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
|
||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||
} else if (side === "right") {
|
||||
updateCurrentView(view);
|
||||
updateRenderView([...renderView, ...currentRender.payload]);
|
||||
updateRenderView(mergeRenderPayloads(renderView, currentRender.payload));
|
||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||
} else {
|
||||
updateCurrentView(view);
|
||||
@@ -127,24 +128,12 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
||||
let scrollWidth: number = 0;
|
||||
let daysDifference: number = 0;
|
||||
|
||||
// if (currentView === "hours")
|
||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||
// if (currentView === "day")
|
||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||
// if (currentView === "week")
|
||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||
// if (currentView === "bi_week")
|
||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||
if (currentView === "month")
|
||||
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
|
||||
// if (currentView === "quarter")
|
||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
|
||||
// if (currentView === "year")
|
||||
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
|
||||
daysDifference = getNumberOfDaysBetweenTwoDates(currentState.data.startDate, date);
|
||||
|
||||
scrollWidth =
|
||||
daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width) + SIDEBAR_WIDTH / 2;
|
||||
Math.abs(daysDifference) * currentState.data.dayWidth -
|
||||
(clientVisibleWidth / 2 - currentState.data.dayWidth) +
|
||||
SIDEBAR_WIDTH / 2;
|
||||
|
||||
scrollContainer.scrollLeft = scrollWidth;
|
||||
};
|
||||
@@ -167,7 +156,6 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
/>
|
||||
<GanttChartMainContent
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
blockToRender={blockToRender}
|
||||
@@ -185,6 +173,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
title={title}
|
||||
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
|
||||
quickAdd={quickAdd}
|
||||
updateBlockDates={updateBlockDates}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { RefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useAutoScroller } from "@/hooks/use-auto-scroller";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
||||
|
||||
type Props = {
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
export const TimelineDragHelper = observer((props: Props) => {
|
||||
const { ganttContainerRef } = props;
|
||||
const { isDragging } = useTimeLineChartStore();
|
||||
|
||||
useAutoScroller(ganttContainerRef, isDragging, SIDEBAR_WIDTH, HEADER_HEIGHT);
|
||||
return <></>;
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks/use-gantt-chart";
|
||||
|
||||
export const BiWeekChartView: FC<any> = observer(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
<div
|
||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm font-medium capitalize ${
|
||||
_item?.today ? `border-red-500 text-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`relative flex h-full w-full flex-1 justify-center ${
|
||||
["sat", "sun"].includes(_item?.dayData?.shortTitle || "") ? `bg-custom-background-80` : ``
|
||||
}`}
|
||||
>
|
||||
{_item?.today && <div className="absolute bottom-0 top-0 border border-red-500"> </div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks/use-gantt-chart";
|
||||
|
||||
export const DayChartView: FC<any> = observer(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
<div
|
||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm font-medium capitalize ${
|
||||
_item?.today ? `border-red-500 text-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`relative flex h-full w-full flex-1 justify-center ${
|
||||
["sat", "sun"].includes(_item?.dayData?.shortTitle || "") ? `bg-gray-100` : ``
|
||||
}`}
|
||||
>
|
||||
{_item?.today && <div className="absolute bottom-0 top-0 border border-red-500"> </div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks/use-gantt-chart";
|
||||
|
||||
export const HourChartView: FC<any> = observer(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
<div
|
||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm font-medium capitalize ${
|
||||
_item?.today ? `border-red-500 text-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`relative flex h-full w-full flex-1 justify-center ${
|
||||
["sat", "sun"].includes(_item?.dayData?.shortTitle || "") ? `bg-gray-100` : ``
|
||||
}`}
|
||||
>
|
||||
{_item?.today && <div className="absolute bottom-0 top-0 border border-red-500"> </div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,7 +1,3 @@
|
||||
export * from "./bi-week";
|
||||
export * from "./day";
|
||||
export * from "./hours";
|
||||
export * from "./month";
|
||||
export * from "./quarter";
|
||||
export * from "./week";
|
||||
export * from "./year";
|
||||
|
||||
@@ -1,75 +1,105 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
// components
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks/use-gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// types
|
||||
import { IMonthBlock } from "../../views";
|
||||
// constants
|
||||
import { IMonthView } from "../../views";
|
||||
import { getNumberOfDaysBetweenTwoDates } from "../../views/helpers";
|
||||
|
||||
export const MonthChartView: FC<any> = observer(() => {
|
||||
// chart hook
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
const monthBlocks: IMonthBlock[] = renderView;
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const monthView: IMonthView = renderView;
|
||||
|
||||
if (!monthView) return <></>;
|
||||
|
||||
const { months, weeks } = monthView;
|
||||
|
||||
const monthsStartDate = new Date(months[0].year, months[0].month, 1);
|
||||
const weeksStartDate = weeks[0].startDate;
|
||||
|
||||
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 min-h-full h-max w-max flex divide-x divide-custom-border-100/50">
|
||||
{monthBlocks?.map((block, rootIndex) => (
|
||||
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
{currentViewData && (
|
||||
<div className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200">
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<div className="h-1/2">
|
||||
<div
|
||||
className="sticky inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{block?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-1/2 w-full flex">
|
||||
{block?.children?.map((monthDay, index) => (
|
||||
{/** Main Month Title */}
|
||||
<div className="flex h-7" style={{ marginLeft: `${marginLeftDays * currentViewData.data.dayWidth}px` }}>
|
||||
{months?.map((monthBlock) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
className="flex-shrink-0 border-b-[0.5px] border-custom-border-200 py-1 text-center capitalize"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
key={`month-${monthBlock?.month}-${monthBlock?.year}`}
|
||||
className="flex outline-[0.5px] outline outline-custom-border-200"
|
||||
style={{ width: `${monthBlock.days * currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-xs">
|
||||
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
|
||||
<span
|
||||
className={cn({
|
||||
"rounded-full bg-custom-primary-100 px-1 text-white": monthDay.today,
|
||||
})}
|
||||
>
|
||||
{monthDay.day}
|
||||
</span>
|
||||
<div
|
||||
className="sticky flex items-center font-normal z-[1] m-1 whitespace-nowrap px-3 py-1 text-base capitalize bg-custom-background-100 text-custom-text-200"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{monthBlock?.title}
|
||||
{monthBlock.today && (
|
||||
<span className={cn("rounded ml-2 font-medium bg-custom-primary-100 px-1 text-2xs text-white")}>
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/** Weeks Sub title */}
|
||||
<div className="h-5 w-full flex">
|
||||
{weeks?.map((weekBlock) => (
|
||||
<div
|
||||
key={`sub-title-${weekBlock.startDate}-${weekBlock.endDate}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 py-1 px-2 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200",
|
||||
{
|
||||
"bg-custom-primary-10": weekBlock.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-xs font-medium text-custom-text-400">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded bg-custom-primary-100 px-1 text-white": weekBlock.today,
|
||||
})}
|
||||
>
|
||||
{weekBlock.startDate.getDate()}-{weekBlock.endDate.getDate()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-x-1 text-xs font-medium">{weekBlock.weekData.shortTitle}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full flex-grow flex divide-x divide-custom-border-100/50">
|
||||
{block?.children?.map((monthDay, index) => (
|
||||
{/** Week Columns */}
|
||||
<div className="h-full w-full flex-grow flex">
|
||||
{weeks?.map((weekBlock) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className="h-full overflow-hidden"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
{["sat", "sun"].includes(monthDay?.dayData?.shortTitle) && (
|
||||
<div className="h-full bg-custom-background-90" />
|
||||
)}
|
||||
</div>
|
||||
key={`column-${weekBlock.startDate}-${weekBlock.endDate}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
|
||||
"bg-custom-primary-10": weekBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,50 +1,93 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// Plane
|
||||
import { cn } from "@plane/editor";
|
||||
// hooks
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks/use-gantt-chart";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../../constants";
|
||||
import { groupMonthsToQuarters, IMonthBlock, IQuarterMonthBlock } from "../../views";
|
||||
|
||||
export const QuarterChartView: FC<any> = observer(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const monthBlocks: IMonthBlock[] = renderView;
|
||||
|
||||
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
{currentViewData &&
|
||||
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
||||
<div
|
||||
key={`month-${quarterBlock.quarterNumber}-${quarterBlock.year}`}
|
||||
className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200"
|
||||
>
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0 outline-[1px] outline outline-custom-border-200"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{/** Main Quarter Title */}
|
||||
<div className="w-full inline-flex h-7 justify-between">
|
||||
<div
|
||||
className="sticky flex items-center font-normal z-[1] my-1 whitespace-nowrap px-3 py-1 text-base capitalize bg-custom-background-100 text-custom-text-200"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{quarterBlock?.title}
|
||||
{quarterBlock.today && (
|
||||
<span className={cn("rounded ml-2 font-medium bg-custom-primary-100 px-1 text-2xs text-white")}>
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="sticky whitespace-nowrap px-3 py-2 text-xs capitalize text-custom-text-400">
|
||||
{quarterBlock.shortTitle}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
<div
|
||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm font-medium capitalize ${
|
||||
_item?.today ? `border-red-500 text-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
{/** Months Sub title */}
|
||||
<div className="h-5 w-full flex">
|
||||
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 text-center capitalize justify-center outline-[0.25px] outline outline-custom-border-200",
|
||||
{
|
||||
"bg-custom-primary-10": monthBlock.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||
>
|
||||
<div className="space-x-1 flex items-center justify-center text-xs font-medium h-full">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded-lg bg-custom-primary-100 px-2 text-white": monthBlock.today,
|
||||
})}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
</div>
|
||||
<div className={`relative flex h-full w-full flex-1 justify-center`}>
|
||||
{_item?.today && <div className="absolute bottom-0 top-0 border border-red-500"> </div>}
|
||||
</div>
|
||||
{monthBlock.monthData.shortTitle}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
{/** Month Columns */}
|
||||
<div className="h-full w-full flex-grow flex">
|
||||
{quarterBlock?.children?.map((monthBlock, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
|
||||
"bg-custom-primary-10": monthBlock.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,54 +1,93 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// Plane
|
||||
import { cn } from "@plane/editor";
|
||||
// hooks
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks/use-gantt-chart";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../../constants";
|
||||
import { IWeekBlock } from "../../views";
|
||||
|
||||
export const WeekChartView: FC<any> = observer(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
const { currentViewData, renderView } = useTimeLineChartStore();
|
||||
const weekBlocks: IWeekBlock[] = renderView;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
{currentViewData &&
|
||||
weekBlocks?.map((block, rootIndex) => (
|
||||
<div
|
||||
key={`month-${block?.startDate}-${block?.endDate}`}
|
||||
className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200"
|
||||
>
|
||||
{/** Header Div */}
|
||||
<div
|
||||
className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0 outline-[1px] outline outline-custom-border-200"
|
||||
style={{
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
{/** Main Months Title */}
|
||||
<div className="w-full inline-flex h-7 justify-between">
|
||||
<div
|
||||
className="sticky flex items-center font-normal z-[1] m-1 whitespace-nowrap px-3 py-1 text-sm capitalize bg-custom-background-100 text-custom-text-200"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH}px`,
|
||||
}}
|
||||
>
|
||||
{block?.title}
|
||||
</div>
|
||||
<div className="sticky whitespace-nowrap px-3 py-2 text-xs capitalize text-custom-text-400">
|
||||
{block?.weekData?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
<div
|
||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm font-medium capitalize ${
|
||||
_item?.today ? `border-red-500 text-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`relative flex h-full w-full flex-1 justify-center ${
|
||||
["sat", "sun"].includes(_item?.dayData?.shortTitle || "") ? `bg-custom-background-80` : ``
|
||||
}`}
|
||||
>
|
||||
{_item?.today && <div className="absolute bottom-0 top-0 border border-red-500"> </div>}
|
||||
</div>
|
||||
{/** Days Sub title */}
|
||||
<div className="h-5 w-full flex">
|
||||
{block?.children?.map((weekDay, index) => (
|
||||
<div
|
||||
key={`sub-title-${rootIndex}-${index}`}
|
||||
className={cn(
|
||||
"flex flex-shrink-0 p-1 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200",
|
||||
{
|
||||
"bg-custom-primary-10": weekDay.today,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
<div className="space-x-1 text-xs font-medium text-custom-text-400">
|
||||
{weekDay.dayData.abbreviation}
|
||||
</div>
|
||||
))}
|
||||
<div className="space-x-1 text-xs font-medium">
|
||||
<span
|
||||
className={cn({
|
||||
"rounded bg-custom-primary-100 px-1 text-white": weekDay.today,
|
||||
})}
|
||||
>
|
||||
{weekDay.date.getDate()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
{/** Day Columns */}
|
||||
<div className="h-full w-full flex-grow flex">
|
||||
{block?.children?.map((weekDay, index) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
|
||||
"bg-custom-primary-10": weekDay.today,
|
||||
})}
|
||||
style={{ width: `${currentViewData?.data.dayWidth}px` }}
|
||||
>
|
||||
{["sat", "sun"].includes(weekDay?.dayData?.shortTitle) && (
|
||||
<div className="h-full bg-custom-background-90 outline-[0.25px] outline outline-custom-border-300" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks/use-gantt-chart";
|
||||
|
||||
export const YearChartView: FC<any> = observer(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { currentViewData, renderView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
|
||||
{renderView &&
|
||||
renderView.length > 0 &&
|
||||
renderView.map((_itemRoot: any, _idxRoot: any) => (
|
||||
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
|
||||
<div className="relative border-b border-custom-border-200">
|
||||
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
|
||||
{_itemRoot?.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full divide-x divide-custom-border-200">
|
||||
{_itemRoot.children &&
|
||||
_itemRoot.children.length > 0 &&
|
||||
_itemRoot.children.map((_item: any, _idx: any) => (
|
||||
<div
|
||||
key={`sub-title-${_idxRoot}-${_idx}`}
|
||||
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
|
||||
style={{ width: `${currentViewData?.data.width}px` }}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 border-b py-1 text-center text-sm font-medium capitalize ${
|
||||
_item?.today ? `border-red-500 text-red-500` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
<div>{_item.title}</div>
|
||||
</div>
|
||||
<div className={`relative flex h-full w-full flex-1 justify-center`}>
|
||||
{_item?.today && <div className="absolute bottom-0 top-0 border border-red-500"> </div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
export const BLOCK_HEIGHT = 44;
|
||||
|
||||
export const HEADER_HEIGHT = 60;
|
||||
export const HEADER_HEIGHT = 48;
|
||||
|
||||
export const GANTT_BREADCRUMBS_HEIGHT = 40;
|
||||
|
||||
export const SIDEBAR_WIDTH = 360;
|
||||
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import React, { FC, createContext } from "react";
|
||||
// mobx store
|
||||
import { GanttStore } from "@/store/issue/issue_gantt_view.store";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
let ganttViewStore = new GanttStore();
|
||||
export enum ETimeLineTypeType {
|
||||
ISSUE = "ISSUE",
|
||||
MODULE = "MODULE",
|
||||
}
|
||||
|
||||
export const GanttStoreContext = createContext<GanttStore>(ganttViewStore);
|
||||
export const TimeLineTypeContext = createContext<ETimeLineTypeType | undefined>(undefined);
|
||||
|
||||
const initializeStore = () => {
|
||||
const newGanttViewStore = ganttViewStore ?? new GanttStore();
|
||||
if (typeof window === "undefined") return newGanttViewStore;
|
||||
if (!ganttViewStore) ganttViewStore = newGanttViewStore;
|
||||
return newGanttViewStore;
|
||||
};
|
||||
|
||||
type GanttStoreProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const GanttStoreProvider: FC<GanttStoreProviderProps> = ({ children }) => {
|
||||
const store = initializeStore();
|
||||
return <GanttStoreContext.Provider value={store}>{children}</GanttStoreContext.Provider>;
|
||||
export const useTimeLineType = () => {
|
||||
const timelineType = useContext(TimeLineTypeContext);
|
||||
|
||||
return timelineType;
|
||||
};
|
||||
|
||||
@@ -3,28 +3,35 @@ import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types";
|
||||
|
||||
// constants
|
||||
export const weeks: WeekMonthDataType[] = [
|
||||
{ key: 0, shortTitle: "sun", title: "sunday" },
|
||||
{ key: 1, shortTitle: "mon", title: "monday" },
|
||||
{ key: 2, shortTitle: "tue", title: "tuesday" },
|
||||
{ key: 3, shortTitle: "wed", title: "wednesday" },
|
||||
{ key: 4, shortTitle: "thurs", title: "thursday" },
|
||||
{ key: 5, shortTitle: "fri", title: "friday" },
|
||||
{ key: 6, shortTitle: "sat", title: "saturday" },
|
||||
{ key: 0, shortTitle: "sun", title: "sunday", abbreviation: "Su" },
|
||||
{ key: 1, shortTitle: "mon", title: "monday", abbreviation: "M" },
|
||||
{ key: 2, shortTitle: "tue", title: "tuesday", abbreviation: "T" },
|
||||
{ key: 3, shortTitle: "wed", title: "wednesday", abbreviation: "W" },
|
||||
{ key: 4, shortTitle: "thurs", title: "thursday", abbreviation: "Th" },
|
||||
{ key: 5, shortTitle: "fri", title: "friday", abbreviation: "F" },
|
||||
{ key: 6, shortTitle: "sat", title: "saturday", abbreviation: "Sa" },
|
||||
];
|
||||
|
||||
export const months: WeekMonthDataType[] = [
|
||||
{ key: 0, shortTitle: "jan", title: "january" },
|
||||
{ key: 1, shortTitle: "feb", title: "february" },
|
||||
{ key: 2, shortTitle: "mar", title: "march" },
|
||||
{ key: 3, shortTitle: "apr", title: "april" },
|
||||
{ key: 4, shortTitle: "may", title: "may" },
|
||||
{ key: 5, shortTitle: "jun", title: "june" },
|
||||
{ key: 6, shortTitle: "jul", title: "july" },
|
||||
{ key: 7, shortTitle: "aug", title: "august" },
|
||||
{ key: 8, shortTitle: "sept", title: "september" },
|
||||
{ key: 9, shortTitle: "oct", title: "october" },
|
||||
{ key: 10, shortTitle: "nov", title: "november" },
|
||||
{ key: 11, shortTitle: "dec", title: "december" },
|
||||
{ key: 0, shortTitle: "jan", title: "january", abbreviation: "Jan" },
|
||||
{ key: 1, shortTitle: "feb", title: "february", abbreviation: "Feb" },
|
||||
{ key: 2, shortTitle: "mar", title: "march", abbreviation: "Mar" },
|
||||
{ key: 3, shortTitle: "apr", title: "april", abbreviation: "Apr" },
|
||||
{ key: 4, shortTitle: "may", title: "may", abbreviation: "May" },
|
||||
{ key: 5, shortTitle: "jun", title: "june", abbreviation: "Jun" },
|
||||
{ key: 6, shortTitle: "jul", title: "july", abbreviation: "Jul" },
|
||||
{ key: 7, shortTitle: "aug", title: "august", abbreviation: "Aug" },
|
||||
{ key: 8, shortTitle: "sept", title: "september", abbreviation: "Sept" },
|
||||
{ key: 9, shortTitle: "oct", title: "october", abbreviation: "Oct" },
|
||||
{ key: 10, shortTitle: "nov", title: "november", abbreviation: "Nov" },
|
||||
{ key: 11, shortTitle: "dec", title: "december", abbreviation: "Dec" },
|
||||
];
|
||||
|
||||
export const quarters: WeekMonthDataType[] = [
|
||||
{ key: 0, shortTitle: "Q1", title: "Jan - Mar", abbreviation: "Q1" },
|
||||
{ key: 1, shortTitle: "Q2", title: "Apr - Jun", abbreviation: "Q2" },
|
||||
{ key: 2, shortTitle: "Q3", title: "Jul - Sept", abbreviation: "Q3" },
|
||||
{ key: 3, shortTitle: "Q4", title: "Oct - Dec", abbreviation: "Q4" },
|
||||
];
|
||||
|
||||
export const charCapitalize = (word: string) => `${word.charAt(0).toUpperCase()}${word.substring(1)}`;
|
||||
@@ -54,50 +61,17 @@ export const datePreview = (date: Date, includeTime: boolean = false) => {
|
||||
|
||||
// context data
|
||||
export const VIEWS_LIST: ChartDataType[] = [
|
||||
// {
|
||||
// key: "hours",
|
||||
// title: "Hours",
|
||||
// data: {
|
||||
// startDate: new Date(),
|
||||
// currentDate: new Date(),
|
||||
// endDate: new Date(),
|
||||
// approxFilterRange: 4,
|
||||
// width: 40,
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: "days",
|
||||
// title: "Days",
|
||||
// data: {
|
||||
// startDate: new Date(),
|
||||
// currentDate: new Date(),
|
||||
// endDate: new Date(),
|
||||
// approxFilterRange: 4,
|
||||
// width: 40,
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: "week",
|
||||
// title: "Week",
|
||||
// data: {
|
||||
// startDate: new Date(),
|
||||
// currentDate: new Date(),
|
||||
// endDate: new Date(),
|
||||
// approxFilterRange: 4,
|
||||
// width: 180, // it will preview week dates with weekends highlighted with 1 week limitations ex: title (Wed 1, Thu 2, Fri 3)
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: "bi_week",
|
||||
// title: "Bi-Week",
|
||||
// data: {
|
||||
// startDate: new Date(),
|
||||
// currentDate: new Date(),
|
||||
// endDate: new Date(),
|
||||
// approxFilterRange: 4,
|
||||
// width: 100, // it will preview monthly all dates with weekends highlighted with 3 week limitations ex: title (Wed 1, Thu 2, Fri 3)
|
||||
// },
|
||||
// },
|
||||
{
|
||||
key: "week",
|
||||
title: "Week",
|
||||
data: {
|
||||
startDate: new Date(),
|
||||
currentDate: new Date(),
|
||||
endDate: new Date(),
|
||||
approxFilterRange: 4, // it will preview week dates with weekends highlighted with 1 week limitations ex: title (Wed 1, Thu 2, Fri 3)
|
||||
dayWidth: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "month",
|
||||
title: "Month",
|
||||
@@ -105,32 +79,21 @@ export const VIEWS_LIST: ChartDataType[] = [
|
||||
startDate: new Date(),
|
||||
currentDate: new Date(),
|
||||
endDate: new Date(),
|
||||
approxFilterRange: 6,
|
||||
width: 55, // it will preview monthly all dates with weekends highlighted with no limitations ex: title (1, 2, 3)
|
||||
approxFilterRange: 6, // it will preview monthly all dates with weekends highlighted with no limitations ex: title (1, 2, 3)
|
||||
dayWidth: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "quarter",
|
||||
title: "Quarter",
|
||||
data: {
|
||||
startDate: new Date(),
|
||||
currentDate: new Date(),
|
||||
endDate: new Date(),
|
||||
approxFilterRange: 18, // it will preview week starting dates all months data and there is 3 months limitation for preview ex: title (2, 9, 16, 23, 30)
|
||||
dayWidth: 5,
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: "quarter",
|
||||
// title: "Quarter",
|
||||
// data: {
|
||||
// startDate: new Date(),
|
||||
// currentDate: new Date(),
|
||||
// endDate: new Date(),
|
||||
// approxFilterRange: 12,
|
||||
// width: 100, // it will preview week starting dates all months data and there is 3 months limitation for preview ex: title (2, 9, 16, 23, 30)
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: "year",
|
||||
// title: "Year",
|
||||
// data: {
|
||||
// startDate: new Date(),
|
||||
// currentDate: new Date(),
|
||||
// endDate: new Date(),
|
||||
// approxFilterRange: 10,
|
||||
// width: 80, // it will preview week starting dates all months data and there is no limitation for preview ex: title (2, 9, 16, 23, 30)
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
export const currentViewDataWithView = (view: TGanttViews = "month") =>
|
||||
|
||||
@@ -8,11 +8,11 @@ import { Plus } from "lucide-react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// types
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
@@ -30,16 +30,20 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
// chart hook
|
||||
const { currentViewData } = useGanttChart();
|
||||
const { currentViewData, currentView } = useTimeLineChartStore();
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (!currentViewData) return;
|
||||
|
||||
const { startDate: chartStartDate, width } = currentViewData.data;
|
||||
const columnNumber = buttonXPosition / width;
|
||||
const { startDate: chartStartDate, dayWidth } = currentViewData.data;
|
||||
const columnNumber = buttonXPosition / dayWidth;
|
||||
|
||||
let numberOfDays = 1;
|
||||
|
||||
if (currentView === "quarter") numberOfDays = 7;
|
||||
|
||||
const startDate = addDays(chartStartDate, columnNumber);
|
||||
const endDate = addDays(startDate, 1);
|
||||
const endDate = addDays(startDate, numberOfDays);
|
||||
|
||||
blockUpdateHandler(block.data, {
|
||||
start_date: renderFormattedPayloadDate(startDate) ?? undefined,
|
||||
@@ -57,8 +61,8 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
|
||||
|
||||
setButtonXPosition(e.offsetX);
|
||||
|
||||
const { startDate: chartStartDate, width } = currentViewData.data;
|
||||
const columnNumber = buttonXPosition / width;
|
||||
const { startDate: chartStartDate, dayWidth } = currentViewData.data;
|
||||
const columnNumber = buttonXPosition / dayWidth;
|
||||
|
||||
const startDate = addDays(chartStartDate, columnNumber);
|
||||
setButtonStartDate(startDate);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// Plane
|
||||
import { cn } from "@plane/editor";
|
||||
//helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
//hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
|
||||
type LeftResizableProps = {
|
||||
enableBlockLeftResize: boolean;
|
||||
handleBlockDrag: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, dragDirection: "left" | "right" | "move") => void;
|
||||
isMoving: "left" | "right" | "move" | undefined;
|
||||
position?: {
|
||||
marginLeft: number;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
export const LeftResizable = observer((props: LeftResizableProps) => {
|
||||
const { enableBlockLeftResize, isMoving, handleBlockDrag, position } = props;
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
const { getDateFromPositionOnGantt } = useTimeLineChartStore();
|
||||
|
||||
const date = position ? getDateFromPositionOnGantt(position.marginLeft, 0) : undefined;
|
||||
const dateString = date ? renderFormattedDate(date) : undefined;
|
||||
|
||||
const isLeftResizing = isMoving === "left" || isMoving === "move";
|
||||
|
||||
if (!enableBlockLeftResize) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isHovering || isLeftResizing) && dateString && (
|
||||
<div className="absolute flex text-xs font-normal text-custom-text-300 h-full w-32 -left-36 justify-end items-center">
|
||||
<div className="px-2 py-1 bg-custom-primary-20 rounded">{dateString}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
onMouseDown={(e) => {
|
||||
handleBlockDrag(e, "left");
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
setIsHovering(true);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setIsHovering(false);
|
||||
}}
|
||||
className="absolute -left-1.5 top-1/2 -translate-y-1/2 z-[6] h-full w-3 cursor-col-resize rounded-md"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-1 top-1/2 -translate-y-1/2 h-7 w-1 z-[5] rounded-sm bg-custom-background-100 transition-all duration-300",
|
||||
{
|
||||
"-left-1.5": isLeftResizing,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// Plane
|
||||
import { cn } from "@plane/editor";
|
||||
//helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
//hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
|
||||
type RightResizableProps = {
|
||||
enableBlockRightResize: boolean;
|
||||
handleBlockDrag: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, dragDirection: "left" | "right" | "move") => void;
|
||||
isMoving: "left" | "right" | "move" | undefined;
|
||||
position?: {
|
||||
marginLeft: number;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
export const RightResizable = observer((props: RightResizableProps) => {
|
||||
const { enableBlockRightResize, handleBlockDrag, isMoving, position } = props;
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
const { getDateFromPositionOnGantt } = useTimeLineChartStore();
|
||||
|
||||
const date = position ? getDateFromPositionOnGantt(position.marginLeft + position.width, -1) : undefined;
|
||||
const dateString = date ? renderFormattedDate(date) : undefined;
|
||||
|
||||
const isRightResizing = isMoving === "right" || isMoving === "move";
|
||||
|
||||
if (!enableBlockRightResize) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isHovering || isRightResizing) && dateString && (
|
||||
<div className="z-[10] absolute flex text-xs font-normal text-custom-text-300 h-full w-32 -right-36 justify-start items-center">
|
||||
<div className="px-2 py-1 bg-custom-primary-20 rounded">{dateString}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
onMouseDown={(e) => handleBlockDrag(e, "right")}
|
||||
onMouseOver={() => {
|
||||
setIsHovering(true);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setIsHovering(false);
|
||||
}}
|
||||
className="absolute -right-1.5 top-1/2 -translate-y-1/2 z-[6] h-full w-3 cursor-col-resize rounded-md"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-1 z-[5] rounded-sm bg-custom-background-100 transition-all duration-300",
|
||||
{
|
||||
"-right-1.5": isRightResizing,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useRef, useState } from "react";
|
||||
// Plane
|
||||
import { setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
import { SIDEBAR_WIDTH } from "../../constants";
|
||||
import { IBlockUpdateDependencyData, IGanttBlock } from "../../types";
|
||||
|
||||
export const useGanttResizable = (
|
||||
block: IGanttBlock,
|
||||
resizableRef: React.RefObject<HTMLDivElement>,
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>,
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>
|
||||
) => {
|
||||
// refs
|
||||
const initialPositionRef = useRef<{ marginLeft: number; width: number; offsetX: number }>({
|
||||
marginLeft: 0,
|
||||
width: 0,
|
||||
offsetX: 0,
|
||||
});
|
||||
const ganttContainerDimensions = useRef<DOMRect | undefined>();
|
||||
const currMouseEvent = useRef<MouseEvent | undefined>();
|
||||
// states
|
||||
const { currentViewData, updateBlockPosition, setIsDragging, getUpdatedPositionAfterDrag } = useTimeLineChartStore();
|
||||
const [isMoving, setIsMoving] = useState<"left" | "right" | "move" | undefined>();
|
||||
|
||||
// handle block resize from the left end
|
||||
const handleBlockDrag = (
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
dragDirection: "left" | "right" | "move"
|
||||
) => {
|
||||
const ganttContainerElement = ganttContainerRef.current;
|
||||
if (!currentViewData || !resizableRef.current || !block.position || !ganttContainerElement) return;
|
||||
|
||||
if (e.button !== 0) return;
|
||||
|
||||
const resizableDiv = resizableRef.current;
|
||||
|
||||
ganttContainerDimensions.current = ganttContainerElement.getBoundingClientRect();
|
||||
|
||||
const dayWidth = currentViewData.data.dayWidth;
|
||||
const mouseX = e.clientX - ganttContainerDimensions.current.left - SIDEBAR_WIDTH + ganttContainerElement.scrollLeft;
|
||||
|
||||
// record position on drag start
|
||||
initialPositionRef.current = {
|
||||
width: block.position.width ?? 0,
|
||||
marginLeft: block.position.marginLeft ?? 0,
|
||||
offsetX: mouseX - block.position.marginLeft,
|
||||
};
|
||||
|
||||
const handleOnScroll = () => {
|
||||
if (currMouseEvent.current) handleMouseMove(currMouseEvent.current);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
currMouseEvent.current = e;
|
||||
setIsMoving(dragDirection);
|
||||
setIsDragging(true);
|
||||
|
||||
if (!ganttContainerDimensions.current) return;
|
||||
|
||||
const { left: containerLeft } = ganttContainerDimensions.current;
|
||||
|
||||
const mouseX = e.clientX - containerLeft - SIDEBAR_WIDTH + ganttContainerElement.scrollLeft;
|
||||
|
||||
let width = initialPositionRef.current.width;
|
||||
let marginLeft = initialPositionRef.current.marginLeft;
|
||||
const blockRight = initialPositionRef.current.width + initialPositionRef.current.marginLeft;
|
||||
|
||||
if (dragDirection === "left") {
|
||||
// calculate new marginLeft and update the initial marginLeft to the newly calculated one
|
||||
marginLeft = Math.round(mouseX / dayWidth) * dayWidth;
|
||||
// calculate new width
|
||||
width = blockRight - marginLeft;
|
||||
} else if (dragDirection === "right") {
|
||||
// calculate new width and update the initialMarginLeft using +=
|
||||
width = Math.round(mouseX / dayWidth) * dayWidth - marginLeft;
|
||||
} else if (dragDirection === "move") {
|
||||
// calculate new marginLeft and update the initial marginLeft using -=
|
||||
marginLeft = Math.round((mouseX - initialPositionRef.current.offsetX) / dayWidth) * dayWidth;
|
||||
}
|
||||
|
||||
// block needs to be at least 1 dayWidth Wide
|
||||
if (width < dayWidth) return;
|
||||
|
||||
resizableDiv.style.width = `${width}px`;
|
||||
resizableDiv.style.transform = `translateX(${marginLeft}px)`;
|
||||
|
||||
const deltaLeft = Math.round((marginLeft - (block.position?.marginLeft ?? 0)) / dayWidth) * dayWidth;
|
||||
const deltaWidth = Math.round((width - (block.position?.width ?? 0)) / dayWidth) * dayWidth;
|
||||
|
||||
// call update blockPosition
|
||||
if (deltaWidth || deltaLeft) updateBlockPosition(block.id, deltaLeft, deltaWidth, dragDirection !== "move");
|
||||
};
|
||||
|
||||
// remove event listeners and call updateBlockDates
|
||||
const handleMouseUp = () => {
|
||||
setIsMoving(undefined);
|
||||
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
ganttContainerElement.removeEventListener("scroll", handleOnScroll);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
try {
|
||||
const blockUpdates = getUpdatedPositionAfterDrag(block.id, dragDirection !== "move");
|
||||
updateBlockDates && updateBlockDates(blockUpdates);
|
||||
} catch (e) {
|
||||
setToast;
|
||||
}
|
||||
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
ganttContainerElement.addEventListener("scroll", handleOnScroll);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
return {
|
||||
isMoving,
|
||||
handleBlockDrag,
|
||||
};
|
||||
};
|
||||
@@ -1,338 +1,64 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { RefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
// hooks
|
||||
import { IGanttBlock } from "@/components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// constants
|
||||
import { SIDEBAR_WIDTH } from "../constants";
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
// Plane-web
|
||||
import { LeftDependencyDraggable, RightDependencyDraggable } from "@/plane-web/components/gantt-chart";
|
||||
//
|
||||
import { LeftResizable } from "./blockResizables/left-resizable";
|
||||
import { RightResizable } from "./blockResizables/right-resizable";
|
||||
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void;
|
||||
handleBlockDrag: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, dragDirection: "left" | "right" | "move") => void;
|
||||
isMoving: "left" | "right" | "move" | undefined;
|
||||
enableBlockLeftResize: boolean;
|
||||
enableBlockRightResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const ChartDraggable: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
block,
|
||||
blockToRender,
|
||||
handleBlock,
|
||||
handleBlockDrag,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
isMoving,
|
||||
ganttContainerRef,
|
||||
} = props;
|
||||
// states
|
||||
const [isLeftResizing, setIsLeftResizing] = useState(false);
|
||||
const [isRightResizing, setIsRightResizing] = useState(false);
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
// refs
|
||||
const resizableRef = useRef<HTMLDivElement>(null);
|
||||
// chart hook
|
||||
const { currentViewData } = useGanttChart();
|
||||
// check if cursor reaches either end while resizing/dragging
|
||||
const checkScrollEnd = (e: MouseEvent): number => {
|
||||
const SCROLL_THRESHOLD = 70;
|
||||
|
||||
let delWidth = 0;
|
||||
|
||||
const ganttContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLDivElement;
|
||||
|
||||
if (!ganttContainer || !ganttSidebar) return 0;
|
||||
|
||||
const posFromLeft = e.clientX;
|
||||
// manually scroll to left if reached the left end while dragging
|
||||
if (posFromLeft - (ganttContainer.getBoundingClientRect().left + ganttSidebar.clientWidth) <= SCROLL_THRESHOLD) {
|
||||
if (e.movementX > 0) return 0;
|
||||
|
||||
delWidth = -5;
|
||||
|
||||
ganttContainer.scrollBy(delWidth, 0);
|
||||
} else delWidth = e.movementX;
|
||||
|
||||
// manually scroll to right if reached the right end while dragging
|
||||
const posFromRight = ganttContainer.getBoundingClientRect().right - e.clientX;
|
||||
if (posFromRight <= SCROLL_THRESHOLD) {
|
||||
if (e.movementX < 0) return 0;
|
||||
|
||||
delWidth = 5;
|
||||
|
||||
ganttContainer.scrollBy(delWidth, 0);
|
||||
} else delWidth = e.movementX;
|
||||
|
||||
return delWidth;
|
||||
};
|
||||
// handle block resize from the left end
|
||||
const handleBlockLeftResize = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (!currentViewData || !resizableRef.current || !block.position) return;
|
||||
|
||||
if (e.button !== 0) return;
|
||||
|
||||
const resizableDiv = resizableRef.current;
|
||||
|
||||
const columnWidth = currentViewData.data.width;
|
||||
|
||||
const blockInitialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
|
||||
|
||||
let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
|
||||
let initialMarginLeft = parseInt(resizableDiv.style.marginLeft);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
let delWidth = 0;
|
||||
|
||||
delWidth = checkScrollEnd(e);
|
||||
|
||||
// calculate new width and update the initialMarginLeft using -=
|
||||
const newWidth = Math.round((initialWidth -= delWidth) / columnWidth) * columnWidth;
|
||||
// calculate new marginLeft and update the initial marginLeft to the newly calculated one
|
||||
const newMarginLeft = initialMarginLeft - (newWidth - (block.position?.width ?? 0));
|
||||
initialMarginLeft = newMarginLeft;
|
||||
|
||||
// block needs to be at least 1 column wide
|
||||
if (newWidth < columnWidth) return;
|
||||
|
||||
resizableDiv.style.width = `${newWidth}px`;
|
||||
resizableDiv.style.marginLeft = `${newMarginLeft}px`;
|
||||
|
||||
if (block.position) {
|
||||
block.position.width = newWidth;
|
||||
block.position.marginLeft = newMarginLeft;
|
||||
}
|
||||
};
|
||||
|
||||
// remove event listeners and call block handler with the updated start date
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
const totalBlockShifts = Math.ceil((resizableDiv.clientWidth - blockInitialWidth) / columnWidth);
|
||||
|
||||
handleBlock(totalBlockShifts, "left");
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
if (e.button !== 0) return;
|
||||
|
||||
const resizableDiv = resizableRef.current;
|
||||
|
||||
const columnWidth = currentViewData.data.width;
|
||||
|
||||
const blockInitialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
|
||||
|
||||
let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
let delWidth = 0;
|
||||
|
||||
delWidth = checkScrollEnd(e);
|
||||
|
||||
// calculate new width and update the initialMarginLeft using +=
|
||||
const newWidth = Math.round((initialWidth += delWidth) / columnWidth) * columnWidth;
|
||||
|
||||
// block needs to be at least 1 column wide
|
||||
if (newWidth < columnWidth) return;
|
||||
|
||||
resizableDiv.style.width = `${Math.max(newWidth, 80)}px`;
|
||||
if (block.position) block.position.width = Math.max(newWidth, 80);
|
||||
};
|
||||
|
||||
// remove event listeners and call block handler with the updated target date
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
const totalBlockShifts = Math.ceil((resizableDiv.clientWidth - blockInitialWidth) / columnWidth);
|
||||
|
||||
handleBlock(totalBlockShifts, "right");
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
if (e.button !== 0) return;
|
||||
|
||||
const resizableDiv = resizableRef.current;
|
||||
|
||||
const columnWidth = currentViewData.data.width;
|
||||
|
||||
const blockInitialMarginLeft = parseInt(resizableDiv.style.marginLeft);
|
||||
|
||||
let initialMarginLeft = parseInt(resizableDiv.style.marginLeft);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setIsMoving(true);
|
||||
|
||||
let delWidth = 0;
|
||||
|
||||
delWidth = checkScrollEnd(e);
|
||||
|
||||
// calculate new marginLeft and update the initial marginLeft using -=
|
||||
const newMarginLeft = Math.round((initialMarginLeft += delWidth) / columnWidth) * columnWidth;
|
||||
|
||||
resizableDiv.style.marginLeft = `${newMarginLeft}px`;
|
||||
|
||||
if (block.position) block.position.marginLeft = newMarginLeft;
|
||||
};
|
||||
|
||||
// remove event listeners and call block handler with the updated dates
|
||||
const handleMouseUp = () => {
|
||||
setIsMoving(false);
|
||||
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
const totalBlockShifts = Math.ceil(
|
||||
(parseInt(resizableDiv.style.marginLeft) - blockInitialMarginLeft) / columnWidth
|
||||
);
|
||||
|
||||
handleBlock(totalBlockShifts, "move");
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
// scroll to a hidden block
|
||||
const handleScrollToBlock = () => {
|
||||
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
if (!scrollContainer || !block.position) return;
|
||||
// update container's scroll position to the block's position
|
||||
scrollContainer.scrollLeft = block.position.marginLeft - 4;
|
||||
};
|
||||
// check if block is hidden on either side
|
||||
const isBlockHiddenOnLeft =
|
||||
block.position?.marginLeft &&
|
||||
block.position?.width &&
|
||||
scrollLeft > block.position.marginLeft + block.position.width;
|
||||
|
||||
useEffect(() => {
|
||||
const ganttContainer = ganttContainerRef.current;
|
||||
if (!ganttContainer) return;
|
||||
|
||||
const handleScroll = () => setScrollLeft(ganttContainer.scrollLeft);
|
||||
ganttContainer.addEventListener("scroll", handleScroll);
|
||||
return () => {
|
||||
ganttContainer.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [ganttContainerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement;
|
||||
const resizableBlock = resizableRef.current;
|
||||
if (!resizableBlock || !intersectionRoot) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
setIsHidden(!entry.isIntersecting);
|
||||
});
|
||||
},
|
||||
{
|
||||
root: intersectionRoot,
|
||||
rootMargin: `0px 0px 0px -${SIDEBAR_WIDTH}px`,
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(resizableBlock);
|
||||
|
||||
return () => {
|
||||
observer.unobserve(resizableBlock);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* move to the hidden block */}
|
||||
{isHidden && (
|
||||
<button
|
||||
type="button"
|
||||
className="sticky z-[1] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||
style={{
|
||||
left: `${SIDEBAR_WIDTH + 4}px`,
|
||||
}}
|
||||
onClick={handleScrollToBlock}
|
||||
>
|
||||
<ArrowRight
|
||||
className={cn("h-3.5 w-3.5", {
|
||||
"rotate-180": isBlockHiddenOnLeft,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<div className="group w-full z-[5] relative inline-flex h-full cursor-pointer items-center font-medium transition-all">
|
||||
{/* left resize drag handle */}
|
||||
<LeftDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
|
||||
<LeftResizable
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
handleBlockDrag={handleBlockDrag}
|
||||
isMoving={isMoving}
|
||||
position={block.position}
|
||||
/>
|
||||
<div
|
||||
ref={resizableRef}
|
||||
className="group relative inline-flex h-full cursor-pointer items-center font-medium transition-all"
|
||||
style={{
|
||||
marginLeft: `${block.position?.marginLeft}px`,
|
||||
width: `${block.position?.width}px`,
|
||||
}}
|
||||
className={cn("relative z-[6] flex h-8 w-full items-center rounded", {
|
||||
"pointer-events-none": isMoving,
|
||||
})}
|
||||
onMouseDown={(e) => enableBlockMove && handleBlockDrag(e, "move")}
|
||||
>
|
||||
{/* left resize drag handle */}
|
||||
{enableBlockLeftResize && (
|
||||
<>
|
||||
<div
|
||||
onMouseDown={handleBlockLeftResize}
|
||||
onMouseEnter={() => setIsLeftResizing(true)}
|
||||
onMouseLeave={() => setIsLeftResizing(false)}
|
||||
className="absolute -left-2.5 top-1/2 -translate-y-1/2 z-[3] h-full w-6 cursor-col-resize rounded-md"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
|
||||
{
|
||||
"-left-2.5": isLeftResizing,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={cn("relative z-[2] flex h-8 w-full items-center rounded", {
|
||||
"pointer-events-none": isMoving,
|
||||
})}
|
||||
onMouseDown={handleBlockMove}
|
||||
>
|
||||
{blockToRender(block.data)}
|
||||
</div>
|
||||
{/* right resize drag handle */}
|
||||
{enableBlockRightResize && (
|
||||
<>
|
||||
<div
|
||||
onMouseDown={handleBlockRightResize}
|
||||
onMouseEnter={() => setIsRightResizing(true)}
|
||||
onMouseLeave={() => setIsRightResizing(false)}
|
||||
className="absolute -right-2.5 top-1/2 -translate-y-1/2 z-[2] h-full w-6 cursor-col-resize rounded-md"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
|
||||
{
|
||||
"-right-2.5": isRightResizing,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{blockToRender(block.data)}
|
||||
</div>
|
||||
</>
|
||||
{/* right resize drag handle */}
|
||||
<RightResizable
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
handleBlockDrag={handleBlockDrag}
|
||||
isMoving={isMoving}
|
||||
position={block.position}
|
||||
/>
|
||||
<RightDependencyDraggable block={block} ganttContainerRef={ganttContainerRef} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./use-gantt-chart";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { GanttStoreContext } from "@/components/gantt-chart/contexts";
|
||||
// types
|
||||
import { IGanttStore } from "@/store/issue/issue_gantt_view.store";
|
||||
|
||||
export const useGanttChart = (): IGanttStore => {
|
||||
const context = useContext(GanttStoreContext);
|
||||
if (context === undefined) throw new Error("useGanttChart must be used within GanttStoreProvider");
|
||||
return context;
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from "./blocks";
|
||||
export * from "./chart";
|
||||
export * from "./helpers";
|
||||
export * from "./hooks";
|
||||
export * from "./root";
|
||||
export * from "./sidebar";
|
||||
export * from "./types";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { ChartDataType, ChartViewRoot, IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
// context
|
||||
import { GanttStoreProvider } from "@/components/gantt-chart/contexts";
|
||||
import { ChartViewRoot, IBlockUpdateData, IBlockUpdateDependencyData } from "@/components/gantt-chart";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
|
||||
type GanttChartRootProps = {
|
||||
border?: boolean;
|
||||
@@ -13,9 +14,9 @@ type GanttChartRootProps = {
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
|
||||
enableBlockLeftResize?: boolean | ((blockId: string) => boolean);
|
||||
enableBlockRightResize?: boolean | ((blockId: string) => boolean);
|
||||
enableBlockMove?: boolean | ((blockId: string) => boolean);
|
||||
@@ -27,7 +28,7 @@ type GanttChartRootProps = {
|
||||
showToday?: boolean;
|
||||
};
|
||||
|
||||
export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
|
||||
const {
|
||||
border = true,
|
||||
title,
|
||||
@@ -36,7 +37,6 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
blockUpdateHandler,
|
||||
sidebarToRender,
|
||||
blockToRender,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
canLoadMoreBlocks,
|
||||
enableBlockLeftResize = false,
|
||||
@@ -49,32 +49,38 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
showAllBlocks = false,
|
||||
showToday = true,
|
||||
quickAdd,
|
||||
updateBlockDates,
|
||||
} = props;
|
||||
|
||||
const { setBlockIds } = useTimeLineChartStore();
|
||||
|
||||
// update the timeline store with updated blockIds
|
||||
useEffect(() => {
|
||||
setBlockIds(blockIds);
|
||||
}, [blockIds]);
|
||||
|
||||
return (
|
||||
<GanttStoreProvider>
|
||||
<ChartViewRoot
|
||||
border={border}
|
||||
title={title}
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
loaderTitle={loaderTitle}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
sidebarToRender={sidebarToRender}
|
||||
blockToRender={blockToRender}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableReorder={enableReorder}
|
||||
enableAddBlock={enableAddBlock}
|
||||
enableSelection={enableSelection}
|
||||
bottomSpacing={bottomSpacing}
|
||||
showAllBlocks={showAllBlocks}
|
||||
quickAdd={quickAdd}
|
||||
showToday={showToday}
|
||||
/>
|
||||
</GanttStoreProvider>
|
||||
<ChartViewRoot
|
||||
border={border}
|
||||
title={title}
|
||||
blockIds={blockIds}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
loaderTitle={loaderTitle}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
sidebarToRender={sidebarToRender}
|
||||
blockToRender={blockToRender}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableReorder={enableReorder}
|
||||
enableAddBlock={enableAddBlock}
|
||||
enableSelection={enableSelection}
|
||||
bottomSpacing={bottomSpacing}
|
||||
showAllBlocks={showAllBlocks}
|
||||
quickAdd={quickAdd}
|
||||
showToday={showToday}
|
||||
updateBlockDates={updateBlockDates}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { CycleGanttSidebarBlock } from "@/components/cycles";
|
||||
import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants";
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks";
|
||||
// components
|
||||
// helpers
|
||||
import { IGanttBlock } from "@/components/gantt-chart/types";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
||||
// types
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const { block, isDragging } = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"rounded bg-custom-background-80": isDragging,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
>
|
||||
<div
|
||||
id={`sidebar-block-${block.id}`}
|
||||
className={cn("group w-full flex items-center gap-2 pr-4", {
|
||||
"bg-custom-background-90": isBlockActive(block.id),
|
||||
})}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
||||
<div className="flex-grow truncate">
|
||||
<CycleGanttSidebarBlock cycleId={block.data.id} />
|
||||
</div>
|
||||
{duration && (
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./sidebar";
|
||||
@@ -1,59 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { CyclesSidebarBlock } from "./block";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
blockIds: string[];
|
||||
enableReorder: boolean;
|
||||
};
|
||||
|
||||
export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
||||
const { blockUpdateHandler, blockIds, getBlockById, enableReorder } = props;
|
||||
|
||||
const handleOnDrop = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blockIds, getBlockById, blockUpdateHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{blockIds ? (
|
||||
blockIds.map((blockId, index) => {
|
||||
const block = getBlockById(blockId);
|
||||
if (!block.start_date || !block.target_date) return null;
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean) => <CyclesSidebarBlock block={block} isDragging={isDragging} />}
|
||||
</GanttDnDHOC>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
<Loader.Item height="34px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./cycles";
|
||||
export * from "./issues";
|
||||
export * from "./modules";
|
||||
export * from "./root";
|
||||
|
||||
@@ -2,14 +2,13 @@ import { observer } from "mobx-react";
|
||||
// components
|
||||
import { Row } from "@plane/ui";
|
||||
import { MultipleSelectEntityAction } from "@/components/core";
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks";
|
||||
import { IssueGanttSidebarBlock } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// constants
|
||||
import { BLOCK_HEIGHT, GANTT_SELECT_GROUP } from "../../constants";
|
||||
// types
|
||||
@@ -25,12 +24,13 @@ type Props = {
|
||||
export const IssuesSidebarBlock = observer((props: Props) => {
|
||||
const { block, enableSelection, isDragging, selectionHelpers } = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
|
||||
const { getIsIssuePeeked } = useIssueDetail();
|
||||
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
const isBlockVisibleOnChart = !!block?.start_date && !!block?.target_date;
|
||||
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
|
||||
if (!block.data) return null;
|
||||
if (!block?.data) return null;
|
||||
|
||||
const isIssueSelected = selectionHelpers?.getIsEntitySelected(block.id);
|
||||
const isIssueFocused = selectionHelpers?.getIsEntityActive(block.id);
|
||||
|
||||
@@ -5,19 +5,22 @@ import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
import { IBlockUpdateData } from "@/components/gantt-chart/types";
|
||||
import { GanttLayoutLIstItem } from "@/components/ui";
|
||||
//hooks
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
//
|
||||
import { useTimeLineChart } from "../../../../hooks/use-timeline-chart";
|
||||
import { ETimeLineTypeType } from "../../contexts";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
// types
|
||||
import { IssuesSidebarBlock } from "./block";
|
||||
|
||||
type Props = {
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
getBlockById: (id: string) => IGanttBlock;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
@@ -32,7 +35,6 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blockUpdateHandler,
|
||||
blockIds,
|
||||
getBlockById,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
loadMoreBlocks,
|
||||
@@ -42,6 +44,8 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
selectionHelpers,
|
||||
} = props;
|
||||
|
||||
const { getBlockById } = useTimeLineChart(ETimeLineTypeType.ISSUE);
|
||||
|
||||
const {
|
||||
issues: { getIssueLoader },
|
||||
} = useIssuesStore();
|
||||
@@ -77,22 +81,30 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
if (!block || (!showAllBlocks && !isBlockVisibleOnSidebar)) return;
|
||||
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
<RenderIfVisible
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
root={ganttContainerRef}
|
||||
horizontalOffset={100}
|
||||
verticalOffset={200}
|
||||
shouldRecordHeights={false}
|
||||
placeholderChildren={<GanttLayoutLIstItem />}
|
||||
>
|
||||
{(isDragging: boolean) => (
|
||||
<IssuesSidebarBlock
|
||||
block={block}
|
||||
enableSelection={enableSelection}
|
||||
isDragging={isDragging}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
<GanttDnDHOC
|
||||
id={block.id}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean) => (
|
||||
<IssuesSidebarBlock
|
||||
block={block}
|
||||
enableSelection={enableSelection}
|
||||
isDragging={isDragging}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
</RenderIfVisible>
|
||||
);
|
||||
})}
|
||||
{canLoadMoreBlocks && (
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
// Plane
|
||||
import { Row } from "@plane/ui";
|
||||
import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants";
|
||||
import { useGanttChart } from "@/components/gantt-chart/hooks";
|
||||
// components
|
||||
import { IGanttBlock } from "@/components/gantt-chart/types";
|
||||
import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants";
|
||||
import { ModuleGanttSidebarBlock } from "@/components/modules";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
||||
// types
|
||||
// constants
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
blockId: string;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const { block, isDragging } = props;
|
||||
const { blockId, isDragging } = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
const { getBlockById, updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
|
||||
const block = getBlockById(blockId);
|
||||
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
if (!block) return <></>;
|
||||
|
||||
const isBlockVisibleOnChart = !!block.start_date && !!block.target_date;
|
||||
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -31,20 +31,17 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="h-full">
|
||||
{blockIds ? (
|
||||
blockIds.map((blockId, index) => {
|
||||
const block = getBlockById(blockId);
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean) => <ModulesSidebarBlock block={block} isDragging={isDragging} />}
|
||||
</GanttDnDHOC>
|
||||
);
|
||||
})
|
||||
blockIds.map((blockId, index) => (
|
||||
<GanttDnDHOC
|
||||
key={blockId}
|
||||
id={blockId}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean) => <ModulesSidebarBlock blockId={blockId} isDragging={isDragging} />}
|
||||
</GanttDnDHOC>
|
||||
))
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
|
||||
@@ -21,7 +21,6 @@ type Props = {
|
||||
enableSelection: boolean | ((blockId: string) => boolean);
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
};
|
||||
@@ -33,7 +32,6 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
sidebarToRender,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
canLoadMoreBlocks,
|
||||
ganttContainerRef,
|
||||
@@ -86,7 +84,6 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
|
||||
title,
|
||||
blockUpdateHandler,
|
||||
blockIds,
|
||||
getBlockById,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
canLoadMoreBlocks,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
export interface IGanttBlock {
|
||||
data: any;
|
||||
id: string;
|
||||
name: string;
|
||||
position?: {
|
||||
marginLeft: number;
|
||||
width: number;
|
||||
};
|
||||
sort_order: number;
|
||||
start_date: Date | undefined;
|
||||
target_date: Date | undefined;
|
||||
start_date: string | undefined;
|
||||
target_date: string | undefined;
|
||||
}
|
||||
|
||||
export interface IBlockUpdateData {
|
||||
@@ -20,13 +21,20 @@ export interface IBlockUpdateData {
|
||||
target_date?: string;
|
||||
}
|
||||
|
||||
export type TGanttViews = "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year";
|
||||
export interface IBlockUpdateDependencyData {
|
||||
id: string;
|
||||
start_date?: string;
|
||||
target_date?: string;
|
||||
}
|
||||
|
||||
export type TGanttViews = "week" | "month" | "quarter";
|
||||
|
||||
// chart render types
|
||||
export interface WeekMonthDataType {
|
||||
key: number;
|
||||
shortTitle: string;
|
||||
title: string;
|
||||
abbreviation: string;
|
||||
}
|
||||
|
||||
export interface ChartDataType {
|
||||
@@ -40,5 +48,5 @@ export interface ChartDataTypeData {
|
||||
currentDate: Date;
|
||||
endDate: Date;
|
||||
approxFilterRange: number;
|
||||
width: number;
|
||||
dayWidth: number;
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// types
|
||||
import { weeks, months } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
// data
|
||||
// helpers
|
||||
import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers";
|
||||
|
||||
type GetAllDaysInMonthInMonthViewType = {
|
||||
date: any;
|
||||
day: any;
|
||||
dayData: any;
|
||||
weekNumber: number;
|
||||
title: string;
|
||||
active: boolean;
|
||||
today: boolean;
|
||||
};
|
||||
const getAllDaysInMonthInMonthView = (month: number, year: number) => {
|
||||
const day: GetAllDaysInMonthInMonthViewType[] = [];
|
||||
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
|
||||
const currentDate = new Date();
|
||||
|
||||
Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => {
|
||||
const date: Date = generateDate(_day + 1, month, year);
|
||||
day.push({
|
||||
date: date,
|
||||
day: _day + 1,
|
||||
dayData: weeks[date.getDay()],
|
||||
weekNumber: getWeekNumberByDate(date),
|
||||
title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`,
|
||||
active: false,
|
||||
today:
|
||||
currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1
|
||||
? true
|
||||
: false,
|
||||
});
|
||||
});
|
||||
|
||||
return day;
|
||||
};
|
||||
|
||||
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => {
|
||||
const currentMonth: number = month;
|
||||
const currentYear: number = year;
|
||||
|
||||
const monthPayload = {
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
children: getAllDaysInMonthInMonthView(currentMonth, currentYear),
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
};
|
||||
|
||||
return monthPayload;
|
||||
};
|
||||
|
||||
export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = monthPayload;
|
||||
const renderPayload: any = [];
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 6;
|
||||
let filteredDates: Date[] = [];
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: filteredDates[0],
|
||||
endDate: filteredDates[filteredDates.length - 1],
|
||||
},
|
||||
};
|
||||
} else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: filteredDates[0] },
|
||||
};
|
||||
} else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] },
|
||||
};
|
||||
}
|
||||
|
||||
if (filteredDates && filteredDates.length > 0)
|
||||
for (const currentDate in filteredDates) {
|
||||
const date = filteredDates[parseInt(currentDate)];
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth();
|
||||
renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear));
|
||||
}
|
||||
|
||||
const scrollWidth =
|
||||
renderPayload
|
||||
.map((monthData: any) => monthData.children.length)
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width;
|
||||
|
||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||
};
|
||||
|
||||
export const getNumberOfDaysBetweenTwoDatesInBiWeek = (startDate: Date, endDate: Date) => {
|
||||
let daysDifference: number = 0;
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const timeDifference: number = startDate.getTime() - endDate.getTime();
|
||||
daysDifference = Math.abs(Math.floor(timeDifference / (1000 * 60 * 60 * 24)));
|
||||
|
||||
return daysDifference;
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
// types
|
||||
import { weeks, months } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
// data
|
||||
|
||||
export const getWeekNumberByDate = (date: Date) => {
|
||||
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
|
||||
const daysOffset = firstDayOfYear.getDay();
|
||||
|
||||
const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000;
|
||||
const weekStart = new Date(firstWeekStart);
|
||||
|
||||
const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1;
|
||||
|
||||
return weekNumber;
|
||||
};
|
||||
|
||||
export const getNumberOfDaysInMonth = (month: number, year: number) => {
|
||||
const date = new Date(year, month, 1);
|
||||
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
date.setDate(date.getDate() - 1);
|
||||
|
||||
return date.getDate();
|
||||
};
|
||||
|
||||
export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day);
|
||||
|
||||
export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => {
|
||||
const months = [];
|
||||
|
||||
const startYear = startDate.getFullYear();
|
||||
const startMonth = startDate.getMonth();
|
||||
|
||||
const endYear = endDate.getFullYear();
|
||||
const endMonth = endDate.getMonth();
|
||||
|
||||
const currentDate = new Date(startYear, startMonth);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth();
|
||||
months.push(new Date(currentYear, currentMonth));
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) months.push(endDate);
|
||||
|
||||
return months;
|
||||
};
|
||||
|
||||
export type GetAllDaysInMonthType = {
|
||||
date: any;
|
||||
day: any;
|
||||
dayData: any;
|
||||
weekNumber: number;
|
||||
title: string;
|
||||
today: boolean;
|
||||
};
|
||||
export const getAllDaysInMonth = (month: number, year: number) => {
|
||||
const day: GetAllDaysInMonthType[] = [];
|
||||
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
|
||||
const currentDate = new Date();
|
||||
|
||||
Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => {
|
||||
const date: Date = generateDate(_day + 1, month, year);
|
||||
day.push({
|
||||
date: date,
|
||||
day: _day + 1,
|
||||
dayData: weeks[date.getDay()],
|
||||
weekNumber: getWeekNumberByDate(date),
|
||||
title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`,
|
||||
today:
|
||||
currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1
|
||||
? true
|
||||
: false,
|
||||
});
|
||||
});
|
||||
|
||||
return day;
|
||||
};
|
||||
|
||||
export const generateMonthDataByMonth = (month: number, year: number) => {
|
||||
const currentMonth: number = month;
|
||||
const currentYear: number = year;
|
||||
|
||||
const monthPayload = {
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
children: getAllDaysInMonth(currentMonth, currentYear),
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
};
|
||||
|
||||
return monthPayload;
|
||||
};
|
||||
|
||||
export const generateMonthDataByYear = (monthPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = monthPayload;
|
||||
const renderPayload: any = [];
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 6;
|
||||
let filteredDates: Date[] = [];
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: filteredDates[0],
|
||||
endDate: filteredDates[filteredDates.length - 1],
|
||||
},
|
||||
};
|
||||
} else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: filteredDates[0] },
|
||||
};
|
||||
} else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] },
|
||||
};
|
||||
}
|
||||
|
||||
if (filteredDates && filteredDates.length > 0)
|
||||
for (const currentDate in filteredDates) {
|
||||
const date = filteredDates[parseInt(currentDate)];
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth();
|
||||
renderPayload.push(generateMonthDataByMonth(currentMonth, currentYear));
|
||||
}
|
||||
|
||||
const scrollWidth =
|
||||
renderPayload
|
||||
.map((monthData: any) => monthData.children.length)
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width;
|
||||
|
||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||
};
|
||||
@@ -1,17 +1,35 @@
|
||||
// Generating the date by using the year, month, and day
|
||||
import { addDaysToDate, findTotalDaysInRange, getDate } from "@/helpers/date-time.helper";
|
||||
import { ChartDataType, IGanttBlock } from "../types";
|
||||
import { IMonthBlock, IMonthView, monthView } from "./month-view";
|
||||
import { quarterView } from "./quarter-view";
|
||||
import { IWeekBlock, weekView } from "./week-view";
|
||||
|
||||
/**
|
||||
* Generates Date by using Day, month and Year
|
||||
* @param day
|
||||
* @param month
|
||||
* @param year
|
||||
* @returns
|
||||
*/
|
||||
export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day);
|
||||
|
||||
// Getting the number of days in a month
|
||||
/**
|
||||
* Returns number of days in month
|
||||
* @param month
|
||||
* @param year
|
||||
* @returns
|
||||
*/
|
||||
export const getNumberOfDaysInMonth = (month: number, year: number) => {
|
||||
const date = new Date(year, month, 1);
|
||||
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
date.setDate(date.getDate() - 1);
|
||||
const date = new Date(year, month + 1, 0);
|
||||
|
||||
return date.getDate();
|
||||
};
|
||||
|
||||
// Getting the week number by date
|
||||
/**
|
||||
* Returns week number from date
|
||||
* @param date
|
||||
* @returns
|
||||
*/
|
||||
export const getWeekNumberByDate = (date: Date) => {
|
||||
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
|
||||
const daysOffset = firstDayOfYear.getDay();
|
||||
@@ -24,67 +42,80 @@ export const getWeekNumberByDate = (date: Date) => {
|
||||
return weekNumber;
|
||||
};
|
||||
|
||||
// Getting all weeks between two dates
|
||||
export const getWeeksByMonthAndYear = (month: number, year: number) => {
|
||||
const weeks = [];
|
||||
const startDate = new Date(year, month, 1);
|
||||
const endDate = new Date(year, month + 1, 0);
|
||||
const currentDate = new Date(startDate.getTime());
|
||||
/**
|
||||
* Returns number of days between two dates
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
* @returns
|
||||
*/
|
||||
export const getNumberOfDaysBetweenTwoDates = (startDate: Date, endDate: Date) => {
|
||||
let daysDifference: number = 0;
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + ((7 - currentDate.getDay()) % 7));
|
||||
const timeDifference: number = startDate.getTime() - endDate.getTime();
|
||||
daysDifference = Math.round(timeDifference / (1000 * 60 * 60 * 24));
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
weeks.push({
|
||||
year: year,
|
||||
month: month,
|
||||
weekNumber: getWeekNumberByDate(currentDate),
|
||||
startDate: new Date(currentDate.getTime()),
|
||||
endDate: new Date(currentDate.getTime() + 6 * 24 * 60 * 60 * 1000),
|
||||
});
|
||||
currentDate.setDate(currentDate.getDate() + 7);
|
||||
}
|
||||
|
||||
return weeks;
|
||||
return daysDifference;
|
||||
};
|
||||
|
||||
// Getting all dates in a week by week number and year
|
||||
export const getAllDatesInWeekByWeekNumber = (weekNumber: number, year: number) => {
|
||||
const januaryFirst = new Date(year, 0, 1);
|
||||
const firstDayOfYear =
|
||||
januaryFirst.getDay() === 0 ? januaryFirst : new Date(year, 0, 1 + (7 - januaryFirst.getDay()));
|
||||
/**
|
||||
* returns a date corresponding to the position on the timeline chart
|
||||
* @param position
|
||||
* @param chartData
|
||||
* @param offsetDays
|
||||
* @returns
|
||||
*/
|
||||
export const getDateFromPositionOnGantt = (position: number, chartData: ChartDataType, offsetDays = 0) => {
|
||||
const numberOfDaysSinceStart = Math.round(position / chartData.data.dayWidth) + offsetDays;
|
||||
|
||||
const startDate = new Date(firstDayOfYear.getTime());
|
||||
startDate.setDate(startDate.getDate() + 7 * (weekNumber - 1));
|
||||
const newDate = addDaysToDate(chartData.data.startDate, numberOfDaysSinceStart);
|
||||
|
||||
const datesInWeek = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const currentDate = new Date(startDate.getTime());
|
||||
currentDate.setDate(currentDate.getDate() + i);
|
||||
datesInWeek.push(currentDate);
|
||||
}
|
||||
if (!newDate) undefined;
|
||||
|
||||
return datesInWeek;
|
||||
return newDate;
|
||||
};
|
||||
|
||||
// Getting the dates between two dates
|
||||
export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => {
|
||||
const dates = [];
|
||||
/**
|
||||
* returns the position and width of the block on the timeline chart from startDate and EndDate
|
||||
* @param chartData
|
||||
* @param itemData
|
||||
* @returns
|
||||
*/
|
||||
export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttBlock) => {
|
||||
let scrollPosition: number = 0;
|
||||
let scrollWidth: number = 0;
|
||||
|
||||
const startYear = startDate.getFullYear();
|
||||
const startMonth = startDate.getMonth();
|
||||
const { startDate: chartStartDate } = chartData.data;
|
||||
const { start_date, target_date } = itemData;
|
||||
|
||||
const endYear = endDate.getFullYear();
|
||||
const endMonth = endDate.getMonth();
|
||||
const itemStartDate = getDate(start_date);
|
||||
const itemTargetDate = getDate(target_date);
|
||||
|
||||
const currentDate = new Date(startYear, startMonth);
|
||||
if (!itemStartDate || !itemTargetDate) return;
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth();
|
||||
dates.push(new Date(currentYear, currentMonth));
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) dates.push(endDate);
|
||||
chartStartDate.setHours(0, 0, 0, 0);
|
||||
itemStartDate.setHours(0, 0, 0, 0);
|
||||
itemTargetDate.setHours(0, 0, 0, 0);
|
||||
|
||||
return dates;
|
||||
// get number of days from chart start date to block's start date
|
||||
const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, itemStartDate, false) ?? 0);
|
||||
|
||||
if (!positionDaysDifference) return;
|
||||
|
||||
// get scroll position from the number of days and width of each day
|
||||
scrollPosition = positionDaysDifference * chartData.data.dayWidth;
|
||||
|
||||
// get width of block
|
||||
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
|
||||
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
|
||||
scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth;
|
||||
|
||||
return { marginLeft: scrollPosition, width: scrollWidth };
|
||||
};
|
||||
|
||||
export const timelineViewHelpers = {
|
||||
week: weekView,
|
||||
month: monthView,
|
||||
quarter: quarterView,
|
||||
};
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
// types
|
||||
import { weeks, months } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
// data
|
||||
|
||||
export const getWeekNumberByDate = (date: Date) => {
|
||||
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
|
||||
const daysOffset = firstDayOfYear.getDay();
|
||||
|
||||
const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000;
|
||||
const weekStart = new Date(firstWeekStart);
|
||||
|
||||
const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1;
|
||||
|
||||
return weekNumber;
|
||||
};
|
||||
|
||||
export const getNumberOfDaysInMonth = (month: number, year: number) => {
|
||||
const date = new Date(year, month, 1);
|
||||
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
date.setDate(date.getDate() - 1);
|
||||
|
||||
return date.getDate();
|
||||
};
|
||||
|
||||
export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day);
|
||||
|
||||
export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => {
|
||||
const months = [];
|
||||
|
||||
const startYear = startDate.getFullYear();
|
||||
const startMonth = startDate.getMonth();
|
||||
|
||||
const endYear = endDate.getFullYear();
|
||||
const endMonth = endDate.getMonth();
|
||||
|
||||
const currentDate = new Date(startYear, startMonth);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth();
|
||||
months.push(new Date(currentYear, currentMonth));
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) months.push(endDate);
|
||||
|
||||
return months;
|
||||
};
|
||||
|
||||
export type GetAllDaysInMonthType = {
|
||||
date: any;
|
||||
day: any;
|
||||
dayData: any;
|
||||
weekNumber: number;
|
||||
title: string;
|
||||
today: boolean;
|
||||
};
|
||||
export const getAllDaysInMonth = (month: number, year: number) => {
|
||||
const day: GetAllDaysInMonthType[] = [];
|
||||
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
|
||||
const currentDate = new Date();
|
||||
|
||||
Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => {
|
||||
const date: Date = generateDate(_day + 1, month, year);
|
||||
day.push({
|
||||
date: date,
|
||||
day: _day + 1,
|
||||
dayData: weeks[date.getDay()],
|
||||
weekNumber: getWeekNumberByDate(date),
|
||||
title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`,
|
||||
today:
|
||||
currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1
|
||||
? true
|
||||
: false,
|
||||
});
|
||||
});
|
||||
|
||||
return day;
|
||||
};
|
||||
|
||||
export const generateMonthDataByMonth = (month: number, year: number) => {
|
||||
const currentMonth: number = month;
|
||||
const currentYear: number = year;
|
||||
|
||||
const monthPayload = {
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
children: getAllDaysInMonth(currentMonth, currentYear),
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
};
|
||||
|
||||
return monthPayload;
|
||||
};
|
||||
|
||||
export const generateMonthDataByYear = (monthPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = monthPayload;
|
||||
const renderPayload: any = [];
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 6;
|
||||
let filteredDates: Date[] = [];
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: filteredDates[0],
|
||||
endDate: filteredDates[filteredDates.length - 1],
|
||||
},
|
||||
};
|
||||
} else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: filteredDates[0] },
|
||||
};
|
||||
} else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] },
|
||||
};
|
||||
}
|
||||
|
||||
if (filteredDates && filteredDates.length > 0)
|
||||
for (const currentDate in filteredDates) {
|
||||
const date = filteredDates[parseInt(currentDate)];
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth();
|
||||
renderPayload.push(generateMonthDataByMonth(currentMonth, currentYear));
|
||||
}
|
||||
|
||||
const scrollWidth =
|
||||
renderPayload
|
||||
.map((monthData: any) => monthData.children.length)
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width;
|
||||
|
||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||
};
|
||||
@@ -1,7 +1,4 @@
|
||||
// export * from "./hours-view";
|
||||
// export * from "./day-view";
|
||||
export * from "./week-view";
|
||||
export * from "./bi-week-view";
|
||||
export * from "./month-view";
|
||||
export * from "./quater-view";
|
||||
export * from "./year-view";
|
||||
export * from "./quarter-view";
|
||||
export * from "./helpers";
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
// types
|
||||
import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
||||
import { weeks, months } from "../data";
|
||||
import { ChartDataType, IGanttBlock } from "../types";
|
||||
// data
|
||||
// helpers
|
||||
import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers";
|
||||
|
||||
type GetAllDaysInMonthInMonthViewType = {
|
||||
date: any;
|
||||
day: any;
|
||||
dayData: any;
|
||||
weekNumber: number;
|
||||
title: string;
|
||||
active: boolean;
|
||||
today: boolean;
|
||||
};
|
||||
|
||||
interface IMonthChild {
|
||||
active: boolean;
|
||||
date: Date;
|
||||
day: number;
|
||||
dayData: {
|
||||
key: number;
|
||||
shortTitle: string;
|
||||
title: string;
|
||||
};
|
||||
title: string;
|
||||
today: boolean;
|
||||
weekNumber: number;
|
||||
}
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
//
|
||||
import { months } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
import { getNumberOfDaysInMonth } from "./helpers";
|
||||
import { getWeeksBetweenTwoDates, IWeekBlock } from "./week-view";
|
||||
|
||||
export interface IMonthBlock {
|
||||
children: IMonthChild[];
|
||||
today: boolean;
|
||||
month: number;
|
||||
days: number;
|
||||
monthData: {
|
||||
key: number;
|
||||
shortTitle: string;
|
||||
@@ -41,157 +18,141 @@ export interface IMonthBlock {
|
||||
title: string;
|
||||
year: number;
|
||||
}
|
||||
[];
|
||||
|
||||
const getAllDaysInMonthInMonthView = (month: number, year: number): IMonthChild[] => {
|
||||
const day: GetAllDaysInMonthInMonthViewType[] = [];
|
||||
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
|
||||
const currentDate = new Date();
|
||||
export interface IMonthView {
|
||||
months: IMonthBlock[];
|
||||
weeks: IWeekBlock[];
|
||||
}
|
||||
|
||||
Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => {
|
||||
const date: Date = generateDate(_day + 1, month, year);
|
||||
day.push({
|
||||
date: date,
|
||||
day: _day + 1,
|
||||
dayData: weeks[date.getDay()],
|
||||
weekNumber: getWeekNumberByDate(date),
|
||||
title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`,
|
||||
active: false,
|
||||
today:
|
||||
currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1
|
||||
? true
|
||||
: false,
|
||||
});
|
||||
});
|
||||
|
||||
return day;
|
||||
};
|
||||
|
||||
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number): IMonthBlock => {
|
||||
const currentMonth: number = month;
|
||||
const currentYear: number = year;
|
||||
|
||||
const monthPayload = {
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
children: getAllDaysInMonthInMonthView(currentMonth, currentYear),
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
};
|
||||
|
||||
return monthPayload;
|
||||
};
|
||||
|
||||
export const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = monthPayload;
|
||||
const renderPayload: any = [];
|
||||
/**
|
||||
* Generate Month Chart data
|
||||
* @param monthPayload
|
||||
* @param side
|
||||
* @returns
|
||||
*/
|
||||
const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = cloneDeep(monthPayload);
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 6;
|
||||
let filteredDates: Date[] = [];
|
||||
let filteredDates: IMonthView = { months: [], weeks: [] };
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
// if side is null generate months on both side of current date
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: filteredDates[0],
|
||||
endDate: filteredDates[filteredDates.length - 1],
|
||||
startDate: filteredDates.weeks[0]?.startDate,
|
||||
endDate: filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate,
|
||||
},
|
||||
};
|
||||
} else if (side === "left") {
|
||||
}
|
||||
// When side is left, generate more months on the left side of the start date
|
||||
else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: filteredDates[0] },
|
||||
data: { ...renderState.data, startDate: filteredDates.weeks[0].startDate },
|
||||
};
|
||||
} else if (side === "right") {
|
||||
}
|
||||
// When side is right, generate more months on the right side of the end date
|
||||
else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] },
|
||||
data: { ...renderState.data, endDate: filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate },
|
||||
};
|
||||
}
|
||||
|
||||
if (filteredDates && filteredDates.length > 0)
|
||||
for (const currentDate in filteredDates) {
|
||||
const date = filteredDates[parseInt(currentDate)];
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth();
|
||||
renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear));
|
||||
}
|
||||
const scrollWidth = filteredDates.weeks.length * monthPayload.data.dayWidth * 7;
|
||||
|
||||
const scrollWidth =
|
||||
renderPayload
|
||||
.map((monthData: any) => monthData.children.length)
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width;
|
||||
|
||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||
return { state: renderState, payload: filteredDates, scrollWidth: scrollWidth };
|
||||
};
|
||||
|
||||
export const getNumberOfDaysBetweenTwoDatesInMonth = (startDate: Date, endDate: Date) => {
|
||||
let daysDifference: number = 0;
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
/**
|
||||
* Get Month View data between two dates, i.e., Months and Weeks between two dates
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
* @returns
|
||||
*/
|
||||
const getMonthsViewBetweenTwoDates = (startDate: Date, endDate: Date): IMonthView => ({
|
||||
months: getMonthsBetweenTwoDates(startDate, endDate),
|
||||
weeks: getWeeksBetweenTwoDates(startDate, endDate, false),
|
||||
});
|
||||
|
||||
const timeDifference: number = startDate.getTime() - endDate.getTime();
|
||||
daysDifference = Math.abs(Math.floor(timeDifference / (1000 * 60 * 60 * 24)));
|
||||
/**
|
||||
* generate array of months between two dates
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
* @returns
|
||||
*/
|
||||
export const getMonthsBetweenTwoDates = (startDate: Date, endDate: Date): IMonthBlock[] => {
|
||||
const monthBlocks = [];
|
||||
|
||||
return daysDifference;
|
||||
const startYear = startDate.getFullYear();
|
||||
const startMonth = startDate.getMonth();
|
||||
|
||||
const today = new Date();
|
||||
const todayMonth = today.getMonth();
|
||||
const todayYear = today.getFullYear();
|
||||
|
||||
const currentDate = new Date(startYear, startMonth);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth();
|
||||
|
||||
monthBlocks.push({
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
days: getNumberOfDaysInMonth(currentMonth, currentYear),
|
||||
today: todayMonth === currentMonth && todayYear === currentYear,
|
||||
});
|
||||
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
|
||||
return monthBlocks;
|
||||
};
|
||||
|
||||
// calc item scroll position and width
|
||||
export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, itemData: IGanttBlock) => {
|
||||
let scrollPosition: number = 0;
|
||||
let scrollWidth: number = 0;
|
||||
/**
|
||||
* Merge two MonthView data payloads
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns
|
||||
*/
|
||||
const mergeMonthRenderPayloads = (a: IMonthView, b: IMonthView): IMonthView => ({
|
||||
months: uniqBy([...a.months, ...b.months], (monthBlock) => `${monthBlock.month}_${monthBlock.year}`),
|
||||
weeks: uniqBy(
|
||||
[...a.weeks, ...b.weeks],
|
||||
(weekBlock) => `${weekBlock.startDate.getTime()}_${weekBlock.endDate.getTime()}`
|
||||
),
|
||||
});
|
||||
|
||||
const { startDate } = chartData.data;
|
||||
const { start_date: itemStartDate, target_date: itemTargetDate } = itemData;
|
||||
|
||||
if (!itemStartDate || !itemTargetDate) return;
|
||||
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
itemStartDate.setHours(0, 0, 0, 0);
|
||||
itemTargetDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const positionDaysDifference = findTotalDaysInRange(startDate, itemStartDate, false);
|
||||
|
||||
if (!positionDaysDifference) return;
|
||||
|
||||
scrollPosition = positionDaysDifference * chartData.data.width;
|
||||
|
||||
let diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12;
|
||||
diffMonths -= startDate.getMonth();
|
||||
diffMonths += itemStartDate.getMonth();
|
||||
|
||||
scrollPosition = scrollPosition + diffMonths;
|
||||
// position code ends
|
||||
|
||||
// width code starts
|
||||
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
|
||||
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
|
||||
scrollWidth = (widthDaysDifference + 1) * chartData.data.width + 1;
|
||||
// width code ends
|
||||
|
||||
return { marginLeft: scrollPosition, width: scrollWidth };
|
||||
export const monthView = {
|
||||
generateChart: generateMonthChart,
|
||||
mergeRenderPayloads: mergeMonthRenderPayloads,
|
||||
};
|
||||
|
||||
141
web/core/components/gantt-chart/views/quarter-view.ts
Normal file
141
web/core/components/gantt-chart/views/quarter-view.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
//
|
||||
import { quarters } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
import { getNumberOfDaysBetweenTwoDates } from "./helpers";
|
||||
import { getMonthsBetweenTwoDates, IMonthBlock } from "./month-view";
|
||||
|
||||
export interface IQuarterMonthBlock {
|
||||
children: IMonthBlock[];
|
||||
quarterNumber: number;
|
||||
shortTitle: string;
|
||||
title: string;
|
||||
year: number;
|
||||
today: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Quarter Chart data, which in turn are months in an array
|
||||
* @param quarterPayload
|
||||
* @param side
|
||||
* @returns
|
||||
*/
|
||||
const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = quarterPayload;
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 12;
|
||||
let filteredDates: IMonthBlock[] = [];
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
// if side is null generate months on both side of current date
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
const startMonthBlock = filteredDates[0];
|
||||
const endMonthBlock = filteredDates[filteredDates.length - 1];
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: new Date(startMonthBlock.year, startMonthBlock.month, 1),
|
||||
endDate: new Date(endMonthBlock.year, endMonthBlock.month + 1, 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
// When side is left, generate more months on the left side of the start date
|
||||
else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
const startMonthBlock = filteredDates[0];
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: new Date(startMonthBlock.year, startMonthBlock.month, 1) },
|
||||
};
|
||||
}
|
||||
// When side is right, generate more months on the right side of the end date
|
||||
else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
const endMonthBlock = filteredDates[filteredDates.length - 1];
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: new Date(endMonthBlock.year, endMonthBlock.month + 1, 0) },
|
||||
};
|
||||
}
|
||||
|
||||
const startMonthBlock = filteredDates[0];
|
||||
const endMonthBlock = filteredDates[filteredDates.length - 1];
|
||||
const startDate = new Date(startMonthBlock.year, startMonthBlock.month, 1);
|
||||
const endDate = new Date(endMonthBlock.year, endMonthBlock.month + 1, 0);
|
||||
|
||||
const days = Math.abs(getNumberOfDaysBetweenTwoDates(startDate, endDate));
|
||||
const scrollWidth = days * quarterPayload.data.dayWidth;
|
||||
|
||||
return { state: renderState, payload: filteredDates, scrollWidth: scrollWidth };
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge two Quarter data payloads
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns
|
||||
*/
|
||||
const mergeQuarterRenderPayloads = (a: IMonthBlock[], b: IMonthBlock[]) => [...a, ...b];
|
||||
|
||||
/**
|
||||
* Group array of Months into Quarters, returns an array og Quarters and it's children Months
|
||||
* @param monthBlocks
|
||||
* @returns
|
||||
*/
|
||||
export const groupMonthsToQuarters = (monthBlocks: IMonthBlock[]): IQuarterMonthBlock[] => {
|
||||
const quartersMap: { [key: string]: IQuarterMonthBlock } = {};
|
||||
|
||||
const today = new Date();
|
||||
const todayQuarterNumber = Math.floor(today.getMonth() / 3);
|
||||
const todayYear = today.getFullYear();
|
||||
|
||||
for (const monthBlock of monthBlocks) {
|
||||
const { month, year } = monthBlock;
|
||||
|
||||
const quarterNumber = Math.floor(month / 3);
|
||||
|
||||
const quarterKey = `Q${quarterNumber}-${year}`;
|
||||
|
||||
if (quartersMap[quarterKey]) {
|
||||
quartersMap[quarterKey].children.push(monthBlock);
|
||||
} else {
|
||||
const quarterData = quarters[quarterNumber];
|
||||
quartersMap[quarterKey] = {
|
||||
children: [monthBlock],
|
||||
quarterNumber,
|
||||
shortTitle: quarterData.shortTitle,
|
||||
title: `${quarterData.title} ${year}`,
|
||||
year,
|
||||
today: todayQuarterNumber === quarterNumber && todayYear === year,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(quartersMap);
|
||||
};
|
||||
|
||||
export const quarterView = {
|
||||
generateChart: generateQuarterChart,
|
||||
mergeRenderPayloads: mergeQuarterRenderPayloads,
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
// types
|
||||
import { weeks, months } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
// data
|
||||
// helpers
|
||||
import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers";
|
||||
|
||||
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => {
|
||||
const currentMonth: number = month;
|
||||
const currentYear: number = year;
|
||||
const today = new Date();
|
||||
|
||||
const weeksBetweenTwoDates = getWeeksByMonthAndYear(month, year);
|
||||
|
||||
const weekPayload = {
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
children: weeksBetweenTwoDates.map((weekData: any) => {
|
||||
const date: Date = weekData.startDate;
|
||||
return {
|
||||
date: date,
|
||||
startDate: weekData.startDate,
|
||||
endDate: weekData.endDate,
|
||||
day: date.getDay(),
|
||||
dayData: weeks[date.getDay()],
|
||||
weekNumber: weekData.weekNumber,
|
||||
title: `W${weekData.weekNumber} (${date.getDate()})`,
|
||||
active: false,
|
||||
today: today >= weekData.startDate && today <= weekData.endDate ? true : false,
|
||||
};
|
||||
}),
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
};
|
||||
|
||||
return weekPayload;
|
||||
};
|
||||
|
||||
export const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = quarterPayload;
|
||||
const renderPayload: any = [];
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 6;
|
||||
let filteredDates: Date[] = [];
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: filteredDates[0],
|
||||
endDate: filteredDates[filteredDates.length - 1],
|
||||
},
|
||||
};
|
||||
} else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 0);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: filteredDates[0] },
|
||||
};
|
||||
} else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] },
|
||||
};
|
||||
}
|
||||
|
||||
if (filteredDates && filteredDates.length > 0)
|
||||
for (const currentDate in filteredDates) {
|
||||
const date = filteredDates[parseInt(currentDate)];
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth();
|
||||
renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear));
|
||||
}
|
||||
|
||||
const scrollWidth =
|
||||
renderPayload
|
||||
.map((monthData: any) => monthData.children.length)
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * quarterPayload.data.width;
|
||||
|
||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||
};
|
||||
|
||||
export const getNumberOfDaysBetweenTwoDatesInQuarter = (startDate: Date, endDate: Date) => {
|
||||
let weeksDifference: number = 0;
|
||||
|
||||
const timeDiff = Math.abs(endDate.getTime() - startDate.getTime());
|
||||
const diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
weeksDifference = Math.floor(diffDays / 7);
|
||||
|
||||
return weeksDifference;
|
||||
};
|
||||
@@ -1,132 +1,194 @@
|
||||
// types
|
||||
//
|
||||
import { weeks, months } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
// data
|
||||
// helpers
|
||||
import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers";
|
||||
|
||||
type GetAllDaysInMonthInMonthViewType = {
|
||||
date: any;
|
||||
day: any;
|
||||
dayData: any;
|
||||
weekNumber: number;
|
||||
title: string;
|
||||
active: boolean;
|
||||
today: boolean;
|
||||
};
|
||||
const getAllDaysInMonthInMonthView = (month: number, year: number) => {
|
||||
const day: GetAllDaysInMonthInMonthViewType[] = [];
|
||||
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
|
||||
const currentDate = new Date();
|
||||
|
||||
Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => {
|
||||
const date: Date = generateDate(_day + 1, month, year);
|
||||
day.push({
|
||||
date: date,
|
||||
day: _day + 1,
|
||||
dayData: weeks[date.getDay()],
|
||||
weekNumber: getWeekNumberByDate(date),
|
||||
title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`,
|
||||
active: false,
|
||||
today:
|
||||
currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1
|
||||
? true
|
||||
: false,
|
||||
});
|
||||
});
|
||||
|
||||
return day;
|
||||
};
|
||||
|
||||
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => {
|
||||
const currentMonth: number = month;
|
||||
const currentYear: number = year;
|
||||
|
||||
const monthPayload = {
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
children: getAllDaysInMonthInMonthView(currentMonth, currentYear),
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
import { getWeekNumberByDate } from "./helpers";
|
||||
export interface IDayBlock {
|
||||
date: Date;
|
||||
day: number;
|
||||
dayData: {
|
||||
key: number;
|
||||
shortTitle: string;
|
||||
title: string;
|
||||
abbreviation: string;
|
||||
};
|
||||
title: string;
|
||||
today: boolean;
|
||||
}
|
||||
|
||||
return monthPayload;
|
||||
};
|
||||
export interface IWeekBlock {
|
||||
children?: IDayBlock[];
|
||||
weekNumber: number;
|
||||
weekData: {
|
||||
shortTitle: string;
|
||||
title: string;
|
||||
};
|
||||
title: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
startMonth: number;
|
||||
startYear: number;
|
||||
endMonth: number;
|
||||
endYear: number;
|
||||
today: boolean;
|
||||
}
|
||||
|
||||
export const generateWeekChart = (monthPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = monthPayload;
|
||||
const renderPayload: any = [];
|
||||
/**
|
||||
* Generate Week Chart data
|
||||
* @param weekPayload
|
||||
* @param side
|
||||
* @returns
|
||||
*/
|
||||
const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = weekPayload;
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 6;
|
||||
let filteredDates: Date[] = [];
|
||||
let filteredDates: IWeekBlock[] = [];
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
// if side is null generate weeks on both side of current date
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: filteredDates[0],
|
||||
endDate: filteredDates[filteredDates.length - 1],
|
||||
startDate: filteredDates[0].startDate,
|
||||
endDate: filteredDates[filteredDates.length - 1].endDate,
|
||||
},
|
||||
};
|
||||
} else if (side === "left") {
|
||||
}
|
||||
// When side is left, generate more weeks on the left side of the start date
|
||||
else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: filteredDates[0] },
|
||||
data: { ...renderState.data, startDate: filteredDates[0].startDate },
|
||||
};
|
||||
} else if (side === "right") {
|
||||
}
|
||||
// When side is right, generate more weeks on the right side of the end date
|
||||
else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] },
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1].endDate },
|
||||
};
|
||||
}
|
||||
|
||||
if (filteredDates && filteredDates.length > 0)
|
||||
for (const currentDate in filteredDates) {
|
||||
const date = filteredDates[parseInt(currentDate)];
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth();
|
||||
renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear));
|
||||
}
|
||||
|
||||
const scrollWidth =
|
||||
renderPayload
|
||||
filteredDates
|
||||
.map((monthData: any) => monthData.children.length)
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width;
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * weekPayload.data.dayWidth;
|
||||
|
||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||
return { state: renderState, payload: filteredDates, scrollWidth: scrollWidth };
|
||||
};
|
||||
|
||||
export const getNumberOfDaysBetweenTwoDatesInWeek = (startDate: Date, endDate: Date) => {
|
||||
let daysDifference: number = 0;
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
/**
|
||||
* Generate weeks array between two dates
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
* @param shouldPopulateDaysForWeek
|
||||
* @returns
|
||||
*/
|
||||
export const getWeeksBetweenTwoDates = (
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
shouldPopulateDaysForWeek: boolean = true
|
||||
): IWeekBlock[] => {
|
||||
const weeks: IWeekBlock[] = [];
|
||||
|
||||
const timeDifference: number = startDate.getTime() - endDate.getTime();
|
||||
daysDifference = Math.abs(Math.floor(timeDifference / (1000 * 60 * 60 * 24)));
|
||||
const currentDate = new Date(startDate.getTime());
|
||||
const today = new Date();
|
||||
|
||||
return daysDifference;
|
||||
currentDate.setDate(currentDate.getDate() - currentDate.getDay());
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const weekStartDate = new Date(currentDate.getTime());
|
||||
const weekEndDate = new Date(currentDate.getTime() + 6 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const monthAtStartOfTheWeek = weekStartDate.getMonth();
|
||||
const yearAtStartOfTheWeek = weekStartDate.getFullYear();
|
||||
const monthAtEndOfTheWeek = weekEndDate.getMonth();
|
||||
const yearAtEndOfTheWeek = weekEndDate.getFullYear();
|
||||
|
||||
const weekNumber = getWeekNumberByDate(currentDate);
|
||||
|
||||
weeks.push({
|
||||
children: shouldPopulateDaysForWeek ? populateDaysForWeek(weekStartDate) : undefined,
|
||||
weekNumber,
|
||||
weekData: {
|
||||
shortTitle: `w${weekNumber}`,
|
||||
title: `Week ${weekNumber}`,
|
||||
},
|
||||
title:
|
||||
monthAtStartOfTheWeek === monthAtEndOfTheWeek
|
||||
? `${months[monthAtStartOfTheWeek].abbreviation} ${yearAtStartOfTheWeek}`
|
||||
: `${months[monthAtStartOfTheWeek].abbreviation} ${yearAtStartOfTheWeek} - ${months[monthAtEndOfTheWeek].abbreviation} ${yearAtEndOfTheWeek}`,
|
||||
startMonth: monthAtStartOfTheWeek,
|
||||
startYear: yearAtStartOfTheWeek,
|
||||
endMonth: monthAtEndOfTheWeek,
|
||||
endYear: yearAtEndOfTheWeek,
|
||||
startDate: weekStartDate,
|
||||
endDate: weekEndDate,
|
||||
today: today >= weekStartDate && today <= weekEndDate ? true : false,
|
||||
});
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 7);
|
||||
}
|
||||
|
||||
return weeks;
|
||||
};
|
||||
|
||||
/**
|
||||
* return back array of 7 days from the date provided
|
||||
* @param startDate
|
||||
* @returns
|
||||
*/
|
||||
const populateDaysForWeek = (startDate: Date): IDayBlock[] => {
|
||||
const currentDate = new Date(startDate);
|
||||
const days: IDayBlock[] = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
days.push({
|
||||
date: new Date(currentDate),
|
||||
day: currentDate.getDay(),
|
||||
dayData: weeks[currentDate.getDay()],
|
||||
title: `${weeks[currentDate.getDay()].abbreviation} ${currentDate.getDate()}`,
|
||||
today: today.setHours(0, 0, 0, 0) == currentDate.setHours(0, 0, 0, 0),
|
||||
});
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge two Week data payloads
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns
|
||||
*/
|
||||
const mergeWeekRenderPayloads = (a: IWeekBlock[], b: IWeekBlock[]) => [...a, ...b];
|
||||
|
||||
export const weekView = {
|
||||
generateChart: generateWeekChart,
|
||||
mergeRenderPayloads: mergeWeekRenderPayloads,
|
||||
};
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
// types
|
||||
import { weeks, months } from "../data";
|
||||
import { ChartDataType } from "../types";
|
||||
// data
|
||||
// helpers
|
||||
import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers";
|
||||
|
||||
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => {
|
||||
const currentMonth: number = month;
|
||||
const currentYear: number = year;
|
||||
const today = new Date();
|
||||
|
||||
const weeksBetweenTwoDates = getWeeksByMonthAndYear(month, year);
|
||||
|
||||
const weekPayload = {
|
||||
year: currentYear,
|
||||
month: currentMonth,
|
||||
monthData: months[currentMonth],
|
||||
children: weeksBetweenTwoDates.map((weekData: any) => {
|
||||
const date: Date = weekData.startDate;
|
||||
return {
|
||||
date: date,
|
||||
startDate: weekData.startDate,
|
||||
endDate: weekData.endDate,
|
||||
day: date.getDay(),
|
||||
dayData: weeks[date.getDay()],
|
||||
weekNumber: weekData.weekNumber,
|
||||
title: `W${weekData.weekNumber} (${date.getDate()})`,
|
||||
active: false,
|
||||
today: today >= weekData.startDate && today <= weekData.endDate ? true : false,
|
||||
};
|
||||
}),
|
||||
title: `${months[currentMonth].title} ${currentYear}`,
|
||||
};
|
||||
|
||||
return weekPayload;
|
||||
};
|
||||
|
||||
export const generateYearChart = (yearPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
let renderState = yearPayload;
|
||||
const renderPayload: any = [];
|
||||
|
||||
const range: number = renderState.data.approxFilterRange || 6;
|
||||
let filteredDates: Date[] = [];
|
||||
let minusDate: Date = new Date();
|
||||
let plusDate: Date = new Date();
|
||||
|
||||
if (side === null) {
|
||||
const currentDate = renderState.data.currentDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: {
|
||||
...renderState.data,
|
||||
startDate: filteredDates[0],
|
||||
endDate: filteredDates[filteredDates.length - 1],
|
||||
},
|
||||
};
|
||||
} else if (side === "left") {
|
||||
const currentDate = renderState.data.startDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 0);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, startDate: filteredDates[0] },
|
||||
};
|
||||
} else if (side === "right") {
|
||||
const currentDate = renderState.data.endDate;
|
||||
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
renderState = {
|
||||
...renderState,
|
||||
data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] },
|
||||
};
|
||||
}
|
||||
|
||||
if (filteredDates && filteredDates.length > 0)
|
||||
for (const currentDate in filteredDates) {
|
||||
const date = filteredDates[parseInt(currentDate)];
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth();
|
||||
renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear));
|
||||
}
|
||||
|
||||
const scrollWidth =
|
||||
renderPayload
|
||||
.map((monthData: any) => monthData.children.length)
|
||||
.reduce((partialSum: number, a: number) => partialSum + a, 0) * yearPayload.data.width;
|
||||
|
||||
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
|
||||
};
|
||||
|
||||
export const getNumberOfDaysBetweenTwoDatesInYear = (startDate: Date, endDate: Date) => {
|
||||
let weeksDifference: number = 0;
|
||||
|
||||
const timeDiff = Math.abs(endDate.getTime() - startDate.getTime());
|
||||
const diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
weeksDifference = Math.floor(diffDays / 7);
|
||||
|
||||
return weeksDifference;
|
||||
};
|
||||
@@ -24,17 +24,17 @@ export const IssueDetailWidgetCollapsibles: FC<Props> = observer((props) => {
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
subIssues: { subIssuesByIssueId },
|
||||
relation: { getRelationsByIssueId },
|
||||
relation: { getRelationCountByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
const subIssues = subIssuesByIssueId(issueId);
|
||||
const issueRelations = getRelationsByIssueId(issueId);
|
||||
const issueRelationsCount = getRelationCountByIssueId(issueId);
|
||||
|
||||
// render conditions
|
||||
const shouldRenderSubIssues = !!subIssues && subIssues.length > 0;
|
||||
const shouldRenderRelations = Object.values(issueRelations ?? {}).some((relation) => relation.length > 0);
|
||||
const shouldRenderRelations = issueRelationsCount > 0;
|
||||
const shouldRenderLinks = !!issue?.link_count && issue?.link_count > 0;
|
||||
const shouldRenderAttachments = !!issue?.attachment_count && issue?.attachment_count > 0;
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"use client";
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CircleDot, CopyPlus, XCircle } from "lucide-react";
|
||||
import { TIssue, TIssueRelationIdMap } from "@plane/types";
|
||||
import { Collapsible, RelatedIcon } from "@plane/ui";
|
||||
import { Collapsible } from "@plane/ui";
|
||||
// components
|
||||
import { RelationIssueList } from "@/components/issues";
|
||||
import { DeleteIssueModal } from "@/components/issues/delete-issue-modal";
|
||||
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// Plane-web
|
||||
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
// helper
|
||||
import { useRelationOperations } from "./helper";
|
||||
|
||||
@@ -20,35 +22,16 @@ type Props = {
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
const ISSUE_RELATION_OPTIONS = [
|
||||
{
|
||||
key: "blocked_by",
|
||||
label: "Blocked by",
|
||||
icon: (size: number) => <CircleDot size={size} />,
|
||||
className: "bg-red-500/20 text-red-700",
|
||||
},
|
||||
{
|
||||
key: "blocking",
|
||||
label: "Blocking",
|
||||
icon: (size: number) => <XCircle size={size} />,
|
||||
className: "bg-yellow-500/20 text-yellow-700",
|
||||
},
|
||||
{
|
||||
key: "relates_to",
|
||||
label: "Relates to",
|
||||
icon: (size: number) => <RelatedIcon height={size} width={size} />,
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
label: "Duplicate of",
|
||||
icon: (size: number) => <CopyPlus size={size} />,
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
},
|
||||
];
|
||||
|
||||
type TIssueCrudState = { toggle: boolean; issueId: string | undefined; issue: TIssue | undefined };
|
||||
|
||||
export type TRelationObject = {
|
||||
key: TIssueRelationTypes;
|
||||
label: string;
|
||||
className: string;
|
||||
icon: (size: number) => React.ReactElement;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// state
|
||||
@@ -96,17 +79,19 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
if (!relations) return null;
|
||||
|
||||
// map relations to array
|
||||
const relationsArray = Object.keys(relations).map((relationKey) => {
|
||||
const issueIds = relations[relationKey as keyof TIssueRelationIdMap];
|
||||
const issueRelationOption = ISSUE_RELATION_OPTIONS.find((option) => option.key === relationKey);
|
||||
return {
|
||||
relationKey: relationKey as keyof TIssueRelationIdMap,
|
||||
issueIds: issueIds,
|
||||
icon: issueRelationOption?.icon,
|
||||
label: issueRelationOption?.label,
|
||||
className: issueRelationOption?.className,
|
||||
};
|
||||
});
|
||||
const relationsArray = (Object.keys(relations) as TIssueRelationTypes[])
|
||||
.filter((relationKey) => !!ISSUE_RELATION_OPTIONS[relationKey])
|
||||
.map((relationKey) => {
|
||||
const issueIds = relations[relationKey];
|
||||
const issueRelationOption = ISSUE_RELATION_OPTIONS[relationKey];
|
||||
return {
|
||||
relationKey: relationKey,
|
||||
issueIds: issueIds,
|
||||
icon: issueRelationOption?.icon,
|
||||
label: issueRelationOption?.label,
|
||||
className: issueRelationOption?.className,
|
||||
};
|
||||
});
|
||||
|
||||
// filter out relations with no issues
|
||||
const filteredRelationsArray = relationsArray.filter((relation) => relation.issueIds.length > 0);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
import { useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { CircleDot, CopyPlus, XCircle } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { RelatedIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { ISSUE_DELETED, ISSUE_UPDATED } from "@/constants/event-tracker";
|
||||
// helper
|
||||
@@ -91,30 +90,3 @@ export const useRelationOperations = (): TRelationIssueOperations => {
|
||||
|
||||
return issueOperations;
|
||||
};
|
||||
|
||||
export const ISSUE_RELATION_OPTIONS = [
|
||||
{
|
||||
key: "blocked_by",
|
||||
label: "Blocked by",
|
||||
icon: (size: number) => <CircleDot size={size} />,
|
||||
className: "bg-red-500/20 text-red-700",
|
||||
},
|
||||
{
|
||||
key: "blocking",
|
||||
label: "Blocking",
|
||||
icon: (size: number) => <XCircle size={size} />,
|
||||
className: "bg-yellow-500/20 text-yellow-700",
|
||||
},
|
||||
{
|
||||
key: "relates_to",
|
||||
label: "Relates to",
|
||||
icon: (size: number) => <RelatedIcon height={size} width={size} />,
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
label: "Duplicate of",
|
||||
icon: (size: number) => <CopyPlus size={size} />,
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { TIssueRelationTypes } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// helper
|
||||
import { ISSUE_RELATION_OPTIONS } from "./helper";
|
||||
// Plane-web
|
||||
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
@@ -30,8 +30,14 @@ export const RelationActionButton: FC<Props> = observer((props) => {
|
||||
const customButtonElement = customButton ? <>{customButton}</> : <Plus className="h-4 w-4" />;
|
||||
|
||||
return (
|
||||
<CustomMenu customButton={customButtonElement} placement="bottom-start" disabled={disabled} closeOnSelect>
|
||||
{ISSUE_RELATION_OPTIONS.map((item, index) => (
|
||||
<CustomMenu
|
||||
customButton={customButtonElement}
|
||||
placement="bottom-start"
|
||||
disabled={disabled}
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{Object.values(ISSUE_RELATION_OPTIONS).map((item, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -17,12 +17,11 @@ export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
|
||||
const { isOpen, issueId, disabled } = props;
|
||||
// store hook
|
||||
const {
|
||||
relation: { getRelationsByIssueId },
|
||||
relation: { getRelationCountByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const issueRelations = getRelationsByIssueId(issueId);
|
||||
const relationsCount = Object.values(issueRelations ?? {}).reduce((acc, curr) => acc + curr.length, 0);
|
||||
const relationsCount = getRelationCountByIssueId(issueId);
|
||||
|
||||
// indicator element
|
||||
const indicatorElement = useMemo(
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TIssueRelationTypes } from "@plane/types";
|
||||
// hooks
|
||||
import { issueRelationObject } from "@/components/issues/issue-detail/relation-select";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
// Plane-web
|
||||
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
//
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
// component helpers
|
||||
// types
|
||||
|
||||
type TIssueRelationActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
@@ -23,7 +22,7 @@ export const IssueRelationActivity: FC<TIssueRelationActivity> = observer((props
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={activity.field ? issueRelationObject[activity.field as TIssueRelationTypes].icon(14) : <></>}
|
||||
icon={activity.field ? ISSUE_RELATION_OPTIONS[activity.field as TIssueRelationTypes].icon(14) : <></>}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -4,42 +4,19 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react";
|
||||
import { TIssueRelationTypes, ISearchIssueResponse } from "@plane/types";
|
||||
// hooks
|
||||
// Plane
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useIssues, useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
// ui
|
||||
// helpers
|
||||
// types
|
||||
|
||||
export type TRelationObject = { className: string; icon: (size: number) => React.ReactElement; placeholder: string };
|
||||
|
||||
export const issueRelationObject: Record<TIssueRelationTypes, TRelationObject> = {
|
||||
relates_to: {
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
icon: (size) => <RelatedIcon height={size} width={size} className="text-custom-text-200" />,
|
||||
placeholder: "Add related issues",
|
||||
},
|
||||
blocking: {
|
||||
className: "bg-yellow-500/20 text-yellow-700",
|
||||
icon: (size) => <XCircle size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
blocked_by: {
|
||||
className: "bg-red-500/20 text-red-700",
|
||||
icon: (size) => <CircleDot size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
duplicate: {
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
icon: (size) => <CopyPlus size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
};
|
||||
// Plane-web
|
||||
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
|
||||
type TIssueRelationSelect = {
|
||||
className?: string;
|
||||
@@ -129,7 +106,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
||||
return (
|
||||
<div
|
||||
key={relationIssueId}
|
||||
className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${issueRelationObject[relationKey].className}`}
|
||||
className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${ISSUE_RELATION_OPTIONS[relationKey].className}`}
|
||||
>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name} isMobile={isMobile}>
|
||||
<Link
|
||||
@@ -160,7 +137,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-custom-text-400">{issueRelationObject[relationKey].placeholder}</span>
|
||||
<span className="text-sm text-custom-text-400">{ISSUE_RELATION_OPTIONS[relationKey].placeholder}</span>
|
||||
)}
|
||||
{!disabled && (
|
||||
<span
|
||||
|
||||
@@ -4,19 +4,20 @@ import { useParams } from "next/navigation";
|
||||
// plane constants
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// hooks
|
||||
import { ChartDataType, GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "@/components/gantt-chart";
|
||||
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
||||
import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "@/components/gantt-chart";
|
||||
import { ETimeLineTypeType, TimeLineTypeContext } from "@/components/gantt-chart/contexts";
|
||||
import { QuickAddIssueRoot, IssueGanttBlock, GanttQuickAddIssueButton } from "@/components/issues";
|
||||
//constants
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { getIssueBlocksStructure } from "@/helpers/issue.helper";
|
||||
//hooks
|
||||
import { useIssues, useUserPermissions } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
|
||||
// plane web hooks
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
@@ -37,11 +38,12 @@ export type GanttStoreType =
|
||||
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
|
||||
const { viewId, isCompletedCycle = false } = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
|
||||
const storeType = useIssueStoreType() as GanttStoreType;
|
||||
const { issues, issuesFilter, issueMap } = useIssues(storeType);
|
||||
const { issues, issuesFilter } = useIssues(storeType);
|
||||
const { fetchIssues, fetchNextIssues, updateIssue, quickAddIssue } = useIssuesActions(storeType);
|
||||
const { initGantt } = useTimeLineChart(ETimeLineTypeType.ISSUE);
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
@@ -56,6 +58,10 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
fetchIssues("init-loader", { canGroup: false, perPageCount: 100 }, viewId);
|
||||
}, [fetchIssues, storeType, viewId]);
|
||||
|
||||
useEffect(() => {
|
||||
initGantt();
|
||||
}, []);
|
||||
|
||||
const issuesIds = (issues.groupedIssueIds?.[ALL_ISSUES] as string[]) ?? [];
|
||||
const nextPageResults = issues.getPaginationData(undefined, undefined)?.nextPageResults;
|
||||
|
||||
@@ -65,21 +71,6 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
fetchNextIssues();
|
||||
}, [fetchNextIssues]);
|
||||
|
||||
const getBlockById = useCallback(
|
||||
(id: string, currentViewData?: ChartDataType | undefined) => {
|
||||
const issue = issueMap[id];
|
||||
const block = getIssueBlocksStructure(issue);
|
||||
if (currentViewData) {
|
||||
return {
|
||||
...block,
|
||||
position: getMonthChartItemPositionWidthInMonth(currentViewData, block),
|
||||
};
|
||||
}
|
||||
return block;
|
||||
},
|
||||
[issueMap]
|
||||
);
|
||||
|
||||
const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
@@ -90,6 +81,23 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
};
|
||||
|
||||
const isAllowed = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
|
||||
const updateBlockDates = useCallback(
|
||||
(
|
||||
updates: {
|
||||
id: string;
|
||||
start_date?: string;
|
||||
target_date?: string;
|
||||
}[]
|
||||
) =>
|
||||
issues.updateIssueDates(workspaceSlug.toString(), projectId.toString(), updates).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error while updating Issue Dates, Please try again Later",
|
||||
});
|
||||
}),
|
||||
[issues]
|
||||
);
|
||||
|
||||
const quickAdd =
|
||||
enableIssueCreation && isAllowed && !isCompletedCycle ? (
|
||||
@@ -107,28 +115,30 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.GANTT}>
|
||||
<div className="h-full w-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blockIds={issuesIds}
|
||||
getBlockById={getBlockById}
|
||||
blockUpdateHandler={updateIssueBlockStructure}
|
||||
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />}
|
||||
sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
||||
enableAddBlock={isAllowed}
|
||||
enableSelection={isBulkOperationsEnabled && isAllowed}
|
||||
quickAdd={quickAdd}
|
||||
loadMoreBlocks={loadMoreIssues}
|
||||
canLoadMoreBlocks={nextPageResults}
|
||||
showAllBlocks
|
||||
/>
|
||||
</div>
|
||||
<TimeLineTypeContext.Provider value={ETimeLineTypeType.ISSUE}>
|
||||
<div className="h-full w-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blockIds={issuesIds}
|
||||
blockUpdateHandler={updateIssueBlockStructure}
|
||||
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />}
|
||||
sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
||||
enableAddBlock={isAllowed}
|
||||
enableSelection={isBulkOperationsEnabled && isAllowed}
|
||||
quickAdd={quickAdd}
|
||||
loadMoreBlocks={loadMoreIssues}
|
||||
canLoadMoreBlocks={nextPageResults}
|
||||
updateBlockDates={updateBlockDates}
|
||||
showAllBlocks
|
||||
/>
|
||||
</div>
|
||||
</TimeLineTypeContext.Provider>
|
||||
</IssueLayoutHOC>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { X, Pencil, Trash, Link as LinkIcon } from "lucide-react";
|
||||
import { TIssue, TIssueRelationTypes } from "@plane/types";
|
||||
// Plane
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { RelationIssueProperty } from "@/components/issues/relations";
|
||||
@@ -13,7 +14,8 @@ import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-red
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
//
|
||||
import { TRelationIssueOperations } from "../issue-detail-widgets/relations/helper";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TIssue, TIssueRelationTypes } from "@plane/types";
|
||||
// Plane
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
import { RelationIssueListItem } from "@/components/issues/relations";
|
||||
// types
|
||||
// Plane-web
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
//
|
||||
import { TRelationIssueOperations } from "../issue-detail-widgets/relations/helper";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// PLane
|
||||
import { IModule } from "@plane/types";
|
||||
// mobx store
|
||||
// components
|
||||
import { ChartDataType, GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "@/components/gantt-chart";
|
||||
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
IBlockUpdateDependencyData,
|
||||
ModuleGanttSidebar,
|
||||
} from "@/components/gantt-chart";
|
||||
import { ETimeLineTypeType, TimeLineTypeContext } from "@/components/gantt-chart/contexts";
|
||||
import { ModuleGanttBlock } from "@/components/modules";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useModule, useModuleFilter, useProject } from "@/hooks/store";
|
||||
// types
|
||||
|
||||
export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getFilteredModuleIds, getModuleById, updateModuleDetails } = useModule();
|
||||
const { getFilteredModuleIds, updateModuleDetails, getModuleById } = useModule();
|
||||
const { currentProjectDisplayFilters: displayFilters } = useModuleFilter();
|
||||
|
||||
// derived values
|
||||
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
|
||||
|
||||
@@ -30,47 +34,40 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
await updateModuleDetails(workspaceSlug.toString(), module.project_id, module.id, payload);
|
||||
};
|
||||
|
||||
const getBlockById = useCallback(
|
||||
(id: string, currentViewData?: ChartDataType | undefined) => {
|
||||
const projectModule = getModuleById(id);
|
||||
const updateBlockDates = async (blockUpdates: IBlockUpdateDependencyData[]) => {
|
||||
const blockUpdate = blockUpdates[0];
|
||||
|
||||
const block = {
|
||||
data: projectModule,
|
||||
id: projectModule?.id ?? "",
|
||||
sort_order: projectModule?.sort_order ?? 0,
|
||||
start_date: getDate(projectModule?.start_date),
|
||||
target_date: getDate(projectModule?.target_date),
|
||||
};
|
||||
if (currentViewData) {
|
||||
return {
|
||||
...block,
|
||||
position: getMonthChartItemPositionWidthInMonth(currentViewData, block),
|
||||
};
|
||||
}
|
||||
return block;
|
||||
},
|
||||
[getModuleById]
|
||||
);
|
||||
if (!blockUpdate) return;
|
||||
|
||||
const payload: Partial<IModule> = {};
|
||||
|
||||
if (blockUpdate.start_date) payload.start_date = blockUpdate.start_date;
|
||||
if (blockUpdate.target_date) payload.target_date = blockUpdate.target_date;
|
||||
|
||||
await updateModuleDetails(workspaceSlug.toString(), projectId.toString(), blockUpdate.id, payload);
|
||||
};
|
||||
|
||||
const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15;
|
||||
|
||||
if (!filteredModuleIds) return null;
|
||||
|
||||
return (
|
||||
<GanttChartRoot
|
||||
title="Modules"
|
||||
loaderTitle="Modules"
|
||||
blockIds={filteredModuleIds}
|
||||
getBlockById={getBlockById}
|
||||
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
|
||||
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
|
||||
blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={isAllowed && displayFilters?.order_by === "sort_order"}
|
||||
enableAddBlock={isAllowed}
|
||||
showAllBlocks
|
||||
/>
|
||||
<TimeLineTypeContext.Provider value={ETimeLineTypeType.MODULE}>
|
||||
<GanttChartRoot
|
||||
title="Modules"
|
||||
loaderTitle="Modules"
|
||||
blockIds={filteredModuleIds}
|
||||
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
|
||||
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
|
||||
blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={isAllowed && displayFilters?.order_by === "sort_order"}
|
||||
enableAddBlock={isAllowed}
|
||||
updateBlockDates={updateBlockDates}
|
||||
showAllBlocks
|
||||
/>
|
||||
</TimeLineTypeContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Row } from "@plane/ui";
|
||||
import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants";
|
||||
import { getRandomLength } from "../utils";
|
||||
|
||||
export const GanttLayoutLIstItem = () => (
|
||||
<div className="flex flex w-full items-center gap-4 px-6 " style={{ height: `${BLOCK_HEIGHT}px` }}>
|
||||
<div className="px-3 h-6 w-8 bg-custom-background-80 rounded" />
|
||||
<div className={`px-3 h-6 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const GanttLayoutLoader = () => (
|
||||
<div className="flex flex-col h-full overflow-x-auto animate-pulse">
|
||||
<div className="min-h-10 w-full border-b border-custom-border-200 ">
|
||||
|
||||
112
web/core/hooks/use-auto-scroller.tsx
Normal file
112
web/core/hooks/use-auto-scroller.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { RefObject, useEffect, useRef } from "react";
|
||||
|
||||
const SCROLL_BY = 1;
|
||||
|
||||
const AUTO_SCROLL_THRESHOLD = 15;
|
||||
const MAX_SPEED_THRESHOLD = 5;
|
||||
|
||||
export const useAutoScroller = (
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
shouldScroll = false,
|
||||
leftOffset = 0,
|
||||
topOffset = 0
|
||||
) => {
|
||||
const containerDimensions = useRef<DOMRect | undefined>();
|
||||
const intervalId = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
|
||||
|
||||
const clearRegisteredTimeout = () => {
|
||||
clearInterval(intervalId.current);
|
||||
};
|
||||
|
||||
const onDragEnd = () => clearRegisteredTimeout();
|
||||
|
||||
const handleAutoScroll = (e: MouseEvent) => {
|
||||
const rect = containerDimensions.current;
|
||||
clearInterval(intervalId.current);
|
||||
|
||||
if (!rect || !shouldScroll || (e.clientX === 0 && e.clientY === 0)) return;
|
||||
|
||||
const { left, top, width, height } = rect;
|
||||
|
||||
const mouseX = e.clientX - left - leftOffset;
|
||||
const mouseY = e.clientY - top - topOffset;
|
||||
|
||||
const currWidth = width - leftOffset;
|
||||
const currHeight = height - topOffset;
|
||||
|
||||
// Get Threshold in percentages
|
||||
const thresholdX = (currWidth / 100) * AUTO_SCROLL_THRESHOLD;
|
||||
const thresholdY = (currHeight / 100) * AUTO_SCROLL_THRESHOLD;
|
||||
const maxSpeedX = (currWidth / 100) * MAX_SPEED_THRESHOLD;
|
||||
const maxSpeedY = (currHeight / 100) * MAX_SPEED_THRESHOLD;
|
||||
|
||||
let scrollByX = 0,
|
||||
scrollByY = 0;
|
||||
|
||||
// Check mouse positions against thresholds
|
||||
if (mouseX < thresholdX) {
|
||||
scrollByX = -1 * SCROLL_BY;
|
||||
if (mouseX < maxSpeedX) {
|
||||
scrollByX *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseX > currWidth - thresholdX) {
|
||||
scrollByX = SCROLL_BY;
|
||||
if (mouseX > currWidth - maxSpeedX) {
|
||||
scrollByX *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseY < thresholdY) {
|
||||
scrollByY = -1 * SCROLL_BY;
|
||||
if (mouseX < maxSpeedY) {
|
||||
scrollByY *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseY > currHeight - thresholdY) {
|
||||
scrollByY = SCROLL_BY;
|
||||
if (mouseY > currHeight - maxSpeedY) {
|
||||
scrollByY *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
// if mouse position breaches threshold, then start to scroll
|
||||
if (scrollByX || scrollByY) {
|
||||
containerRef.current?.scrollBy(scrollByX, scrollByY);
|
||||
intervalId.current = setInterval(() => {
|
||||
containerRef.current?.scrollBy(scrollByX, scrollByY);
|
||||
}, 16);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const containerElement = containerRef.current;
|
||||
|
||||
if (!containerElement || !shouldScroll) return;
|
||||
|
||||
containerElement.addEventListener("drag", handleAutoScroll);
|
||||
containerElement.addEventListener("mousemove", handleAutoScroll);
|
||||
document.addEventListener("mouseup", onDragEnd);
|
||||
document.addEventListener("dragend", onDragEnd);
|
||||
|
||||
return () => {
|
||||
containerElement?.removeEventListener("drag", handleAutoScroll);
|
||||
containerElement?.removeEventListener("mousemove", handleAutoScroll);
|
||||
document.removeEventListener("mouseup", onDragEnd);
|
||||
document.removeEventListener("dragend", onDragEnd);
|
||||
};
|
||||
}, [shouldScroll, intervalId]);
|
||||
|
||||
useEffect(() => {
|
||||
const containerElement = containerRef.current;
|
||||
|
||||
if (!containerElement || !shouldScroll) {
|
||||
clearRegisteredTimeout();
|
||||
containerDimensions.current = undefined;
|
||||
}
|
||||
|
||||
containerDimensions.current = containerElement?.getBoundingClientRect();
|
||||
}, [shouldScroll]);
|
||||
};
|
||||
26
web/core/hooks/use-timeline-chart.ts
Normal file
26
web/core/hooks/use-timeline-chart.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
// types
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { IBaseTimelineStore } from "ee/store/timeline/base-timeline.store";
|
||||
import { ETimeLineTypeType, useTimeLineType } from "../components/gantt-chart/contexts";
|
||||
|
||||
export const useTimeLineChart = (timeLineType: ETimeLineTypeType): IBaseTimelineStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useTimeLineChart must be used within StoreProvider");
|
||||
|
||||
switch (timeLineType) {
|
||||
case ETimeLineTypeType.ISSUE:
|
||||
return context.timelineStore.issuesTimeLineStore;
|
||||
case ETimeLineTypeType.MODULE:
|
||||
return context.timelineStore.modulesTimeLineStore;
|
||||
}
|
||||
};
|
||||
|
||||
export const useTimeLineChartStore = () => {
|
||||
const timelineType = useTimeLineType();
|
||||
|
||||
if (!timelineType) throw new Error("useTimeLineChartStore must be used within TimeLineTypeContext");
|
||||
|
||||
return useTimeLineChart(timelineType);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
@@ -8,6 +8,7 @@ import useSWR from "swr";
|
||||
// components
|
||||
import { JoinProject } from "@/components/auth-screens";
|
||||
import { EmptyState, LogoSpinner } from "@/components/common";
|
||||
import { ETimeLineTypeType } from "@/components/gantt-chart/contexts";
|
||||
// hooks
|
||||
import {
|
||||
useCommandPalette,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
useProjectView,
|
||||
useUserPermissions,
|
||||
} from "@/hooks/store";
|
||||
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
|
||||
// local
|
||||
import { persistence } from "@/local-db/storage.sqlite";
|
||||
// plane web constants
|
||||
@@ -43,6 +45,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
const { loader, getProjectById, fetchProjectDetails } = useProject();
|
||||
const { fetchAllCycles } = useCycle();
|
||||
const { fetchModulesSlim, fetchModules } = useModule();
|
||||
const { initGantt } = useTimeLineChart(ETimeLineTypeType.MODULE);
|
||||
const { fetchViews } = useProjectView();
|
||||
const {
|
||||
project: { fetchProjectMembers },
|
||||
@@ -55,6 +58,11 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
|
||||
const projectMemberInfo = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()];
|
||||
|
||||
// Initialize module timeline chart
|
||||
useEffect(() => {
|
||||
initGantt();
|
||||
}, []);
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_SYNC_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||
workspaceSlug && projectId
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { startSpan } from "@sentry/nextjs";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
// types
|
||||
import type {
|
||||
IIssueDisplayProperties,
|
||||
@@ -36,8 +37,12 @@ export class IssueService extends APIService {
|
||||
queries?: any,
|
||||
config = {}
|
||||
): Promise<TIssuesResponse> {
|
||||
const path =
|
||||
(queries.expand as string)?.includes("issue_relation") && !queries.group_by
|
||||
? `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues-detail/`
|
||||
: `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`;
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`,
|
||||
path,
|
||||
{
|
||||
params: queries,
|
||||
},
|
||||
@@ -63,6 +68,9 @@ export class IssueService extends APIService {
|
||||
}
|
||||
|
||||
async getIssues(workspaceSlug: string, projectId: string, queries?: any, config = {}): Promise<TIssuesResponse> {
|
||||
if (!isEmpty(queries.expand as string) && !queries.group_by)
|
||||
return await this.getIssuesFromServer(workspaceSlug, projectId, queries, config);
|
||||
|
||||
const response = await startSpan({ name: "GET_ISSUES" }, async () => {
|
||||
const res = await persistence.getIssues(workspaceSlug, projectId, queries, config);
|
||||
return res;
|
||||
@@ -225,6 +233,18 @@ export class IssueService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateIssueDates(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
updates: { id: string; start_date?: string; target_date?: string }[]
|
||||
): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-dates/`, { updates })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async subIssues(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueSubIssues> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`)
|
||||
.then((response) => response?.data)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { TIssueRelation, TIssue, TIssueRelationTypes } from "@plane/types";
|
||||
import type { TIssueRelation, TIssue } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// Plane-web
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
// types
|
||||
|
||||
export class IssueRelationService extends APIService {
|
||||
constructor() {
|
||||
|
||||
@@ -29,12 +29,19 @@ import {
|
||||
TPaginationData,
|
||||
TBulkOperationsPayload,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import { IBlockUpdateDependencyData } from "@/components/gantt-chart";
|
||||
// constants
|
||||
import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
// helpers
|
||||
import { convertToISODateString } from "@/helpers/date-time.helper";
|
||||
// local-db
|
||||
import { updatePersistentLayer } from "@/local-db/utils/utils";
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
|
||||
import { ModuleService } from "@/services/module.service";
|
||||
//
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import {
|
||||
getDifference,
|
||||
@@ -45,9 +52,6 @@ import {
|
||||
getSubGroupIssueKeyActions,
|
||||
} from "./base-issues-utils";
|
||||
import { IBaseIssueFilterStore } from "./issue-filter-helper.store";
|
||||
// constants
|
||||
// helpers
|
||||
// services
|
||||
|
||||
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
|
||||
|
||||
@@ -109,6 +113,7 @@ export interface IBaseIssuesStore {
|
||||
addModuleIds: string[],
|
||||
removeModuleIds: string[]
|
||||
): Promise<void>;
|
||||
updateIssueDates(workspaceSlug: string, projectId: string, updates: IBlockUpdateDependencyData[]): Promise<void>;
|
||||
}
|
||||
|
||||
// This constant maps the group by keys to the respective issue property that the key relies on
|
||||
@@ -226,6 +231,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
issueUpdate: action,
|
||||
createDraftIssue: action,
|
||||
updateDraftIssue: action,
|
||||
updateIssueDates: action,
|
||||
issueQuickAdd: action.bound,
|
||||
removeIssue: action.bound,
|
||||
issueArchive: action.bound,
|
||||
@@ -476,6 +482,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
// fetch parent stats if required, to be handled in the Implemented class
|
||||
this.fetchParentStats(workspaceSlug, projectId, id);
|
||||
|
||||
this.rootIssueStore.issueDetail.relation.extractRelationsFromIssues(issueList);
|
||||
|
||||
// store Pagination options for next subsequent calls and data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, options);
|
||||
}
|
||||
@@ -501,6 +509,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
this.loader[getGroupKey(groupId, subGroupId)] = undefined;
|
||||
});
|
||||
|
||||
this.rootIssueStore.issueDetail.relation.extractRelationsFromIssues(issueList);
|
||||
|
||||
// store Pagination data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId);
|
||||
}
|
||||
@@ -792,6 +802,50 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
});
|
||||
};
|
||||
|
||||
async updateIssueDates(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
updates: { id: string; start_date?: string; target_date?: string }[]
|
||||
) {
|
||||
const issueDatesBeforeChange: { id: string; start_date?: string; target_date?: string }[] = [];
|
||||
try {
|
||||
const getIssueById = this.rootIssueStore.issues.getIssueById;
|
||||
runInAction(() => {
|
||||
for (const update of updates) {
|
||||
const dates: Partial<TIssue> = {};
|
||||
if (update.start_date) dates.start_date = update.start_date;
|
||||
if (update.target_date) dates.target_date = update.target_date;
|
||||
|
||||
const currIssue = getIssueById(update.id);
|
||||
|
||||
if (currIssue) {
|
||||
issueDatesBeforeChange.push({
|
||||
id: update.id,
|
||||
start_date: currIssue.start_date ?? undefined,
|
||||
target_date: currIssue.target_date ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
this.issueUpdate(workspaceSlug, projectId, update.id, dates, false);
|
||||
}
|
||||
});
|
||||
|
||||
await this.issueService.updateIssueDates(workspaceSlug, projectId, updates);
|
||||
} catch (e) {
|
||||
runInAction(() => {
|
||||
for (const update of issueDatesBeforeChange) {
|
||||
const dates: Partial<TIssue> = {};
|
||||
if (update.start_date) dates.start_date = update.start_date;
|
||||
if (update.target_date) dates.target_date = update.target_date;
|
||||
|
||||
this.issueUpdate(workspaceSlug, projectId, update.id, dates, false);
|
||||
}
|
||||
});
|
||||
console.error("error while updating Timeline dependencies");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to add issues to a particular Cycle
|
||||
* @param workspaceSlug
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user