From 8373f20944b3c17c780a4958beee2f635045248c Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:43:00 +0530 Subject: [PATCH 01/33] fix: issues n plus 1 (#1785) --- apiserver/plane/api/views/issue.py | 12 ++++++++++++ apiserver/plane/api/views/view.py | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 95e598dae..32d80ecf7 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -362,6 +362,12 @@ class UserWorkSpaceIssues(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) .filter(**filters) ) @@ -744,6 +750,12 @@ class SubIssuesEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) ) state_distribution = ( diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index 874bb94fb..32ba24c8b 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -19,6 +19,7 @@ from plane.db.models import ( IssueView, Issue, IssueViewFavorite, + IssueReaction, ) from plane.utils.issue_filters import issue_filters @@ -77,6 +78,12 @@ class ViewIssuesEndpoint(BaseAPIView): .select_related("parent") .prefetch_related("assignees") .prefetch_related("labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) ) serializer = IssueLiteSerializer(issues, many=True) From 2769a738989e582603091f6533b5540e0217d91d Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:43:43 +0530 Subject: [PATCH 02/33] remove: auto start date configuration (#1799) --- apiserver/plane/db/models/issue.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 2a4462942..1b85af797 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -108,11 +108,7 @@ class Issue(ProjectBaseModel): ~models.Q(name="Triage"), project=self.project ).first() self.state = random_state - if random_state.group == "started": - self.start_date = timezone.now().date() else: - if default_state.group == "started": - self.start_date = timezone.now().date() self.state = default_state except ImportError: pass @@ -127,8 +123,6 @@ class Issue(ProjectBaseModel): PageBlock.objects.filter(issue_id=self.id).filter().update( completed_at=timezone.now() ) - elif self.state.group == "started": - self.start_date = timezone.now().date() else: PageBlock.objects.filter(issue_id=self.id).filter().update( completed_at=None @@ -153,9 +147,6 @@ class Issue(ProjectBaseModel): if largest_sort_order is not None: self.sort_order = largest_sort_order + 10000 - # If adding it to started state - if self.state.group == "started": - self.start_date = timezone.now().date() # Strip the html tags using html parser self.description_stripped = ( None From 085cd1960e92e12e207803bdcc3df96f6c1ba229 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:44:20 +0530 Subject: [PATCH 03/33] fix: project members endpoint email (#1804) * fix: project members endpoint email * dev: cycle and module assignee display name --- apiserver/plane/api/serializers/__init__.py | 1 + apiserver/plane/api/serializers/cycle.py | 1 + apiserver/plane/api/views/cycle.py | 3 ++- apiserver/plane/api/views/module.py | 3 ++- apiserver/plane/api/views/project.py | 3 ++- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 683ed9670..34e235e38 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -18,6 +18,7 @@ from .project import ( ProjectFavoriteSerializer, ProjectLiteSerializer, ProjectMemberLiteSerializer, + ProjectMemberAdminSerializer, ) from .state import StateSerializer, StateLiteSerializer from .view import IssueViewSerializer, IssueViewFavoriteSerializer diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index 5b7bb7598..1abd63b7f 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -41,6 +41,7 @@ class CycleSerializer(BaseSerializer): { "avatar": assignee.avatar, "first_name": assignee.first_name, + "display_name": assignee.display_name, "id": assignee.id, } for issue_cycle in obj.issue_cycle.all() diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 268485b6e..885199f83 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -370,7 +370,8 @@ class CycleViewSet(BaseViewSet): .annotate(last_name=F("assignees__last_name")) .annotate(assignee_id=F("assignees__id")) .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar") + .annotate(display_name=F("assignees__display_name")) + .values("first_name", "last_name", "assignee_id", "avatar", "display_name") .annotate(total_issues=Count("assignee_id")) .annotate( completed_issues=Count( diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 2a7532ecf..7d60456a0 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -173,8 +173,9 @@ class ModuleViewSet(BaseViewSet): .annotate(first_name=F("assignees__first_name")) .annotate(last_name=F("assignees__last_name")) .annotate(assignee_id=F("assignees__id")) + .annotate(display_name=F("assignees__display_name")) .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar") + .values("first_name", "last_name", "assignee_id", "avatar", "display_name") .annotate(total_issues=Count("assignee_id")) .annotate( completed_issues=Count( diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 98484f74b..5911011b5 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -23,6 +23,7 @@ from plane.api.serializers import ( ProjectDetailSerializer, ProjectMemberInviteSerializer, ProjectFavoriteSerializer, + ProjectMemberAdminSerializer, ) from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission @@ -451,7 +452,7 @@ class UserProjectInvitationsViewset(BaseViewSet): class ProjectMemberViewSet(BaseViewSet): - serializer_class = ProjectMemberSerializer + serializer_class = ProjectMemberAdminSerializer model = ProjectMember permission_classes = [ ProjectBasePermission, From 11abd3cadf46a583f61c05e5f9436ea62976213f Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:45:04 +0530 Subject: [PATCH 04/33] fix: user id for default analytics (#1808) --- apiserver/plane/api/views/analytic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/api/views/analytic.py index 7d5786c19..feb766b46 100644 --- a/apiserver/plane/api/views/analytic.py +++ b/apiserver/plane/api/views/analytic.py @@ -243,21 +243,21 @@ class DefaultAnalyticsEndpoint(BaseAPIView): ) most_issue_created_user = ( queryset.exclude(created_by=None) - .values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name") + .values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id") .annotate(count=Count("id")) .order_by("-count") )[:5] most_issue_closed_user = ( queryset.filter(completed_at__isnull=False, assignees__isnull=False) - .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name") + .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id") .annotate(count=Count("id")) .order_by("-count") )[:5] pending_issue_user = ( queryset.filter(completed_at__isnull=True) - .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name") + .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id") .annotate(count=Count("id")) .order_by("-count") ) From 0a1483c4820786809f19f695fad21b47a421cf3d Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:45:51 +0530 Subject: [PATCH 05/33] chore: issue list date filters and properties (#1820) * dev: start date and target date validation and filter for null dated issues * dev: remove print logs * dev: issue property dates --- apiserver/plane/api/serializers/issue.py | 5 +++ apiserver/plane/api/views/issue.py | 1 - .../db/migrations/0043_auto_20230809_1645.py | 38 +++++++++++++++++++ apiserver/plane/db/models/workspace.py | 1 + apiserver/plane/utils/issue_filters.py | 10 ++++- 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 apiserver/plane/db/migrations/0043_auto_20230809_1645.py diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 770880ef0..b2fcb0a85 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -111,6 +111,11 @@ class IssueCreateSerializer(BaseSerializer): "updated_at", ] + def validate(self, data): + if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None): + raise serializers.ValidationError("Start date cannot exceed target date") + return data + def create(self, validated_data): blockers = validated_data.pop("blockers_list", None) assignees = validated_data.pop("assignees_list", None) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 32d80ecf7..077ff4023 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -169,7 +169,6 @@ class IssueViewSet(BaseViewSet): def list(self, request, slug, project_id): try: filters = issue_filters(request.query_params, "GET") - print(filters) # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", None] diff --git a/apiserver/plane/db/migrations/0043_auto_20230809_1645.py b/apiserver/plane/db/migrations/0043_auto_20230809_1645.py new file mode 100644 index 000000000..3dbbc44a2 --- /dev/null +++ b/apiserver/plane/db/migrations/0043_auto_20230809_1645.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.3 on 2023-08-09 11:15 + +from django.db import migrations + + +def update_user_issue_properties(apps, schema_editor): + IssuePropertyModel = apps.get_model("db", "IssueProperty") + updated_issue_properties = [] + for obj in IssuePropertyModel.objects.all(): + obj.properties["start_date"] = True + updated_issue_properties.append(obj) + IssuePropertyModel.objects.bulk_update( + updated_issue_properties, ["properties"], batch_size=100 + ) + + +def workspace_member_properties(apps, schema_editor): + WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember") + updated_workspace_members = [] + for obj in WorkspaceMemberModel.objects.all(): + obj.view_props["properties"]["start_date"] = True + obj.default_props["properties"]["start_date"] = True + updated_workspace_members.append(obj) + + WorkspaceMemberModel.objects.bulk_update( + updated_workspace_members, ["view_props", "default_props"], batch_size=100 + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("db", "0042_alter_analyticview_created_by_and_more"), + ] + + operations = [ + migrations.RunPython(update_user_issue_properties), + migrations.RunPython(workspace_member_properties), + ] diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 09db42002..48d8c9f2d 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -33,6 +33,7 @@ def get_default_props(): "estimate": True, "created_on": True, "updated_on": True, + "start_date": True, }, "showEmptyGroups": True, } diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index a7a946e60..1a4c5322d 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -292,9 +292,16 @@ def filter_subscribed_issues(params, filter, method): return filter +def filter_start_target_date_issues(params, filter, method): + start_target_date = params.get("start_target_date", "false") + if start_target_date == "true": + filter["target_date__isnull"] = False + filter["start_date__isnull"] = False + return filter + + def issue_filters(query_params, method): filter = dict() - print(query_params) ISSUE_FILTER = { "state": filter_state, @@ -318,6 +325,7 @@ def issue_filters(query_params, method): "inbox_status": filter_inbox_status, "sub_issue": filter_sub_issue_toggle, "subscriber": filter_subscribed_issues, + "start_target_date": filter_start_target_date_issues, } for key, value in ISSUE_FILTER.items(): From be86a7d38eac5035c7642b32c1d414174112cbdd Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:46:29 +0530 Subject: [PATCH 06/33] feat: project member role (#1828) --- apiserver/plane/api/serializers/project.py | 1 + apiserver/plane/api/views/project.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 643518daa..9e30976ec 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -94,6 +94,7 @@ class ProjectDetailSerializer(BaseSerializer): total_modules = serializers.IntegerField(read_only=True) is_member = serializers.BooleanField(read_only=True) sort_order = serializers.FloatField(read_only=True) + member_role = serializers.IntegerField(read_only=True) class Meta: model = Project diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 5911011b5..7387accd6 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -110,6 +110,12 @@ class ProjectViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .annotate( + member_role=ProjectMember.objects.filter( + project_id=OuterRef("pk"), + member_id=self.request.user.id, + ).values("role") + ) .distinct() ) From e06ee25800d308f5e459f230f754ff62fee8cee2 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:46:50 +0530 Subject: [PATCH 07/33] fix: project states create (#1830) --- apiserver/plane/api/views/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 7387accd6..123f2d29f 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -187,7 +187,7 @@ class ProjectViewSet(BaseViewSet): project_id=serializer.data["id"], member=request.user, role=20 ) - if serializer.data["project_lead"] is not None: + if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(request.user.id): ProjectMember.objects.create( project_id=serializer.data["id"], member_id=serializer.data["project_lead"], From edaeae1b6989ac4cb93f9046fbda278073c217f7 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:48:02 +0530 Subject: [PATCH 08/33] chore: sort order for cycle and modules (#1835) --- apiserver/plane/api/views/cycle.py | 3 +++ apiserver/plane/api/views/module.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 885199f83..a3d89fa81 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -165,6 +165,9 @@ class CycleViewSet(BaseViewSet): try: queryset = self.get_queryset() cycle_view = request.GET.get("cycle_view", "all") + order_by = request.GET.get("order_by", "sort_order") + + queryset = queryset.order_by(order_by) # All Cycles if cycle_view == "all": diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 7d60456a0..1cd741f84 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -53,6 +53,8 @@ class ModuleViewSet(BaseViewSet): ) def get_queryset(self): + order_by = self.request.GET.get("order_by", "sort_order") + subquery = ModuleFavorite.objects.filter( user=self.request.user, module_id=OuterRef("pk"), @@ -106,7 +108,7 @@ class ModuleViewSet(BaseViewSet): filter=Q(issue_module__issue__state__group="backlog"), ) ) - .order_by("-is_favorite", "name") + .order_by(order_by, "name") ) def perform_destroy(self, instance): From def10af1e2c60046c3be2eba90e3b6349fd430d1 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:48:30 +0530 Subject: [PATCH 09/33] fix: issue date filtering (#1834) --- apiserver/plane/utils/issue_filters.py | 35 +++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 1a4c5322d..34e1e8203 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -124,10 +124,11 @@ def filter_created_at(params, filter, method): else: if params.get("created_at", None) and len(params.get("created_at")): for query in params.get("created_at"): - if query.get("timeline", "after") == "after": - filter["created_at__date__gte"] = query.get("datetime") + created_at_query = query.split(";") + if len(created_at_query) == 2 and "after" in created_at_query: + filter["created_at__date__gte"] = created_at_query[0] else: - filter["created_at__date__lte"] = query.get("datetime") + filter["created_at__date__lte"] = created_at_query[0] return filter @@ -144,10 +145,11 @@ def filter_updated_at(params, filter, method): else: if params.get("updated_at", None) and len(params.get("updated_at")): for query in params.get("updated_at"): - if query.get("timeline", "after") == "after": - filter["updated_at__date__gte"] = query.get("datetime") + updated_at_query = query.split(";") + if len(updated_at_query) == 2 and "after" in updated_at_query: + filter["updated_at__date__gte"] = updated_at_query[0] else: - filter["updated_at__date__lte"] = query.get("datetime") + filter["updated_at__date__lte"] = updated_at_query[0] return filter @@ -164,10 +166,11 @@ def filter_start_date(params, filter, method): else: if params.get("start_date", None) and len(params.get("start_date")): for query in params.get("start_date"): - if query.get("timeline", "after") == "after": - filter["start_date__gte"] = query.get("datetime") + start_date_query = query.split(";") + if len(start_date_query) == 2 and "after" in start_date_query: + filter["start_date__gte"] = start_date_query[0] else: - filter["start_date__lte"] = query.get("datetime") + filter["start_date__lte"] = start_date_query[0] return filter @@ -184,10 +187,11 @@ def filter_target_date(params, filter, method): else: if params.get("target_date", None) and len(params.get("target_date")): for query in params.get("target_date"): - if query.get("timeline", "after") == "after": - filter["target_date__gt"] = query.get("datetime") + target_date_query = query.split(";") + if len(target_date_query) == 2 and "after" in target_date_query: + filter["target_date__gt"] = target_date_query[0] else: - filter["target_date__lt"] = query.get("datetime") + filter["target_date__lt"] = target_date_query[0] return filter @@ -205,10 +209,11 @@ def filter_completed_at(params, filter, method): else: if params.get("completed_at", None) and len(params.get("completed_at")): for query in params.get("completed_at"): - if query.get("timeline", "after") == "after": - filter["completed_at__date__gte"] = query.get("datetime") + completed_at_query = query.split(";") + if len(completed_at_query) == 2 and "after" in completed_at_query: + filter["completed_at__date__gte"] = completed_at_query[0] else: - filter["completed_at__lte"] = query.get("datetime") + filter["completed_at__lte"] = completed_at_query[0] return filter From 289e81d6eb3d8d8ba1e33225971380c1fb3c219a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:48:52 +0530 Subject: [PATCH 10/33] chore: issue activity user redirection added (#1805) * feat: issue activity user redirection added * feat: analytics page user redirection --- .../scope-and-demand/leaderboard.tsx | 11 +++++++--- .../scope-and-demand/scope-and-demand.tsx | 4 ++++ apps/app/components/core/activity.tsx | 22 +++++++++++++++---- apps/app/types/analytics.d.ts | 2 ++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx index c22f60aa9..156c09d4c 100644 --- a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx +++ b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx @@ -5,18 +5,23 @@ type Props = { firstName: string; lastName: string; count: number; + id: string; }[]; title: string; + workspaceSlug: string; }; -export const AnalyticsLeaderboard: React.FC = ({ users, title }) => ( +export const AnalyticsLeaderboard: React.FC = ({ users, title, workspaceSlug }) => (
{title}
{users.length > 0 ? (
{users.map((user) => ( -
@@ -38,7 +43,7 @@ export const AnalyticsLeaderboard: React.FC = ({ users, title }) => (
{user.count} -
+ ))}
) : ( diff --git a/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx b/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx index c4acf8f45..7f40ee79a 100644 --- a/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx +++ b/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx @@ -60,8 +60,10 @@ export const ScopeAndDemand: React.FC = ({ fullScreen = true }) => { lastName: user?.created_by__last_name, display_name: user?.created_by__display_name, count: user?.count, + id: user?.created_by__id, }))} title="Most issues created" + workspaceSlug={workspaceSlug?.toString() ?? ""} /> ({ @@ -70,8 +72,10 @@ export const ScopeAndDemand: React.FC = ({ fullScreen = true }) => { lastName: user?.assignees__last_name, display_name: user?.assignees__display_name, count: user?.count, + id: user?.assignees__id, }))} title="Most issues closed" + workspaceSlug={workspaceSlug?.toString() ?? ""} />
diff --git a/apps/app/components/core/activity.tsx b/apps/app/components/core/activity.tsx index 833f4fd16..7ddf9c33c 100644 --- a/apps/app/components/core/activity.tsx +++ b/apps/app/components/core/activity.tsx @@ -35,6 +35,22 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => { ); }; +const UserLink = ({ activity }: { activity: IIssueActivity }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( + + {activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value} + + ); +}; + const activityDetails: { [key: string]: { message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode; @@ -46,8 +62,7 @@ const activityDetails: { if (activity.old_value === "") return ( <> - added a new assignee{" "} - {activity.new_value} + added a new assignee {showIssue && ( <> {" "} @@ -60,8 +75,7 @@ const activityDetails: { else return ( <> - removed the assignee{" "} - {activity.old_value} + removed the assignee {showIssue && ( <> {" "} diff --git a/apps/app/types/analytics.d.ts b/apps/app/types/analytics.d.ts index 94afb2c02..651596f19 100644 --- a/apps/app/types/analytics.d.ts +++ b/apps/app/types/analytics.d.ts @@ -69,6 +69,7 @@ export interface IDefaultAnalyticsUser { assignees__first_name: string; assignees__last_name: string; assignees__display_name: string; + assignees__id: string; count: number; } @@ -80,6 +81,7 @@ export interface IDefaultAnalyticsResponse { created_by__first_name: string; created_by__last_name: string; created_by__display_name: string; + created_by__id: string; count: number; }[]; open_estimate_sum: number; From 5c964d144a7ac739b13c2c146c949b78f05ca6dd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:50:05 +0530 Subject: [PATCH 11/33] style: ui improvement (#1806) * style: kanban board theming * style: assignee ui revamp * style: kanban board header, label , priority , state and avatar ui revamp * style: kanban card state dropdown * style: sidebar profile dropdown * style: sidebar dropdown icon * style: sidebar workspace dropdown * style: assignee select component * fix: state select * style: consistent app header button --- .../core/filters/issues-view-filter.tsx | 2 +- .../core/views/board-view/board-header.tsx | 17 ++++----- .../core/views/board-view/single-issue.tsx | 7 ++-- apps/app/components/issues/label.tsx | 4 +- .../my-issues/my-issues-view-options.tsx | 2 +- .../issues/view-select/assignee.tsx | 9 +++-- .../issues/view-select/priority.tsx | 12 ++---- .../components/issues/view-select/state.tsx | 5 ++- .../profile/profile-issues-view-options.tsx | 2 +- apps/app/components/ui/avatar.tsx | 38 +++++++++++++------ .../components/workspace/sidebar-dropdown.tsx | 28 +++++++------- .../projects/[projectId]/issues/index.tsx | 5 ++- 12 files changed, 71 insertions(+), 60 deletions(-) diff --git a/apps/app/components/core/filters/issues-view-filter.tsx b/apps/app/components/core/filters/issues-view-filter.tsx index ca2ea59f7..0bdd30563 100644 --- a/apps/app/components/core/filters/issues-view-filter.tsx +++ b/apps/app/components/core/filters/issues-view-filter.tsx @@ -155,7 +155,7 @@ export const IssuesFilterView: React.FC = () => { {({ open }) => ( <> = ({ >
@@ -155,11 +155,7 @@ export const BoardHeader: React.FC = ({ > {getGroupTitle()} - + {groupedIssues?.[groupTitle].length ?? 0}
@@ -174,9 +170,12 @@ export const BoardHeader: React.FC = ({ }} > {isCollapsed ? ( - + ) : ( - + )} {!disableUserActions && selectedGroup !== "created_by" && ( diff --git a/apps/app/components/core/views/board-view/single-issue.tsx b/apps/app/components/core/views/board-view/single-issue.tsx index 5aaa71407..689020bb5 100644 --- a/apps/app/components/core/views/board-view/single-issue.tsx +++ b/apps/app/components/core/views/board-view/single-issue.tsx @@ -232,7 +232,7 @@ export const SingleBoardIssue: React.FC = ({
= ({ {issue.project_detail.identifier}-{issue.sequence_id}
)} -
{issue.name}
+
{issue.name}
-
+
{properties.priority && ( = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} + customButton user={user} selfPositioned /> diff --git a/apps/app/components/issues/label.tsx b/apps/app/components/issues/label.tsx index f3a7be9dd..c5a48d0ad 100644 --- a/apps/app/components/issues/label.tsx +++ b/apps/app/components/issues/label.tsx @@ -18,7 +18,7 @@ export const ViewIssueLabel: React.FC = ({ issue, maxRender = 1 }) => ( {issue.label_details.map((label, index) => (
@@ -35,7 +35,7 @@ export const ViewIssueLabel: React.FC = ({ issue, maxRender = 1 }) => ( ))} ) : ( -
+
{ {({ open }) => ( <> = ({ > {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? (
- +
) : ( -
- +
+
)}
@@ -87,6 +87,7 @@ export const ViewAssigneeSelect: React.FC = ({ return ( { const newData = issue.assignees ?? []; diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 5d3bcd089..f9872729c 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -67,14 +67,8 @@ export const ViewPrioritySelect: React.FC = ({ noBorder ? "" : issue.priority === "urgent" - ? "border-red-500/20 bg-red-500/20" - : issue.priority === "high" - ? "border-orange-500/20 bg-orange-500/20" - : issue.priority === "medium" - ? "border-yellow-500/20 bg-yellow-500/20" - : issue.priority === "low" - ? "border-green-500/20 bg-green-500/20" - : "border-custom-border-200 bg-custom-background-80" + ? "border-red-500/20 bg-red-500" + : "border-custom-border-300 bg-custom-background-100" } items-center`} > = ({ issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", `text-sm ${ issue.priority === "urgent" - ? "text-red-500" + ? "text-white" : issue.priority === "high" ? "text-orange-500" : issue.priority === "medium" diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 7f5844697..460a11272 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -74,9 +74,9 @@ export const ViewStateSelect: React.FC = ({ position={tooltipPosition} >
- + {selectedOption && - getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)} + getStateGroupIcon(selectedOption.group, "14", "14", selectedOption.color)} {selectedOption?.name ?? "State"}
@@ -131,6 +131,7 @@ export const ViewStateSelect: React.FC = ({ disabled={isNotAllowed} onOpen={() => setFetchStates(true)} noChevron + selfPositioned={selfPositioned} /> ); }; diff --git a/apps/app/components/profile/profile-issues-view-options.tsx b/apps/app/components/profile/profile-issues-view-options.tsx index 94ba6ef17..904e1ea65 100644 --- a/apps/app/components/profile/profile-issues-view-options.tsx +++ b/apps/app/components/profile/profile-issues-view-options.tsx @@ -146,7 +146,7 @@ export const ProfileIssuesViewOptions: React.FC = () => { {({ open }) => ( <> = ({ user, index, - height = "20px", - width = "20px", + height = "24px", + width = "24px", fontSize = "12px", }) => (
= ({ > {user && user.avatar && user.avatar !== "" ? (
= ({ > {user.display_name}
) : (
= ({ users, userIds, - length = 5, + length = 3, showLength = true, }) => { const router = useRouter(); @@ -88,7 +92,7 @@ export const AssigneesList: React.FC = ({ if ((users && users.length === 0) || (userIds && userIds.length === 0)) return ( -
+
No user
); @@ -100,7 +104,14 @@ export const AssigneesList: React.FC = ({ {users.slice(0, length).map((user, index) => ( ))} - {users.length > length ? +{users.length - length} : null} + {users.length > length ? ( +
+
+ + {users.length - length} +
+
+ ) : null} )} {userIds && ( @@ -112,7 +123,12 @@ export const AssigneesList: React.FC = ({ })} {showLength ? ( userIds.length > length ? ( - +{userIds.length - length} +
+
+ + {userIds.length - length} +
+
) : null ) : ( "" diff --git a/apps/app/components/workspace/sidebar-dropdown.tsx b/apps/app/components/workspace/sidebar-dropdown.tsx index e1c92282b..807a7de8b 100644 --- a/apps/app/components/workspace/sidebar-dropdown.tsx +++ b/apps/app/components/workspace/sidebar-dropdown.tsx @@ -146,13 +146,12 @@ export const WorkspaceSidebarDropdown = () => { >
-
{user?.display_name}
- Workspace + Workspace {workspaces ? ( -
+
{workspaces.length > 0 ? ( workspaces.map((workspace) => ( @@ -160,7 +159,7 @@ export const WorkspaceSidebarDropdown = () => { +
)} - - {orderedJoinedProjects.map((project, index) => ( - - {(provided, snapshot) => ( -
- handleDeleteProject(project)} - handleCopyText={() => handleCopyText(project.id)} - /> -
- )} -
- ))} -
+ + + {orderedJoinedProjects.map((project, index) => ( + + {(provided, snapshot) => ( +
+ handleDeleteProject(project)} + handleCopyText={() => handleCopyText(project.id)} + /> +
+ )} +
+ ))} +
+
{provided.placeholder} )} @@ -239,43 +289,7 @@ export const ProjectSidebarList: FC = () => { )} - {otherProjects && otherProjects.length > 0 && ( - p.id === projectId) ? true : false} - > - {({ open }) => ( - <> - {!store?.theme?.sidebarCollapsed && ( - - Other Projects - - - )} - - {otherProjects?.map((project, index) => ( - handleDeleteProject(project)} - handleCopyText={() => handleCopyText(project.id)} - shortContextMenu - /> - ))} - - - )} - - )} + {allProjects && allProjects.length === 0 && (
)} + router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)} + > +
+ + Settings +
+
)}
diff --git a/apps/app/components/workspace/index.ts b/apps/app/components/workspace/index.ts index d40b9d58e..8e354a718 100644 --- a/apps/app/components/workspace/index.ts +++ b/apps/app/components/workspace/index.ts @@ -9,3 +9,4 @@ export * from "./issues-stats"; export * from "./settings-header"; export * from "./sidebar-dropdown"; export * from "./sidebar-menu"; +export * from "./sidebar-quick-action"; diff --git a/apps/app/components/workspace/sidebar-menu.tsx b/apps/app/components/workspace/sidebar-menu.tsx index 3e4d5472b..59882284a 100644 --- a/apps/app/components/workspace/sidebar-menu.tsx +++ b/apps/app/components/workspace/sidebar-menu.tsx @@ -51,7 +51,7 @@ export const WorkspaceSidebarMenu = () => { const { collapsed: sidebarCollapse } = useTheme(); return ( -
+
{workspaceLinks(workspaceSlug as string).map((link, index) => { const isActive = link.name === "Settings" diff --git a/apps/app/components/workspace/sidebar-quick-action.tsx b/apps/app/components/workspace/sidebar-quick-action.tsx new file mode 100644 index 000000000..781ef353f --- /dev/null +++ b/apps/app/components/workspace/sidebar-quick-action.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +// ui +import { Icon } from "components/ui"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; + +export const WorkspaceSidebarQuickAction = () => { + const store: any = useMobxStore(); + + return ( +
+ + + +
+ ); +}; diff --git a/apps/app/layouts/app-layout/app-sidebar.tsx b/apps/app/layouts/app-layout/app-sidebar.tsx index 60ef645cf..7143d9cad 100644 --- a/apps/app/layouts/app-layout/app-sidebar.tsx +++ b/apps/app/layouts/app-layout/app-sidebar.tsx @@ -5,6 +5,7 @@ import { WorkspaceHelpSection, WorkspaceSidebarDropdown, WorkspaceSidebarMenu, + WorkspaceSidebarQuickAction, } from "components/workspace"; import { ProjectSidebarList } from "components/project"; // mobx react lite @@ -30,6 +31,7 @@ const Sidebar: React.FC = observer(({ toggleSidebar, setToggleSide >
+ From e2b5657c3e139f1d86094a008549725f6a64c94e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:56:26 +0530 Subject: [PATCH 13/33] chore: profile cover photo (#1836) --- .../components/core/image-picker-popover.tsx | 4 +- .../[workspaceSlug]/me/profile/index.tsx | 38 ++++++++++++++++++- apps/app/types/users.d.ts | 1 + 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/app/components/core/image-picker-popover.tsx b/apps/app/components/core/image-picker-popover.tsx index 7144bd01f..402cba022 100644 --- a/apps/app/components/core/image-picker-popover.tsx +++ b/apps/app/components/core/image-picker-popover.tsx @@ -27,8 +27,8 @@ const unsplashEnabled = const tabOptions = [ { - key: "unsplash", - title: "Unsplash", + key: "images", + title: "Images", }, { key: "upload", diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index 71c9cd094..c91776f55 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -12,7 +12,7 @@ import useToast from "hooks/use-toast"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import SettingsNavbar from "layouts/settings-navbar"; // components -import { ImageUploadModal } from "components/core"; +import { ImagePickerPopover, ImageUploadModal } from "components/core"; // ui import { CustomSelect, DangerButton, Input, SecondaryButton, Spinner } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; @@ -26,6 +26,7 @@ import { USER_ROLES } from "constants/workspace"; const defaultValues: Partial = { avatar: "", + cover_image: "", first_name: "", last_name: "", email: "", @@ -68,6 +69,7 @@ const Profile: NextPage = () => { first_name: formData.first_name, last_name: formData.last_name, avatar: formData.avatar, + cover_image: formData.cover_image, role: formData.role, display_name: formData.display_name, }; @@ -202,6 +204,40 @@ const Profile: NextPage = () => {
+
+
+

Cover Photo

+

+ Select your cover photo from the given library. +

+
+
+
+
+ {myProfile?.name +
+ { + setValue("cover_image", imageUrl); + }} + value={ + watch("cover_image") ?? + "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab" + } + /> +
+
+
+
+

Full Name

diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts index 68f0b0c78..c23512ecf 100644 --- a/apps/app/types/users.d.ts +++ b/apps/app/types/users.d.ts @@ -11,6 +11,7 @@ import { export interface IUser { avatar: string; + cover_image: string | null; created_at: readonly Date; created_location: readonly string; date_joined: readonly Date; From 762ef422cab294d0b785ace11dce5ba78c30dd1b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:57:11 +0530 Subject: [PATCH 14/33] chore: project search added in project list page (#1837) --- .../pages/[workspaceSlug]/projects/index.tsx | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/apps/app/pages/[workspaceSlug]/projects/index.tsx b/apps/app/pages/[workspaceSlug]/projects/index.tsx index 6c60d4abd..4dd6a234e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/index.tsx @@ -16,7 +16,7 @@ import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { JoinProjectModal } from "components/project/join-project-modal"; import { DeleteProjectModal, SingleProjectCard } from "components/project"; // ui -import { EmptyState, Loader, PrimaryButton } from "components/ui"; +import { EmptyState, Icon, Loader, PrimaryButton } from "components/ui"; import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; @@ -34,6 +34,8 @@ const ProjectsPage: NextPage = () => { const router = useRouter(); const { workspaceSlug } = router.query; + const [query, setQuery] = useState(""); + const { user } = useUserAuth(); // context data const { activeWorkspace } = useWorkspaces(); @@ -42,6 +44,15 @@ const ProjectsPage: NextPage = () => { const [deleteProject, setDeleteProject] = useState(null); const [selectedProjectToJoin, setSelectedProjectToJoin] = useState(null); + const filteredProjectList = + query === "" + ? projects + : projects?.filter( + (project) => + project.name.toLowerCase().includes(query.toLowerCase()) || + project.identifier.toLowerCase().includes(query.toLowerCase()) + ); + return ( { } right={ - { - const e = new KeyboardEvent("keydown", { key: "p" }); - document.dispatchEvent(e); - }} - > - - Add Project - +
+
+ + setQuery(e.target.value)} + placeholder="Search" + /> +
+ + { + const e = new KeyboardEvent("keydown", { key: "p" }); + document.dispatchEvent(e); + }} + > + + Add Project + +
} > { data={projects?.find((item) => item.id === deleteProject) ?? null} user={user} /> - {projects ? ( + {filteredProjectList ? (
- {projects.length > 0 ? ( + {filteredProjectList.length > 0 ? (
- {projects.map((project) => ( + {filteredProjectList.map((project) => ( Date: Fri, 11 Aug 2023 15:59:13 +0530 Subject: [PATCH 15/33] chore: gantt chart resizable blocks, y-axis drag and drop (#1810) * chore: gantt chart resizable blocks * chore: right scroll added * chore: left scroll added * fix: build errors * chore: remove unnecessary console logs * chore: add block type and remove info toggle * feat: gantt chart blocks y-axis drag and drop * chore: disable drag flag * fix: y-axis drag mutation * fix: scroll container undefined error * fix: negative scroll * fix: negative scroll * style: blocks tooltip consistency --- .../core/filters/issues-view-filter.tsx | 229 +++++++-------- .../cycles/cycles-list-gantt-chart.tsx | 103 ++++--- .../cycles/cycles-list/all-cycles-list.tsx | 4 +- .../cycles-list/completed-cycles-list.tsx | 4 +- .../cycles/cycles-list/draft-cycles-list.tsx | 4 +- .../cycles-list/upcoming-cycles-list.tsx | 4 +- apps/app/components/cycles/cycles-view.tsx | 7 +- apps/app/components/cycles/gantt-chart.tsx | 98 ++----- .../components/gantt-chart/blocks/block.tsx | 103 +++++++ .../gantt-chart/blocks/blocks-display.tsx | 178 ++++++++++++ .../components/gantt-chart/blocks/index.ts | 2 + .../components/gantt-chart/blocks/index.tsx | 82 ------ .../components/gantt-chart/chart/bi-week.tsx | 2 +- apps/app/components/gantt-chart/chart/day.tsx | 2 +- .../components/gantt-chart/chart/hours.tsx | 2 +- .../components/gantt-chart/chart/index.tsx | 57 ++-- .../components/gantt-chart/chart/month.tsx | 43 +-- .../components/gantt-chart/chart/quarter.tsx | 2 +- .../app/components/gantt-chart/chart/week.tsx | 2 +- .../app/components/gantt-chart/chart/year.tsx | 2 +- .../gantt-chart/helpers/block-structure.tsx | 14 + .../gantt-chart/helpers/draggable.tsx | 261 ++++++++++-------- .../components/gantt-chart/helpers/index.ts | 1 + .../gantt-chart/hooks/block-update.tsx | 43 +++ apps/app/components/gantt-chart/index.ts | 4 + apps/app/components/gantt-chart/root.tsx | 15 +- .../app/components/gantt-chart/types/index.ts | 24 +- .../gantt-chart/views/month-view.ts | 40 ++- .../components/gantt-chart/views/year-view.ts | 2 - apps/app/components/issues/gantt-chart.tsx | 96 ++----- apps/app/components/issues/modal.tsx | 6 +- apps/app/components/modules/gantt-chart.tsx | 94 ++----- .../modules/modules-list-gantt-chart.tsx | 99 ++++--- apps/app/components/project/sidebar-list.tsx | 4 +- apps/app/components/views/gantt-chart.tsx | 90 ++---- apps/app/constants/fetch-keys.ts | 14 +- .../hooks/gantt-chart/cycle-issues-view.tsx | 18 +- apps/app/hooks/gantt-chart/issue-view.tsx | 16 +- .../hooks/gantt-chart/module-issues-view.tsx | 18 +- .../hooks/gantt-chart/view-issues-view.tsx | 13 +- apps/app/layouts/app-layout/app-sidebar.tsx | 1 + .../projects/[projectId]/modules/index.tsx | 6 +- apps/app/services/modules.service.ts | 4 +- apps/app/types/cycles.d.ts | 1 + apps/app/types/modules.d.ts | 1 + yarn.lock | 142 +++++----- 46 files changed, 1109 insertions(+), 848 deletions(-) create mode 100644 apps/app/components/gantt-chart/blocks/block.tsx create mode 100644 apps/app/components/gantt-chart/blocks/blocks-display.tsx create mode 100644 apps/app/components/gantt-chart/blocks/index.ts delete mode 100644 apps/app/components/gantt-chart/blocks/index.tsx create mode 100644 apps/app/components/gantt-chart/helpers/block-structure.tsx create mode 100644 apps/app/components/gantt-chart/helpers/index.ts create mode 100644 apps/app/components/gantt-chart/hooks/block-update.tsx diff --git a/apps/app/components/core/filters/issues-view-filter.tsx b/apps/app/components/core/filters/issues-view-filter.tsx index 0bdd30563..2fa80c975 100644 --- a/apps/app/components/core/filters/issues-view-filter.tsx +++ b/apps/app/components/core/filters/issues-view-filter.tsx @@ -113,44 +113,46 @@ export const IssuesFilterView: React.FC = () => { ))}
)} - { - const key = option.key as keyof typeof filters; + {issueView !== "gantt_chart" && ( + { + const key = option.key as keyof typeof filters; - if (key === "target_date") { - const valueExists = checkIfArraysHaveSameElements( - filters.target_date ?? [], - option.value - ); - - setFilters({ - target_date: valueExists ? null : option.value, - }); - } else { - const valueExists = filters[key]?.includes(option.value); - - if (valueExists) - setFilters( - { - [option.key]: ((filters[key] ?? []) as any[])?.filter( - (val) => val !== option.value - ), - }, - !Boolean(viewId) + if (key === "target_date") { + const valueExists = checkIfArraysHaveSameElements( + filters.target_date ?? [], + option.value ); - else - setFilters( - { - [option.key]: [...((filters[key] ?? []) as any[]), option.value], - }, - !Boolean(viewId) - ); - } - }} - direction="left" - height="rg" - /> + + setFilters({ + target_date: valueExists ? null : option.value, + }); + } else { + const valueExists = filters[key]?.includes(option.value); + + if (valueExists) + setFilters( + { + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }, + !Boolean(viewId) + ); + else + setFilters( + { + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }, + !Boolean(viewId) + ); + } + }} + direction="left" + height="rg" + /> + )} {({ open }) => ( <> @@ -177,8 +179,9 @@ export const IssuesFilterView: React.FC = () => {
- {issueView !== "calendar" && issueView !== "spreadsheet" && ( - <> + {issueView !== "calendar" && + issueView !== "spreadsheet" && + issueView !== "gantt_chart" && (

Group by

@@ -206,34 +209,34 @@ export const IssuesFilterView: React.FC = () => {
-
-

Order by

-
- option.key === orderBy)?.name ?? - "Select" - } - className="!w-full" - buttonClassName="w-full" - > - {ORDER_BY_OPTIONS.map((option) => - groupByProperty === "priority" && - option.key === "priority" ? null : ( - { - setOrderBy(option.key); - }} - > - {option.name} - - ) - )} - -
+ )} + {issueView !== "calendar" && issueView !== "spreadsheet" && ( +
+

Order by

+
+ option.key === orderBy)?.name ?? + "Select" + } + className="!w-full" + buttonClassName="w-full" + > + {ORDER_BY_OPTIONS.map((option) => + groupByProperty === "priority" && option.key === "priority" ? null : ( + { + setOrderBy(option.key); + }} + > + {option.name} + + ) + )} +
- +
)}

Issue type

@@ -263,16 +266,19 @@ export const IssuesFilterView: React.FC = () => {
{issueView !== "calendar" && issueView !== "spreadsheet" && ( - <> -
-

Show sub-issues

-
- setShowSubIssues(!showSubIssues)} - /> -
+
+

Show sub-issues

+
+ setShowSubIssues(!showSubIssues)} + />
+
+ )} + {issueView !== "calendar" && + issueView !== "spreadsheet" && + issueView !== "gantt_chart" && (

Show empty states

@@ -282,6 +288,10 @@ export const IssuesFilterView: React.FC = () => { />
+ )} + {issueView !== "calendar" && + issueView !== "spreadsheet" && + issueView !== "gantt_chart" && (
- - )} + )}
-
-

Display Properties

-
- {Object.keys(properties).map((key) => { - if (key === "estimate" && !isEstimateActive) return null; + {issueView !== "gantt_chart" && ( +
+

Display Properties

+
+ {Object.keys(properties).map((key) => { + if (key === "estimate" && !isEstimateActive) return null; - if ( - issueView === "spreadsheet" && - (key === "attachment_count" || - key === "link" || - key === "sub_issue_count") - ) - return null; + if ( + issueView === "spreadsheet" && + (key === "attachment_count" || + key === "link" || + key === "sub_issue_count") + ) + return null; - if ( - issueView !== "spreadsheet" && - (key === "created_on" || key === "updated_on") - ) - return null; + if ( + issueView !== "spreadsheet" && + (key === "created_on" || key === "updated_on") + ) + return null; - return ( - - ); - })} + return ( + + ); + })} +
-
+ )}
diff --git a/apps/app/components/cycles/cycles-list-gantt-chart.tsx b/apps/app/components/cycles/cycles-list-gantt-chart.tsx index 938fd9a39..ea66f0929 100644 --- a/apps/app/components/cycles/cycles-list-gantt-chart.tsx +++ b/apps/app/components/cycles/cycles-list-gantt-chart.tsx @@ -1,21 +1,28 @@ import { FC } from "react"; -// next imports -import Link from "next/link"; + import { useRouter } from "next/router"; + +import { KeyedMutator } from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// hooks +import useUser from "hooks/use-user"; // components -import { GanttChartRoot } from "components/gantt-chart"; -// ui -import { Tooltip } from "components/ui"; +import { CycleGanttBlock, GanttChartRoot, IBlockUpdateData } from "components/gantt-chart"; // types import { ICycle } from "types"; type Props = { cycles: ICycle[]; + mutateCycles: KeyedMutator; }; -export const CyclesListGanttChartView: FC = ({ cycles }) => { +export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; + + const { user } = useUser(); // rendering issues on gantt sidebar const GanttSidebarBlockView = ({ data }: any) => ( @@ -28,53 +35,65 @@ export const CyclesListGanttChartView: FC = ({ cycles }) => {
); - // rendering issues on gantt card - const GanttBlockView = ({ data }: { data: ICycle }) => ( - -
-
- -
- {data?.name} -
-
-
- - ); + const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { + if (!workspaceSlug || !user) return; - // handle gantt issue start date and target date - const handleUpdateDates = async (data: any) => { - const payload = { - id: data?.id, - start_date: data?.start_date, - target_date: data?.target_date, - }; + mutateCycles((prevData) => { + if (!prevData) return prevData; + + const newList = prevData.map((p) => ({ + ...p, + ...(p.id === cycle.id + ? { + start_date: payload.start_date ? payload.start_date : p.start_date, + target_date: payload.target_date ? payload.target_date : p.end_date, + sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0]; + newList.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + return newList; + }, false); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) + newPayload.sort_order = payload.sort_order.newSortOrder; + + cyclesService + .patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user) + .finally(() => mutateCycles()); }; - const blockFormat = (blocks: any) => + const blockFormat = (blocks: ICycle[]) => blocks && blocks.length > 0 - ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); - return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), - data: _block, - }; - }) + ? blocks + .filter((b) => b.start_date && b.end_date) + .map((block) => ({ + data: block, + id: block.id, + sort_order: block.sort_order, + start_date: new Date(block.start_date ?? ""), + target_date: new Date(block.end_date ?? ""), + })) : []; return (
handleCycleUpdate(block, payload)} sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + blockRender={(data: any) => } + enableLeftDrag={false} + enableRightDrag={false} />
); diff --git a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx index 7ebd92a50..26bf0799c 100644 --- a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx @@ -17,7 +17,7 @@ export const AllCyclesList: React.FC = ({ viewType }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: allCyclesList } = useSWR( + const { data: allCyclesList, mutate } = useSWR( workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => @@ -25,5 +25,5 @@ export const AllCyclesList: React.FC = ({ viewType }) => { : null ); - return ; + return ; }; diff --git a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx index 79a427d95..552596d93 100644 --- a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx @@ -17,7 +17,7 @@ export const CompletedCyclesList: React.FC = ({ viewType }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: completedCyclesList } = useSWR( + const { data: completedCyclesList, mutate } = useSWR( workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => @@ -29,5 +29,5 @@ export const CompletedCyclesList: React.FC = ({ viewType }) => { : null ); - return ; + return ; }; diff --git a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx index fd2dccc93..05815dc9c 100644 --- a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx @@ -17,7 +17,7 @@ export const DraftCyclesList: React.FC = ({ viewType }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: draftCyclesList } = useSWR( + const { data: draftCyclesList, mutate } = useSWR( workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => @@ -25,5 +25,5 @@ export const DraftCyclesList: React.FC = ({ viewType }) => { : null ); - return ; + return ; }; diff --git a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx index 140727cb8..eba212af2 100644 --- a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx @@ -17,7 +17,7 @@ export const UpcomingCyclesList: React.FC = ({ viewType }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: upcomingCyclesList } = useSWR( + const { data: upcomingCyclesList, mutate } = useSWR( workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => @@ -29,5 +29,5 @@ export const UpcomingCyclesList: React.FC = ({ viewType }) => { : null ); - return ; + return ; }; diff --git a/apps/app/components/cycles/cycles-view.tsx b/apps/app/components/cycles/cycles-view.tsx index 6f3fa336a..ede13b65e 100644 --- a/apps/app/components/cycles/cycles-view.tsx +++ b/apps/app/components/cycles/cycles-view.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; +import { KeyedMutator, mutate } from "swr"; // services import cyclesService from "services/cycles.service"; @@ -35,10 +35,11 @@ import { type Props = { cycles: ICycle[] | undefined; + mutateCycles: KeyedMutator; viewType: string | null; }; -export const CyclesView: React.FC = ({ cycles, viewType }) => { +export const CyclesView: React.FC = ({ cycles, mutateCycles, viewType }) => { const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false); const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState(null); @@ -202,7 +203,7 @@ export const CyclesView: React.FC = ({ cycles, viewType }) => { ))}
) : ( - + ) ) : (
diff --git a/apps/app/components/cycles/gantt-chart.tsx b/apps/app/components/cycles/gantt-chart.tsx index 0deb10079..fe276b50d 100644 --- a/apps/app/components/cycles/gantt-chart.tsx +++ b/apps/app/components/cycles/gantt-chart.tsx @@ -1,20 +1,27 @@ -import { FC } from "react"; -// next imports -import Link from "next/link"; import { useRouter } from "next/router"; -// components -import { GanttChartRoot } from "components/gantt-chart"; -// ui -import { Tooltip } from "components/ui"; + // hooks +import useIssuesView from "hooks/use-issues-view"; +import useUser from "hooks/use-user"; import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view"; +import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; +// components +import { + GanttChartRoot, + IssueGanttBlock, + renderIssueBlocksStructure, +} from "components/gantt-chart"; +// types +import { IIssue } from "types"; -type Props = {}; - -export const CycleIssuesGanttChartView: FC = ({}) => { +export const CycleIssuesGanttChartView = () => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; + const { orderBy } = useIssuesView(); + + const { user } = useUser(); + const { ganttIssues, mutateGanttIssues } = useGanttChartCycleIssues( workspaceSlug as string, projectId as string, @@ -32,77 +39,18 @@ export const CycleIssuesGanttChartView: FC = ({}) => {
); - // rendering issues on gantt card - const GanttBlockView = ({ data }: any) => ( - - -
- -
- {data?.name} -
-
- {data.infoToggle && ( - -
- - info - -
-
- )} -
- - ); - - // handle gantt issue start date and target date - const handleUpdateDates = async (data: any) => { - const payload = { - id: data?.id, - start_date: data?.start_date, - target_date: data?.target_date, - }; - - console.log("payload", payload); - }; - - const blockFormat = (blocks: any) => - blocks && blocks.length > 0 - ? blocks.map((_block: any) => { - let startDate = new Date(_block.created_at); - let targetDate = new Date(_block.updated_at); - let infoToggle = true; - - if (_block?.start_date && _block.target_date) { - startDate = _block?.start_date; - targetDate = _block.target_date; - infoToggle = false; - } - - return { - start_date: new Date(startDate), - target_date: new Date(targetDate), - infoToggle: infoToggle, - data: _block, - }; - }) - : []; - return (
+ updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + blockRender={(data: any) => } + enableReorder={orderBy === "sort_order"} />
); diff --git a/apps/app/components/gantt-chart/blocks/block.tsx b/apps/app/components/gantt-chart/blocks/block.tsx new file mode 100644 index 000000000..52fd1fe52 --- /dev/null +++ b/apps/app/components/gantt-chart/blocks/block.tsx @@ -0,0 +1,103 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; + +// ui +import { Tooltip } from "components/ui"; +// helpers +import { renderShortDate } from "helpers/date-time.helper"; +// types +import { ICycle, IIssue, IModule } from "types"; +// constants +import { MODULE_STATUS } from "constants/module"; + +export const IssueGanttBlock = ({ issue }: { issue: IIssue }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( + + +
+ +
{issue.name}
+
+ {renderShortDate(issue.start_date ?? "")} to{" "} + {renderShortDate(issue.target_date ?? "")} +
+
+ } + position="top-left" + > +
+ {issue.name} +
+ +
+ + ); +}; + +export const CycleGanttBlock = ({ cycle }: { cycle: ICycle }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( + + +
+ +
{cycle.name}
+
+ {renderShortDate(cycle.start_date ?? "")} to {renderShortDate(cycle.end_date ?? "")} +
+
+ } + position="top-left" + > +
+ {cycle.name} +
+ +
+ + ); +}; + +export const ModuleGanttBlock = ({ module }: { module: IModule }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( + + +
s.value === module.status)?.color }} + /> + +
{module.name}
+
+ {renderShortDate(module.start_date ?? "")} to{" "} + {renderShortDate(module.target_date ?? "")} +
+
+ } + position="top-left" + > +
+ {module.name} +
+ +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/blocks/blocks-display.tsx b/apps/app/components/gantt-chart/blocks/blocks-display.tsx new file mode 100644 index 000000000..fd43c733e --- /dev/null +++ b/apps/app/components/gantt-chart/blocks/blocks-display.tsx @@ -0,0 +1,178 @@ +import { FC } from "react"; + +// react-beautiful-dnd +import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +// helpers +import { ChartDraggable } from "../helpers/draggable"; +import { renderDateFormat } from "helpers/date-time.helper"; +// types +import { IBlockUpdateData, IGanttBlock } from "../types"; + +export const GanttChartBlocks: FC<{ + itemsContainerWidth: number; + blocks: IGanttBlock[] | null; + sidebarBlockRender: FC; + blockRender: FC; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + enableLeftDrag: boolean; + enableRightDrag: boolean; + enableReorder: boolean; +}> = ({ + itemsContainerWidth, + blocks, + sidebarBlockRender, + blockRender, + blockUpdateHandler, + enableLeftDrag, + enableRightDrag, + enableReorder, +}) => { + const handleChartBlockPosition = ( + block: IGanttBlock, + totalBlockShifts: number, + dragDirection: "left" | "right" + ) => { + let updatedDate = new Date(); + + if (dragDirection === "left") { + const originalDate = new Date(block.start_date); + + const currentDay = originalDate.getDate(); + updatedDate = new Date(originalDate); + + updatedDate.setDate(currentDay - totalBlockShifts); + } else { + const originalDate = new Date(block.target_date); + + const currentDay = originalDate.getDate(); + updatedDate = new Date(originalDate); + + updatedDate.setDate(currentDay + totalBlockShifts); + } + + blockUpdateHandler(block.data, { + [dragDirection === "left" ? "start_date" : "target_date"]: renderDateFormat(updatedDate), + }); + }; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination, draggableId } = result; + + if (!destination) return; + + if (source.index === destination.index && document) { + // const draggedBlock = document.querySelector(`#${draggableId}`) as HTMLElement; + // const blockStyles = window.getComputedStyle(draggedBlock); + + // console.log(blockStyles.marginLeft); + + return; + } + + let updatedSortOrder = blocks[source.index].sort_order; + + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + else if (destination.index === blocks.length - 1) + updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( +
+ + + {(droppableProvided, droppableSnapshot) => ( +
+ <> + {blocks && + blocks.length > 0 && + blocks.map( + (block, index: number) => + block.start_date && + block.target_date && ( + + {(provided) => ( +
+ handleChartBlockPosition(block, ...args)} + enableLeftDrag={enableLeftDrag} + enableRightDrag={enableRightDrag} + provided={provided} + > +
+ {blockRender({ + ...block.data, + })} +
+
+
+ )} +
+ ) + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ + {/* sidebar */} + {/*
+ {blocks && + blocks.length > 0 && + blocks.map((block: any, _idx: number) => ( +
+ {sidebarBlockRender(block?.data)} +
+ ))} +
*/} +
+ ); +}; diff --git a/apps/app/components/gantt-chart/blocks/index.ts b/apps/app/components/gantt-chart/blocks/index.ts new file mode 100644 index 000000000..8773b2797 --- /dev/null +++ b/apps/app/components/gantt-chart/blocks/index.ts @@ -0,0 +1,2 @@ +export * from "./block"; +export * from "./blocks-display"; diff --git a/apps/app/components/gantt-chart/blocks/index.tsx b/apps/app/components/gantt-chart/blocks/index.tsx deleted file mode 100644 index dcc3a2910..000000000 --- a/apps/app/components/gantt-chart/blocks/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { FC, useEffect, useState } from "react"; -// helpers -import { ChartDraggable } from "../helpers/draggable"; -// data -import { datePreview } from "../data"; - -export const GanttChartBlocks: FC<{ - itemsContainerWidth: number; - blocks: null | any[]; - sidebarBlockRender: FC; - blockRender: FC; -}> = ({ itemsContainerWidth, blocks, sidebarBlockRender, blockRender }) => { - const handleChartBlockPosition = (block: any) => { - // setChartBlocks((prevData: any) => - // prevData.map((_block: any) => (_block?.data?.id == block?.data?.id ? block : _block)) - // ); - }; - - return ( -
-
- {blocks && - blocks.length > 0 && - blocks.map((block: any, _idx: number) => ( - <> - {block.start_date && block.target_date && ( - -
-
-
- {block?.start_date ? datePreview(block?.start_date) : "-"} -
-
- -
- {blockRender({ - ...block?.data, - infoToggle: block?.infoToggle ? true : false, - })} -
- -
-
- {block?.target_date ? datePreview(block?.target_date) : "-"} -
-
-
-
- )} - - ))} -
- - {/* sidebar */} - {/*
- {blocks && - blocks.length > 0 && - blocks.map((block: any, _idx: number) => ( -
- {sidebarBlockRender(block?.data)} -
- ))} -
*/} -
- ); -}; diff --git a/apps/app/components/gantt-chart/chart/bi-week.tsx b/apps/app/components/gantt-chart/chart/bi-week.tsx index 1e1173ad4..3637f88ea 100644 --- a/apps/app/components/gantt-chart/chart/bi-week.tsx +++ b/apps/app/components/gantt-chart/chart/bi-week.tsx @@ -25,7 +25,7 @@ export const BiWeekChartView: FC = () => {
= () => {
= () => {
void; + blocks: IGanttBlock[] | null; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; sidebarBlockRender: FC; blockRender: FC; + enableLeftDrag: boolean; + enableRightDrag: boolean; + enableReorder: boolean; }; export const ChartViewRoot: FC = ({ @@ -54,6 +52,9 @@ export const ChartViewRoot: FC = ({ blockUpdateHandler, sidebarBlockRender, blockRender, + enableLeftDrag, + enableRightDrag, + enableReorder, }) => { const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); @@ -62,13 +63,13 @@ export const ChartViewRoot: FC = ({ const [blocksSidebarView, setBlocksSidebarView] = useState(false); // blocks state management starts - const [chartBlocks, setChartBlocks] = useState(null); + const [chartBlocks, setChartBlocks] = useState(null); - const renderBlockStructure = (view: any, blocks: any) => + const renderBlockStructure = (view: any, blocks: IGanttBlock[]) => blocks && blocks.length > 0 - ? blocks.map((_block: any) => ({ - ..._block, - position: getMonthChartItemPositionWidthInMonth(view, _block), + ? blocks.map((block: any) => ({ + ...block, + position: getMonthChartItemPositionWidthInMonth(view, block), })) : []; @@ -154,13 +155,14 @@ export const ChartViewRoot: FC = ({ const updatingCurrentLeftScrollPosition = (width: number) => { const scrollContainer = document.getElementById("scroll-container") as HTMLElement; - scrollContainer.scrollLeft = width + scrollContainer.scrollLeft; - setItemsContainerWidth(width + scrollContainer.scrollLeft); + scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft; + setItemsContainerWidth(width + scrollContainer?.scrollLeft); }; const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => { const scrollContainer = document.getElementById("scroll-container") as HTMLElement; - const clientVisibleWidth: number = scrollContainer.clientWidth; + + const clientVisibleWidth: number = scrollContainer?.clientWidth; let scrollWidth: number = 0; let daysDifference: number = 0; @@ -189,9 +191,9 @@ export const ChartViewRoot: FC = ({ const onScroll = () => { const scrollContainer = document.getElementById("scroll-container") as HTMLElement; - const scrollWidth: number = scrollContainer.scrollWidth; - const clientVisibleWidth: number = scrollContainer.clientWidth; - const currentScrollPosition: number = scrollContainer.scrollLeft; + const scrollWidth: number = scrollContainer?.scrollWidth; + const clientVisibleWidth: number = scrollContainer?.clientWidth; + const currentScrollPosition: number = scrollContainer?.scrollLeft; const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth; @@ -207,6 +209,7 @@ export const ChartViewRoot: FC = ({ const scrollContainer = document.getElementById("scroll-container") as HTMLElement; scrollContainer.addEventListener("scroll", onScroll); + return () => { scrollContainer.removeEventListener("scroll", onScroll); }; @@ -242,7 +245,7 @@ export const ChartViewRoot: FC = ({
*/} {/* chart header */} -
+
{/*
setBlocksSidebarView(() => !blocksSidebarView)} @@ -301,8 +304,8 @@ export const ChartViewRoot: FC = ({
setFullScreenMode(() => !fullScreenMode)} + className="transition-all border border-custom-border-200 p-1 flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80" + onClick={() => setFullScreenMode((prevData) => !prevData)} > {fullScreenMode ? ( @@ -325,6 +328,10 @@ export const ChartViewRoot: FC = ({ blocks={chartBlocks} sidebarBlockRender={sidebarBlockRender} blockRender={blockRender} + blockUpdateHandler={blockUpdateHandler} + enableLeftDrag={enableLeftDrag} + enableRightDrag={enableRightDrag} + enableReorder={enableReorder} /> )} diff --git a/apps/app/components/gantt-chart/chart/month.tsx b/apps/app/components/gantt-chart/chart/month.tsx index 68e517960..b6c68b5d1 100644 --- a/apps/app/components/gantt-chart/chart/month.tsx +++ b/apps/app/components/gantt-chart/chart/month.tsx @@ -1,48 +1,55 @@ import { FC } from "react"; -// context + +// hooks import { useChart } from "../hooks"; +// types +import { IMonthBlock } from "../views"; export const MonthChartView: FC = () => { - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentViewData, renderView } = useChart(); + + const monthBlocks: IMonthBlock[] = renderView; return ( <> -
- {renderView && - renderView.length > 0 && - renderView.map((_itemRoot: any, _idxRoot: any) => ( -
+
+ {monthBlocks && + monthBlocks.length > 0 && + monthBlocks.map((block, _idxRoot) => ( +
- {_itemRoot?.title} + {block?.title}
-
- {_itemRoot.children && - _itemRoot.children.length > 0 && - _itemRoot.children.map((_item: any, _idx: any) => ( +
+ {block?.children && + block?.children.length > 0 && + block?.children.map((monthDay, _idx) => (
-
{_item.title}
+
{monthDay?.title}
- {_item?.today && ( -
+ {monthDay?.today && ( +
)}
diff --git a/apps/app/components/gantt-chart/chart/quarter.tsx b/apps/app/components/gantt-chart/chart/quarter.tsx index 67605b6b5..abe56c83e 100644 --- a/apps/app/components/gantt-chart/chart/quarter.tsx +++ b/apps/app/components/gantt-chart/chart/quarter.tsx @@ -25,7 +25,7 @@ export const QuarterChartView: FC = () => {
= () => {
= () => {
+ blocks && blocks.length > 0 + ? blocks.map((block) => ({ + data: block, + id: block.id, + sort_order: block.sort_order, + start_date: new Date(block.start_date ?? ""), + target_date: new Date(block.target_date ?? ""), + })) + : []; diff --git a/apps/app/components/gantt-chart/helpers/draggable.tsx b/apps/app/components/gantt-chart/helpers/draggable.tsx index a5404cc8e..8a85a0dd3 100644 --- a/apps/app/components/gantt-chart/helpers/draggable.tsx +++ b/apps/app/components/gantt-chart/helpers/draggable.tsx @@ -1,138 +1,155 @@ -import { useState, useRef } from "react"; +import React, { useRef, useState } from "react"; -export const ChartDraggable = ({ children, block, handleBlock, className }: any) => { - const [dragging, setDragging] = useState(false); +// react-beautiful-dnd +import { DraggableProvided } from "react-beautiful-dnd"; +import { useChart } from "../hooks"; +// types +import { IGanttBlock } from "../types"; - const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0); - const [blockPositionLeft, setBlockPositionLeft] = useState(0); - const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0); +type Props = { + children: any; + block: IGanttBlock; + handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right") => void; + enableLeftDrag: boolean; + enableRightDrag: boolean; + provided: DraggableProvided; +}; - const handleMouseDown = (event: any) => { - const chartBlockPositionLeft: number = block.position.marginLeft; - const blockPositionLeft: number = event.target.getBoundingClientRect().left; - const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left; +export const ChartDraggable: React.FC = ({ + children, + block, + handleBlock, + enableLeftDrag = true, + enableRightDrag = true, + provided, +}) => { + const [isLeftResizing, setIsLeftResizing] = useState(false); + const [isRightResizing, setIsRightResizing] = useState(false); - console.log("--------------------"); - console.log("chartBlockPositionLeft", chartBlockPositionLeft); - console.log("blockPositionLeft", blockPositionLeft); - console.log("dragBlockOffsetX", dragBlockOffsetX); - console.log("-->"); + const parentDivRef = useRef(null); + const resizableRef = useRef(null); - setDragging(true); - setChartBlockPositionLeft(chartBlockPositionLeft); - setBlockPositionLeft(blockPositionLeft); - setDragBlockOffsetX(dragBlockOffsetX); - }; + const { currentViewData } = useChart(); - const handleMouseMove = (event: any) => { - if (!dragging) return; + const handleDrag = (dragDirection: "left" | "right") => { + if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position) + return; - const currentBlockPosition = event.clientX - dragBlockOffsetX; - console.log("currentBlockPosition", currentBlockPosition); - if (currentBlockPosition <= blockPositionLeft) { - const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition); - console.log("updatedPosition", updatedPosition); - handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } }); - } else { - const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition); - console.log("updatedPosition", updatedPosition); - handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } }); - } - console.log("--------------------"); - }; + const resizableDiv = resizableRef.current; + const parentDiv = parentDivRef.current; - const handleMouseUp = () => { - setDragging(false); - setChartBlockPositionLeft(0); - setBlockPositionLeft(0); - setDragBlockOffsetX(0); + 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 = block?.position?.marginLeft; + + const handleMouseMove = (e: MouseEvent) => { + if (!window) return; + + let delWidth = 0; + + const posFromLeft = e.clientX; + const posFromRight = window.innerWidth - e.clientX; + + const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; + const appSidebar = document.querySelector("#app-sidebar") as HTMLElement; + + // manually scroll to left if reached the left end while dragging + if (posFromLeft - appSidebar.clientWidth <= 70) { + if (e.movementX > 0) return; + + delWidth = dragDirection === "left" ? -5 : 5; + + scrollContainer.scrollBy(-1 * Math.abs(delWidth), 0); + } else delWidth = dragDirection === "left" ? -1 * e.movementX : e.movementX; + + // manually scroll to right if reached the right end while dragging + if (posFromRight <= 70) { + if (e.movementX < 0) return; + + delWidth = dragDirection === "left" ? -5 : 5; + + scrollContainer.scrollBy(Math.abs(delWidth), 0); + } else delWidth = dragDirection === "left" ? -1 * e.movementX : e.movementX; + + // 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 = `${newWidth}px`; + if (block.position) block.position.width = newWidth; + + // update the margin left of the block if dragging from the left end + if (dragDirection === "left") { + // calculate new marginLeft and update the initial marginLeft using -= + const newMarginLeft = + Math.round((initialMarginLeft -= delWidth) / columnWidth) * columnWidth; + + parentDiv.style.marginLeft = `${newMarginLeft}px`; + if (block.position) block.position.marginLeft = newMarginLeft; + } + }; + + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + + const totalBlockShifts = Math.ceil( + (resizableDiv.clientWidth - blockInitialWidth) / columnWidth + ); + + handleBlock(totalBlockShifts, dragDirection); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); }; return (
- {children} + {enableLeftDrag && ( + <> +
handleDrag("left")} + onMouseEnter={() => setIsLeftResizing(true)} + onMouseLeave={() => setIsLeftResizing(false)} + className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[1] w-6 h-10 bg-brand-backdrop rounded-md cursor-col-resize" + /> +
+ + )} + {React.cloneElement(children, { ref: resizableRef, ...provided.dragHandleProps })} + {enableRightDrag && ( + <> +
handleDrag("right")} + onMouseEnter={() => setIsRightResizing(true)} + onMouseLeave={() => setIsRightResizing(false)} + className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[1] w-6 h-6 bg-brand-backdrop rounded-md cursor-col-resize" + /> +
+ + )}
); }; - -// import { useState } from "react"; - -// export const ChartDraggable = ({ children, id, className = "", style }: any) => { -// const [dragging, setDragging] = useState(false); - -// const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0); -// const [blockPositionLeft, setBlockPositionLeft] = useState(0); -// const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0); - -// const handleDragStart = (event: any) => { -// // event.dataTransfer.setData("text/plain", event.target.id); - -// const chartBlockPositionLeft: number = parseInt(event.target.style.left.slice(0, -2)); -// const blockPositionLeft: number = event.target.getBoundingClientRect().left; -// const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left; - -// console.log("chartBlockPositionLeft", chartBlockPositionLeft); -// console.log("blockPositionLeft", blockPositionLeft); -// console.log("dragBlockOffsetX", dragBlockOffsetX); -// console.log("--------------------"); - -// setDragging(true); -// setChartBlockPositionLeft(chartBlockPositionLeft); -// setBlockPositionLeft(blockPositionLeft); -// setDragBlockOffsetX(dragBlockOffsetX); -// }; - -// const handleDragEnd = () => { -// setDragging(false); -// setChartBlockPositionLeft(0); -// setBlockPositionLeft(0); -// setDragBlockOffsetX(0); -// }; - -// const handleDragOver = (event: any) => { -// event.preventDefault(); -// if (dragging) { -// const scrollContainer = document.getElementById(`block-parent-${id}`) as HTMLElement; -// const currentBlockPosition = event.clientX - dragBlockOffsetX; -// console.log('currentBlockPosition') -// if (currentBlockPosition <= blockPositionLeft) { -// const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition); -// console.log("updatedPosition", updatedPosition); -// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`; -// } else { -// const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition); -// console.log("updatedPosition", updatedPosition); -// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`; -// } -// console.log("--------------------"); -// } -// }; - -// const handleDrop = (event: any) => { -// event.preventDefault(); -// setDragging(false); -// setChartBlockPositionLeft(0); -// setBlockPositionLeft(0); -// setDragBlockOffsetX(0); -// }; - -// return ( -//
-// {children} -//
-// ); -// }; diff --git a/apps/app/components/gantt-chart/helpers/index.ts b/apps/app/components/gantt-chart/helpers/index.ts new file mode 100644 index 000000000..c4c919ec0 --- /dev/null +++ b/apps/app/components/gantt-chart/helpers/index.ts @@ -0,0 +1 @@ +export * from "./block-structure"; diff --git a/apps/app/components/gantt-chart/hooks/block-update.tsx b/apps/app/components/gantt-chart/hooks/block-update.tsx new file mode 100644 index 000000000..d9d808b38 --- /dev/null +++ b/apps/app/components/gantt-chart/hooks/block-update.tsx @@ -0,0 +1,43 @@ +import { KeyedMutator } from "swr"; + +// services +import issuesService from "services/issues.service"; +// types +import { ICurrentUserResponse, IIssue } from "types"; +import { IBlockUpdateData } from "../types"; + +export const updateGanttIssue = ( + issue: IIssue, + payload: IBlockUpdateData, + mutate: KeyedMutator, + user: ICurrentUserResponse | undefined, + workspaceSlug: string | undefined +) => { + if (!issue || !workspaceSlug || !user) return; + + mutate((prevData: IIssue[]) => { + if (!prevData) return prevData; + + const newList = prevData.map((p) => ({ + ...p, + ...(p.id === issue.id ? payload : {}), + })); + + if (payload.sort_order) { + const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0]; + removedElement.sort_order = payload.sort_order.newSortOrder; + newList.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + return newList; + }, false); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) + newPayload.sort_order = payload.sort_order.newSortOrder; + + issuesService + .patchIssue(workspaceSlug, issue.project, issue.id, newPayload, user) + .finally(() => mutate()); +}; diff --git a/apps/app/components/gantt-chart/index.ts b/apps/app/components/gantt-chart/index.ts index 1efe34c51..4520ee194 100644 --- a/apps/app/components/gantt-chart/index.ts +++ b/apps/app/components/gantt-chart/index.ts @@ -1 +1,5 @@ +export * from "./blocks"; +export * from "./helpers"; +export * from "./hooks"; export * from "./root"; +export * from "./types"; diff --git a/apps/app/components/gantt-chart/root.tsx b/apps/app/components/gantt-chart/root.tsx index c52bc55b0..077e8a896 100644 --- a/apps/app/components/gantt-chart/root.tsx +++ b/apps/app/components/gantt-chart/root.tsx @@ -3,15 +3,20 @@ import { FC } from "react"; import { ChartViewRoot } from "./chart"; // context import { ChartContextProvider } from "./contexts"; +// types +import { IBlockUpdateData, IGanttBlock } from "./types"; type GanttChartRootProps = { border?: boolean; title: null | string; loaderTitle: string; - blocks: any; - blockUpdateHandler: (data: any) => void; + blocks: IGanttBlock[] | null; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; sidebarBlockRender: FC; blockRender: FC; + enableLeftDrag?: boolean; + enableRightDrag?: boolean; + enableReorder?: boolean; }; export const GanttChartRoot: FC = ({ @@ -22,6 +27,9 @@ export const GanttChartRoot: FC = ({ blockUpdateHandler, sidebarBlockRender, blockRender, + enableLeftDrag = true, + enableRightDrag = true, + enableReorder = true, }) => ( = ({ blockUpdateHandler={blockUpdateHandler} sidebarBlockRender={sidebarBlockRender} blockRender={blockRender} + enableLeftDrag={enableLeftDrag} + enableRightDrag={enableRightDrag} + enableReorder={enableReorder} /> ); diff --git a/apps/app/components/gantt-chart/types/index.ts b/apps/app/components/gantt-chart/types/index.ts index aa6ebe9da..645fd9c87 100644 --- a/apps/app/components/gantt-chart/types/index.ts +++ b/apps/app/components/gantt-chart/types/index.ts @@ -5,10 +5,32 @@ export type allViewsType = { data: Object | null; }; +export interface IGanttBlock { + data: any; + id: string; + position?: { + marginLeft: number; + width: number; + }; + sort_order: number; + start_date: Date; + target_date: Date; +} + +export interface IBlockUpdateData { + sort_order?: { + destinationIndex: number; + newSortOrder: number; + sourceIndex: number; + }; + start_date?: string; + target_date?: string; +} + export interface ChartContextData { allViews: allViewsType[]; currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; - currentViewData: any; + currentViewData: ChartDataType | undefined; renderView: any; } diff --git a/apps/app/components/gantt-chart/views/month-view.ts b/apps/app/components/gantt-chart/views/month-view.ts index 7211a45eb..db21e372b 100644 --- a/apps/app/components/gantt-chart/views/month-view.ts +++ b/apps/app/components/gantt-chart/views/month-view.ts @@ -1,5 +1,5 @@ // types -import { ChartDataType } from "../types"; +import { ChartDataType, IGanttBlock } from "../types"; // data import { weeks, months } from "../data"; // helpers @@ -19,7 +19,35 @@ type GetAllDaysInMonthInMonthViewType = { active: boolean; today: boolean; }; -const getAllDaysInMonthInMonthView = (month: number, year: number) => { + +interface IMonthChild { + active: boolean; + date: Date; + day: number; + dayData: { + key: number; + shortTitle: string; + title: string; + }; + title: string; + today: boolean; + weekNumber: number; +} + +export interface IMonthBlock { + children: IMonthChild[]; + month: number; + monthData: { + key: number; + shortTitle: string; + title: string; + }; + title: string; + year: number; +} +[]; + +const getAllDaysInMonthInMonthView = (month: number, year: number): IMonthChild[] => { const day: GetAllDaysInMonthInMonthViewType[] = []; const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year); const currentDate = new Date(); @@ -45,7 +73,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number) => { return day; }; -const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => { +const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number): IMonthBlock => { const currentMonth: number = month; const currentYear: number = year; @@ -162,7 +190,11 @@ export const getNumberOfDaysBetweenTwoDatesInMonth = (startDate: Date, endDate: return daysDifference; }; -export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, itemData: any) => { +// calc item scroll position and width +export const getMonthChartItemPositionWidthInMonth = ( + chartData: ChartDataType, + itemData: IGanttBlock +) => { let scrollPosition: number = 0; let scrollWidth: number = 0; diff --git a/apps/app/components/gantt-chart/views/year-view.ts b/apps/app/components/gantt-chart/views/year-view.ts index 76edb0d57..82d397e97 100644 --- a/apps/app/components/gantt-chart/views/year-view.ts +++ b/apps/app/components/gantt-chart/views/year-view.ts @@ -100,8 +100,6 @@ export const generateYearChart = (yearPayload: ChartDataType, side: null | "left .map((monthData: any) => monthData.children.length) .reduce((partialSum: number, a: number) => partialSum + a, 0) * yearPayload.data.width; - console.log("scrollWidth", scrollWidth); - return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; }; diff --git a/apps/app/components/issues/gantt-chart.tsx b/apps/app/components/issues/gantt-chart.tsx index d8a54619f..4912183a8 100644 --- a/apps/app/components/issues/gantt-chart.tsx +++ b/apps/app/components/issues/gantt-chart.tsx @@ -1,20 +1,27 @@ -import { FC } from "react"; -// next imports -import Link from "next/link"; import { useRouter } from "next/router"; -// components -import { GanttChartRoot } from "components/gantt-chart"; -// ui -import { Tooltip } from "components/ui"; + // hooks +import useIssuesView from "hooks/use-issues-view"; +import useUser from "hooks/use-user"; import useGanttChartIssues from "hooks/gantt-chart/issue-view"; +import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; +// components +import { + GanttChartRoot, + IssueGanttBlock, + renderIssueBlocksStructure, +} from "components/gantt-chart"; +// types +import { IIssue } from "types"; -type Props = {}; - -export const IssueGanttChartView: FC = ({}) => { +export const IssueGanttChartView = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { orderBy } = useIssuesView(); + + const { user } = useUser(); + const { ganttIssues, mutateGanttIssues } = useGanttChartIssues( workspaceSlug as string, projectId as string @@ -31,76 +38,19 @@ export const IssueGanttChartView: FC = ({}) => {
); - // rendering issues on gantt card - const GanttBlockView = ({ data }: any) => ( - - -
- -
- {data?.name} -
-
- {data.infoToggle && ( - -
- - info - -
-
- )} -
- - ); - - // handle gantt issue start date and target date - const handleUpdateDates = async (data: any) => { - const payload = { - id: data?.id, - start_date: data?.start_date, - target_date: data?.target_date, - }; - }; - - const blockFormat = (blocks: any) => - blocks && blocks.length > 0 - ? blocks.map((_block: any) => { - let startDate = new Date(_block.created_at); - let targetDate = new Date(_block.updated_at); - let infoToggle = true; - - if (_block?.start_date && _block.target_date) { - startDate = _block?.start_date; - targetDate = _block.target_date; - infoToggle = false; - } - - return { - start_date: new Date(startDate), - target_date: new Date(targetDate), - infoToggle: infoToggle, - data: _block, - }; - }) - : []; - return (
+ updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + blockRender={(data: any) => } + enableReorder={orderBy === "sort_order"} />
); diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index 5a66c98b8..8f13a8047 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -248,7 +248,11 @@ export const CreateUpdateIssueModal: React.FC = ({ await addIssueToModule(res.id, payload.module); if (issueView === "calendar") mutate(calendarFetchKey); - if (issueView === "gantt_chart") mutate(ganttFetchKey); + if (issueView === "gantt_chart") + mutate(ganttFetchKey, { + start_target_date: true, + order_by: "sort_order", + }); if (issueView === "spreadsheet") mutate(spreadsheetFetchKey); if (groupedIssues) mutateMyIssues(); diff --git a/apps/app/components/modules/gantt-chart.tsx b/apps/app/components/modules/gantt-chart.tsx index 3ef46deb4..8ab8b6024 100644 --- a/apps/app/components/modules/gantt-chart.tsx +++ b/apps/app/components/modules/gantt-chart.tsx @@ -1,13 +1,20 @@ import { FC } from "react"; -// next imports -import Link from "next/link"; + import { useRouter } from "next/router"; -// components -import { GanttChartRoot } from "components/gantt-chart"; -// ui -import { Tooltip } from "components/ui"; + // hooks +import useIssuesView from "hooks/use-issues-view"; +import useUser from "hooks/use-user"; import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view"; +import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; +// components +import { + GanttChartRoot, + IssueGanttBlock, + renderIssueBlocksStructure, +} from "components/gantt-chart"; +// types +import { IIssue } from "types"; type Props = {}; @@ -15,6 +22,10 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; + const { orderBy } = useIssuesView(); + + const { user } = useUser(); + const { ganttIssues, mutateGanttIssues } = useGanttChartModuleIssues( workspaceSlug as string, projectId as string, @@ -32,77 +43,18 @@ export const ModuleIssuesGanttChartView: FC = ({}) => {
); - // rendering issues on gantt card - const GanttBlockView = ({ data }: any) => ( - - -
- -
- {data?.name} -
-
- {data.infoToggle && ( - -
- - info - -
-
- )} -
- - ); - - // handle gantt issue start date and target date - const handleUpdateDates = async (data: any) => { - const payload = { - id: data?.id, - start_date: data?.start_date, - target_date: data?.target_date, - }; - - console.log("payload", payload); - }; - - const blockFormat = (blocks: any) => - blocks && blocks.length > 0 - ? blocks.map((_block: any) => { - let startDate = new Date(_block.created_at); - let targetDate = new Date(_block.updated_at); - let infoToggle = true; - - if (_block?.start_date && _block.target_date) { - startDate = _block?.start_date; - targetDate = _block.target_date; - infoToggle = false; - } - - return { - start_date: new Date(startDate), - target_date: new Date(targetDate), - infoToggle: infoToggle, - data: _block, - }; - }) - : []; - return (
+ updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + blockRender={(data: any) => } + enableReorder={orderBy === "sort_order"} />
); diff --git a/apps/app/components/modules/modules-list-gantt-chart.tsx b/apps/app/components/modules/modules-list-gantt-chart.tsx index b739f0b1e..2dd482d8b 100644 --- a/apps/app/components/modules/modules-list-gantt-chart.tsx +++ b/apps/app/components/modules/modules-list-gantt-chart.tsx @@ -1,11 +1,15 @@ import { FC } from "react"; -// next imports -import Link from "next/link"; + import { useRouter } from "next/router"; + +import { KeyedMutator } from "swr"; + +// services +import modulesService from "services/modules.service"; +// hooks +import useUser from "hooks/use-user"; // components -import { GanttChartRoot } from "components/gantt-chart"; -// ui -import { Tooltip } from "components/ui"; +import { GanttChartRoot, IBlockUpdateData, ModuleGanttBlock } from "components/gantt-chart"; // types import { IModule } from "types"; // constants @@ -13,11 +17,14 @@ import { MODULE_STATUS } from "constants/module"; type Props = { modules: IModule[]; + mutateModules: KeyedMutator; }; -export const ModulesListGanttChartView: FC = ({ modules }) => { +export const ModulesListGanttChartView: FC = ({ modules, mutateModules }) => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; + + const { user } = useUser(); // rendering issues on gantt sidebar const GanttSidebarBlockView = ({ data }: any) => ( @@ -32,42 +39,52 @@ export const ModulesListGanttChartView: FC = ({ modules }) => {
); - // rendering issues on gantt card - const GanttBlockView = ({ data }: { data: IModule }) => ( - - -
s.value === data.status)?.color }} - /> - -
- {data?.name} -
-
-
- - ); + const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => { + if (!workspaceSlug || !user) return; - // handle gantt issue start date and target date - const handleUpdateDates = async (data: any) => { - const payload = { - id: data?.id, - start_date: data?.start_date, - target_date: data?.target_date, - }; + mutateModules((prevData) => { + if (!prevData) return prevData; + + const newList = prevData.map((p) => ({ + ...p, + ...(p.id === module.id + ? { + start_date: payload.start_date ? payload.start_date : p.start_date, + target_date: payload.target_date ? payload.target_date : p.target_date, + sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0]; + newList.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + return newList; + }, false); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) + newPayload.sort_order = payload.sort_order.newSortOrder; + + modulesService + .patchModule(workspaceSlug.toString(), module.project, module.id, newPayload, user) + .finally(() => mutateModules()); }; - const blockFormat = (blocks: any) => + const blockFormat = (blocks: IModule[]) => blocks && blocks.length > 0 - ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); - return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), - data: _block, - }; - }) + ? blocks + .filter((b) => b.start_date && b.target_date) + .map((block) => ({ + data: block, + id: block.id, + sort_order: block.sort_order, + start_date: new Date(block.start_date ?? ""), + target_date: new Date(block.target_date ?? ""), + })) : []; return ( @@ -76,9 +93,9 @@ export const ModulesListGanttChartView: FC = ({ modules }) => { title="Modules" loaderTitle="Modules" blocks={modules ? blockFormat(modules) : null} - blockUpdateHandler={handleUpdateDates} + blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)} sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + blockRender={(data: any) => } />
); diff --git a/apps/app/components/project/sidebar-list.tsx b/apps/app/components/project/sidebar-list.tsx index 63a5e0f80..29dafd6cf 100644 --- a/apps/app/components/project/sidebar-list.tsx +++ b/apps/app/components/project/sidebar-list.tsx @@ -103,9 +103,7 @@ export const ProjectSidebarList: FC = () => { ? (projectsList[destination.index + 1].sort_order as number) : (projectsList[destination.index - 1].sort_order as number); - updatedSortOrder = Math.round( - (destinationSortingOrder + relativeDestinationSortingOrder) / 2 - ); + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; } mutate( diff --git a/apps/app/components/views/gantt-chart.tsx b/apps/app/components/views/gantt-chart.tsx index 6687cb93f..630ffaca0 100644 --- a/apps/app/components/views/gantt-chart.tsx +++ b/apps/app/components/views/gantt-chart.tsx @@ -1,13 +1,19 @@ import { FC } from "react"; -// next imports -import Link from "next/link"; + import { useRouter } from "next/router"; -// components -import { GanttChartRoot } from "components/gantt-chart"; -// ui -import { Tooltip } from "components/ui"; + // hooks import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view"; +import useUser from "hooks/use-user"; +import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; +// components +import { + GanttChartRoot, + IssueGanttBlock, + renderIssueBlocksStructure, +} from "components/gantt-chart"; +// types +import { IIssue } from "types"; type Props = {}; @@ -15,6 +21,8 @@ export const ViewIssuesGanttChartView: FC = ({}) => { const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query; + const { user } = useUser(); + const { ganttIssues, mutateGanttIssues } = useGanttChartViewIssues( workspaceSlug as string, projectId as string, @@ -32,77 +40,17 @@ export const ViewIssuesGanttChartView: FC = ({}) => {
); - // rendering issues on gantt card - const GanttBlockView = ({ data }: any) => ( - - -
- -
- {data?.name} -
-
- {data.infoToggle && ( - -
- - info - -
-
- )} -
- - ); - - // handle gantt issue start date and target date - const handleUpdateDates = async (data: any) => { - const payload = { - id: data?.id, - start_date: data?.start_date, - target_date: data?.target_date, - }; - - console.log("payload", payload); - }; - - const blockFormat = (blocks: any) => - blocks && blocks.length > 0 - ? blocks.map((_block: any) => { - let startDate = new Date(_block.created_at); - let targetDate = new Date(_block.updated_at); - let infoToggle = true; - - if (_block?.start_date && _block.target_date) { - startDate = _block?.start_date; - targetDate = _block.target_date; - infoToggle = false; - } - - return { - start_date: new Date(startDate), - target_date: new Date(targetDate), - infoToggle: infoToggle, - data: _block, - }; - }) - : []; - return (
+ updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } sidebarBlockRender={(data: any) => } - blockRender={(data: any) => } + blockRender={(data: any) => } />
); diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index b6d164068..295d55f77 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -2,13 +2,23 @@ import { objToQueryParams } from "helpers/string.helper"; import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types"; const paramsToKey = (params: any) => { - const { state, priority, assignees, created_by, labels, target_date, sub_issue } = params; + const { + state, + priority, + assignees, + created_by, + labels, + target_date, + sub_issue, + start_target_date, + } = params; let stateKey = state ? state.split(",") : []; let priorityKey = priority ? priority.split(",") : []; let assigneesKey = assignees ? assignees.split(",") : []; let createdByKey = created_by ? created_by.split(",") : []; let labelsKey = labels ? labels.split(",") : []; + const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE"; const targetDateKey = target_date ?? ""; const type = params.type ? params.type.toUpperCase() : "NULL"; const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL"; @@ -21,7 +31,7 @@ const paramsToKey = (params: any) => { createdByKey = createdByKey.sort().join("_"); labelsKey = labelsKey.sort().join("_"); - return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${sub_issue}`; + return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`; }; const inboxParamsToKey = (params: any) => { diff --git a/apps/app/hooks/gantt-chart/cycle-issues-view.tsx b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx index 1782ca339..25baf0d3e 100644 --- a/apps/app/hooks/gantt-chart/cycle-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx @@ -2,6 +2,8 @@ import useSWR from "swr"; // services import cyclesService from "services/cycles.service"; +// hooks +import useIssuesView from "hooks/use-issues-view"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; @@ -10,15 +12,27 @@ const useGanttChartCycleIssues = ( projectId: string | undefined, cycleId: string | undefined ) => { + const { orderBy, filters, showSubIssues } = useIssuesView(); + + const params: any = { + order_by: orderBy, + type: filters?.type ? filters?.type : undefined, + sub_issue: showSubIssues, + start_target_date: true, + }; + // all issues under the workspace and project const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( - workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) : null, + workspaceSlug && projectId && cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) + : null, workspaceSlug && projectId && cycleId ? () => cyclesService.getCycleIssuesWithParams( workspaceSlug.toString(), projectId.toString(), - cycleId.toString() + cycleId.toString(), + params ) : null ); diff --git a/apps/app/hooks/gantt-chart/issue-view.tsx b/apps/app/hooks/gantt-chart/issue-view.tsx index bcdb0fca4..c7ffa0ffe 100644 --- a/apps/app/hooks/gantt-chart/issue-view.tsx +++ b/apps/app/hooks/gantt-chart/issue-view.tsx @@ -2,15 +2,27 @@ import useSWR from "swr"; // services import issuesService from "services/issues.service"; +// hooks +import useIssuesView from "hooks/use-issues-view"; // fetch-keys import { PROJECT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys"; const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: string | undefined) => { + const { orderBy, filters, showSubIssues } = useIssuesView(); + + const params: any = { + order_by: orderBy, + type: filters?.type ? filters?.type : undefined, + sub_issue: showSubIssues, + start_target_date: true, + }; + // all issues under the workspace and project const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId) : null, + workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params) : null, workspaceSlug && projectId - ? () => issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString()) + ? () => + issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params) : null ); diff --git a/apps/app/hooks/gantt-chart/module-issues-view.tsx b/apps/app/hooks/gantt-chart/module-issues-view.tsx index baf995944..ca686f4e0 100644 --- a/apps/app/hooks/gantt-chart/module-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/module-issues-view.tsx @@ -2,6 +2,8 @@ import useSWR from "swr"; // services import modulesService from "services/modules.service"; +// hooks +import useIssuesView from "hooks/use-issues-view"; // fetch-keys import { MODULE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; @@ -10,15 +12,27 @@ const useGanttChartModuleIssues = ( projectId: string | undefined, moduleId: string | undefined ) => { + const { orderBy, filters, showSubIssues } = useIssuesView(); + + const params: any = { + order_by: orderBy, + type: filters?.type ? filters?.type : undefined, + sub_issue: showSubIssues, + start_target_date: true, + }; + // all issues under the workspace and project const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( - workspaceSlug && projectId && moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString()) : null, + workspaceSlug && projectId && moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) + : null, workspaceSlug && projectId && moduleId ? () => modulesService.getModuleIssuesWithParams( workspaceSlug.toString(), projectId.toString(), - moduleId.toString() + moduleId.toString(), + params ) : null ); diff --git a/apps/app/hooks/gantt-chart/view-issues-view.tsx b/apps/app/hooks/gantt-chart/view-issues-view.tsx index 7fc138570..b66b35128 100644 --- a/apps/app/hooks/gantt-chart/view-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/view-issues-view.tsx @@ -17,14 +17,15 @@ const useGanttChartViewIssues = ( // all issues under the view const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( - workspaceSlug && projectId && viewId ? VIEW_ISSUES(viewId.toString(), viewGanttParams) : null, + workspaceSlug && projectId && viewId + ? VIEW_ISSUES(viewId.toString(), { ...viewGanttParams, start_target_date: true }) + : null, workspaceSlug && projectId && viewId ? () => - issuesService.getIssuesWithParams( - workspaceSlug.toString(), - projectId.toString(), - viewGanttParams - ) + issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), { + ...viewGanttParams, + start_target_date: true, + }) : null ); diff --git a/apps/app/layouts/app-layout/app-sidebar.tsx b/apps/app/layouts/app-layout/app-sidebar.tsx index 7143d9cad..04cc8393a 100644 --- a/apps/app/layouts/app-layout/app-sidebar.tsx +++ b/apps/app/layouts/app-layout/app-sidebar.tsx @@ -25,6 +25,7 @@ const Sidebar: React.FC = observer(({ toggleSidebar, setToggleSide return (
{ : null ); - const { data: modules } = useSWR( + const { data: modules, mutate: mutateModules } = useSWR( workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, workspaceSlug && projectId ? () => modulesService.getModules(workspaceSlug as string, projectId as string) @@ -139,7 +139,9 @@ const ProjectModules: NextPage = () => {
)} - {modulesView === "gantt_chart" && } + {modulesView === "gantt_chart" && ( + + )}
) : ( , user: ICurrentUserResponse | undefined ): Promise { return this.patch( @@ -127,7 +127,7 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, moduleId: string, - queries?: Partial + queries?: any ): Promise< | IIssue[] | { diff --git a/apps/app/types/cycles.d.ts b/apps/app/types/cycles.d.ts index df358b7a9..955e82222 100644 --- a/apps/app/types/cycles.d.ts +++ b/apps/app/types/cycles.d.ts @@ -29,6 +29,7 @@ export interface ICycle { owned_by: IUser; project: string; project_detail: IProjectLite; + sort_order: number; start_date: string | null; started_issues: number; total_issues: number; diff --git a/apps/app/types/modules.d.ts b/apps/app/types/modules.d.ts index 96afcff48..eefd42788 100644 --- a/apps/app/types/modules.d.ts +++ b/apps/app/types/modules.d.ts @@ -43,6 +43,7 @@ export interface IModule { name: string; project: string; project_detail: IProjectLite; + sort_order: number; start_date: string | null; started_issues: number; status: "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled" | null; diff --git a/yarn.lock b/yarn.lock index 9f1999d04..01e9d2a84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2993,26 +2993,26 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69" integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== -"@sentry-internal/tracing@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.61.1.tgz#8055b7dfbf89b7089a591b27e05484d5f6773948" - integrity sha512-E8J6ZMXHGdWdmgKBK/ounuUppDK65c4Hphin6iVckDGMEATn0auYAKngeyRUMLof1167DssD8wxcIA4aBvmScA== +"@sentry-internal/tracing@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.62.0.tgz#f14400f20a32844f2895a8a333080d52fa32cd1d" + integrity sha512-LHT8i2c93JhQ1uBU1cqb5AIhmHPWlyovE4ZQjqEizk6Fk7jXc9L8kKhaIWELVPn8Xg6YtfGWhRBZk3ssj4JpfQ== dependencies: - "@sentry/core" "7.61.1" - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry/core" "7.62.0" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" tslib "^2.4.1 || ^1.9.3" -"@sentry/browser@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.61.1.tgz#ce5005ea76d4c2e91c09a43b218c25cc5e9c1340" - integrity sha512-v6Wv0O/PF+sqji+WWpJmxAlQafsiKmsXQLzKAIntVjl3HbYO5oVS3ubCyqfxSlLxIhM5JuHcEOLn6Zi3DPtpcw== +"@sentry/browser@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.62.0.tgz#0b00a0ed8e4cd4873f7ec413b094ec6b170bb085" + integrity sha512-e52EPiRtPTZv+9iFIZT3n8qNozc8ymqT0ra7QwkwbVuF9fWSCOc1gzkTa9VKd/xwcGzOfglozl2O+Zz4GtoGUg== dependencies: - "@sentry-internal/tracing" "7.61.1" - "@sentry/core" "7.61.1" - "@sentry/replay" "7.61.1" - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry-internal/tracing" "7.62.0" + "@sentry/core" "7.62.0" + "@sentry/replay" "7.62.0" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" tslib "^2.4.1 || ^1.9.3" "@sentry/cli@^1.74.6": @@ -3027,88 +3027,88 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.61.1.tgz#8043c7cecf5ca0601f6c61979fb2880ceac37287" - integrity sha512-WTRt0J33KhUbYuDQZ5G58kdsNeQ5JYrpi6o+Qz+1xTv60DQq/tBGRJ7d86SkmdnGIiTs6W1hsxAtyiLS0y9d2A== +"@sentry/core@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.62.0.tgz#3d9571741b052b1f2fa8fb8ae0088de8e79b4f4e" + integrity sha512-l6n+c3mSlWa+FhT/KBrAU1BtbaLYCljf5MuGlH6NKRpnBcrZCbzk8ZuFcSND+gr2SqxycQkhEWX1zxVHPDdZxw== dependencies: - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" tslib "^2.4.1 || ^1.9.3" -"@sentry/integrations@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.61.1.tgz#ca9bf2fc59c852f5e73543bb7e69b181a4ef2d45" - integrity sha512-mdmWzUQmW1viOiW0/Gi6AQ5LXukqhuefjzLdn5o6HMxiAgskIpNX+0+BOQ/6162/o7mHWSTNEHqEzMNTK2ppLw== +"@sentry/integrations@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.62.0.tgz#fad35d8de97890b35269d132636218ae157dab22" + integrity sha512-BNlW4xczhbL+zmmc8kFZunjKBrVYZsAltQ/gMuaHw5iiEr+chVMgQDQ2A9EVB7WEtuTJQ0XmeqofH2nAk2qYHg== dependencies: - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" localforage "^1.8.1" tslib "^2.4.1 || ^1.9.3" "@sentry/nextjs@^7.36.0": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.61.1.tgz#556bd48740dd67694ee54aaed042a22c255290ed" - integrity sha512-ssq0AX+QaDzLSeA45lQLt3OVkzUNiNsI5loMU9gq+Bsts3KOHnykturFvdrb5T3WuIucE6PsswNjZIWqP+lrMg== + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.62.0.tgz#6a5362dc03c768e8ef855ea7c26f94dddc40d7eb" + integrity sha512-Hg5D8dAgGkn+ZoTh2SSOx35hcVJUf9QO4D2FKFmPwFpnrpP/thcusE7m2k6jsUlK6jBvZhtC0rcZk26K3WsioA== dependencies: "@rollup/plugin-commonjs" "24.0.0" - "@sentry/core" "7.61.1" - "@sentry/integrations" "7.61.1" - "@sentry/node" "7.61.1" - "@sentry/react" "7.61.1" - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry/core" "7.62.0" + "@sentry/integrations" "7.62.0" + "@sentry/node" "7.62.0" + "@sentry/react" "7.62.0" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" "@sentry/webpack-plugin" "1.20.0" chalk "3.0.0" rollup "2.78.0" stacktrace-parser "^0.1.10" tslib "^2.4.1 || ^1.9.3" -"@sentry/node@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.61.1.tgz#bc49d321d0a511936f8bdd0bbd3ddc5e01b8d98c" - integrity sha512-+crVAeymXdWZcDuwU9xySf4sVv2fHOFlr13XqeXl73q4zqKJM1IX4VUO9On3+jTyGfB5SCAuBBYpzA3ehBfeYw== +"@sentry/node@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.62.0.tgz#8ccac64974748705103fccd3cf40f76003bad94a" + integrity sha512-2z1JmYV97eJ8zwshJA15hppjRdUeMhbaL8LSsbdtx7vTMmjuaIGfPR4EnI4Fhuw+J1Nnf5sE/CRKpZCCa74vXw== dependencies: - "@sentry-internal/tracing" "7.61.1" - "@sentry/core" "7.61.1" - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry-internal/tracing" "7.62.0" + "@sentry/core" "7.62.0" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" cookie "^0.4.1" https-proxy-agent "^5.0.0" lru_map "^0.3.3" tslib "^2.4.1 || ^1.9.3" -"@sentry/react@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.61.1.tgz#88a62fe9a847ffb0feeff935c49737abd7904007" - integrity sha512-n8xNT05gdERpETvq3GJZ2lP6HZYLRQQoUDc13egDzKf840MzCjle0LiLmsVhRv8AL1GnWaIPwnvTGvS4BuNlvw== +"@sentry/react@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.62.0.tgz#8fa7246ba61f57c007893d76dcd5784b4e12d34e" + integrity sha512-jCQEs6lYGQdqj6XXWdR+i5IzJMgrSzTFI/TSMSeTdAeldmppg7uuRuJlBJGaWsxoiwed539Vn3kitRswn1ugeA== dependencies: - "@sentry/browser" "7.61.1" - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry/browser" "7.62.0" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" hoist-non-react-statics "^3.3.2" tslib "^2.4.1 || ^1.9.3" -"@sentry/replay@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.61.1.tgz#20cdb5f31b5ce25a7afe11bcaaf67b1f875d2833" - integrity sha512-Nsnnzx8c+DRjnfQ0Md11KGdY21XOPa50T2B3eBEyFAhibvYEc/68PuyVWkMBQ7w9zo/JV+q6HpIXKD0THUtqZA== +"@sentry/replay@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.62.0.tgz#9131c24ae2e797ae47983834ba88b3b5c7f6e566" + integrity sha512-mSbqtV6waQAvWTG07uR211jft63HduRXdHq+1xuaKulDcZ9chOkYqOCMpL0HjRIANEiZRTDDKlIo4s+3jkY5Ug== dependencies: - "@sentry/core" "7.61.1" - "@sentry/types" "7.61.1" - "@sentry/utils" "7.61.1" + "@sentry/core" "7.62.0" + "@sentry/types" "7.62.0" + "@sentry/utils" "7.62.0" -"@sentry/types@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.61.1.tgz#225912689459c92e62f0b6e3ff145f6dbf72ff0e" - integrity sha512-CpPKL+OfwYOduRX9AT3p+Ie1fftgcCPd5WofTVVq7xeWRuerOOf2iJd0v+8yHQ25omgres1YOttDkCcvQRn4Jw== +"@sentry/types@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.62.0.tgz#f15729f656459ffa3a5998fafe9d17ee7fb1c9ff" + integrity sha512-oPy/fIT3o2VQWLTq01R2W/jt13APYMqZCVa0IT3lF9lgxzgfTbeZl3nX2FgCcc8ntDZC0dVw03dL+wLvjPqQpQ== -"@sentry/utils@7.61.1": - version "7.61.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.61.1.tgz#1545db778b7309d122a7f04eb0e803173c80c581" - integrity sha512-pUPXoiuYrTEPcBHjRizFB6eZEGm/6cTBwdWSHUjkGKvt19zuZ1ixFJQV6LrIL/AMeiQbmfQ+kTd/8SR7E9rcTQ== +"@sentry/utils@7.62.0": + version "7.62.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.62.0.tgz#915501c6056d704a9625239a1f584a7b2e4492ea" + integrity sha512-12w+Lpvn2iaocgjf6AxhtBz7XG8iFE5aMyt9BTuQp1/7sOjtEVNHlDlGrHbtPqxNCmL2SEcmNHka1panLqWHDw== dependencies: - "@sentry/types" "7.61.1" + "@sentry/types" "7.62.0" tslib "^2.4.1 || ^1.9.3" "@sentry/webpack-plugin@1.20.0": @@ -6439,9 +6439,9 @@ levn@^0.4.1: type-check "~0.4.0" lib0@^0.2.42, lib0@^0.2.74: - version "0.2.79" - resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.79.tgz#b82ee41bfab31a4358bbc0c8ad0645394149a4a9" - integrity sha512-fIdPbxzMVq10wt3ou1lp3/f9n5ciHZ6t+P1vyGy3XXr018AntTYM4eg24sNFcNq8SYDQwmhhoGdS58IlYBzfBw== + version "0.2.80" + resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.80.tgz#97f560c1240b947b825f9923fdfa45c1b4bd7cb8" + integrity sha512-1yVb13p19DrgbL7M/zQmRe/5tQrm37QlCHOssk+G8Q9qnZBh6Azfk876zhaxmKqyMnFGbQqBjH+CV0zkpr+TTw== dependencies: isomorphic.js "^0.2.4" From 88e987b902c19ed8121cd33c0edf07d779d8425e Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:03:15 +0530 Subject: [PATCH 16/33] feat: cycle and module sort order (#1841) * dev: cycle and module ordering * dev: sort order for smallest --- .../db/migrations/0042_auto_20230809_1745.py | 30 +++++++++++++++++++ apiserver/plane/db/models/cycle.py | 12 ++++++++ apiserver/plane/db/models/module.py | 12 ++++++++ 3 files changed, 54 insertions(+) create mode 100644 apiserver/plane/db/migrations/0042_auto_20230809_1745.py diff --git a/apiserver/plane/db/migrations/0042_auto_20230809_1745.py b/apiserver/plane/db/migrations/0042_auto_20230809_1745.py new file mode 100644 index 000000000..8bac2d954 --- /dev/null +++ b/apiserver/plane/db/migrations/0042_auto_20230809_1745.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.3 on 2023-08-09 12:15 +import random +from django.db import migrations + +def random_cycle_order(apps, schema_editor): + CycleModel = apps.get_model("db", "Cycle") + updated_cycles = [] + for obj in CycleModel.objects.all(): + obj.sort_order = random.randint(1, 65536) + updated_cycles.append(obj) + CycleModel.objects.bulk_update(updated_cycles, ["sort_order"], batch_size=100) + +def random_module_order(apps, schema_editor): + ModuleModel = apps.get_model("db", "Module") + updated_modules = [] + for obj in ModuleModel.objects.all(): + obj.sort_order = random.randint(1, 65536) + updated_modules.append(obj) + ModuleModel.objects.bulk_update(updated_modules, ["sort_order"], batch_size=100) + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0041_user_display_name_alter_analyticview_created_by_and_more'), + ] + + operations = [ + migrations.RunPython(random_cycle_order), + migrations.RunPython(random_module_order), + ] diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py index c8c43cef4..56301e3d3 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -17,6 +17,7 @@ class Cycle(ProjectBaseModel): related_name="owned_by_cycle", ) view_props = models.JSONField(default=dict) + sort_order = models.FloatField(default=65535) class Meta: verbose_name = "Cycle" @@ -24,6 +25,17 @@ class Cycle(ProjectBaseModel): db_table = "cycles" ordering = ("-created_at",) + def save(self, *args, **kwargs): + if self._state.adding: + smallest_sort_order = Cycle.objects.filter( + project=self.project + ).aggregate(smallest=models.Min("sort_order"))["smallest"] + + if smallest_sort_order is not None: + self.sort_order = smallest_sort_order - 10000 + + super(Cycle, self).save(*args, **kwargs) + def __str__(self): """Return name of the cycle""" return f"{self.name} <{self.project.name}>" diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index 8ad0ec838..ad1e16080 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -40,6 +40,7 @@ class Module(ProjectBaseModel): through_fields=("module", "member"), ) view_props = models.JSONField(default=dict) + sort_order = models.FloatField(default=65535) class Meta: unique_together = ["name", "project"] @@ -48,6 +49,17 @@ class Module(ProjectBaseModel): db_table = "modules" ordering = ("-created_at",) + def save(self, *args, **kwargs): + if self._state.adding: + smallest_sort_order = Module.objects.filter( + project=self.project + ).aggregate(smallest=models.Min("sort_order"))["smallest"] + + if smallest_sort_order is not None: + self.sort_order = smallest_sort_order - 10000 + + super(Module, self).save(*args, **kwargs) + def __str__(self): return f"{self.name} {self.start_date} {self.target_date}" From abec46a725f154806245721b473c0a5cb6975e2f Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:16:20 +0530 Subject: [PATCH 17/33] fix: cmdk not changing theme if active theme is custom-theme (#1842) --- .../app/components/command-palette/change-interface-theme.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/app/components/command-palette/change-interface-theme.tsx b/apps/app/components/command-palette/change-interface-theme.tsx index b34212b7f..489d8ac31 100644 --- a/apps/app/components/command-palette/change-interface-theme.tsx +++ b/apps/app/components/command-palette/change-interface-theme.tsx @@ -7,6 +7,8 @@ import { useTheme } from "next-themes"; import { SettingIcon } from "components/icons"; import userService from "services/user.service"; import useUser from "hooks/use-user"; +// helper +import { unsetCustomCssVariables } from "helpers/theme.helper"; type Props = { setIsPaletteOpen: Dispatch>; @@ -22,6 +24,8 @@ export const ChangeInterfaceTheme: React.FC = ({ setIsPaletteOpen }) => { const updateUserTheme = (newTheme: string) => { if (!user) return; + unsetCustomCssVariables(); + setTheme(newTheme); mutateUser((prevData) => { From 66170499830410a9a76bddcde4ef8cf51f0b9254 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:38:03 +0530 Subject: [PATCH 18/33] fix: sub issue endpoint for state distribution (#1845) --- apiserver/plane/api/views/issue.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 077ff4023..11c459cf0 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -758,18 +758,16 @@ class SubIssuesEndpoint(BaseAPIView): ) state_distribution = ( - State.objects.filter(~Q(name="Triage"), workspace__slug=slug) - .annotate( - state_count=Count( - "state_issue", - filter=Q(state_issue__parent_id=issue_id), - ) + State.objects.filter( + workspace__slug=slug, state_issue__parent_id=issue_id ) - .order_by("group") - .values("group", "state_count") + .annotate(state_group=F("group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") ) - result = {item["group"]: item["state_count"] for item in state_distribution} + result = {item["state_group"]: item["state_count"] for item in state_distribution} serializer = IssueLiteSerializer( sub_issues, From ad4cdcc5128224ef7bb412fe75b90bf0fe93317b Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:16:37 +0530 Subject: [PATCH 19/33] fix: cmdk modal not closing when choosing an option (#1833) --- .../app/components/command-palette/command-k.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/app/components/command-palette/command-k.tsx b/apps/app/components/command-palette/command-k.tsx index 75d7e5bcc..6f3a0e5ef 100644 --- a/apps/app/components/command-palette/command-k.tsx +++ b/apps/app/components/command-palette/command-k.tsx @@ -354,8 +354,8 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { - router.push(currentSection.path(item)); setIsPaletteOpen(false); + router.push(currentSection.path(item)); }} value={`${key}-${item?.name}`} className="focus:outline-none" @@ -379,6 +379,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { + setIsPaletteOpen(false); setPlaceholder("Change state..."); setSearchTerm(""); setPages([...pages, "change-issue-state"]); @@ -460,6 +461,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { + setIsPaletteOpen(false); const e = new KeyboardEvent("keydown", { key: "c", }); @@ -479,6 +481,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { + setIsPaletteOpen(false); const e = new KeyboardEvent("keydown", { key: "p", }); @@ -500,6 +503,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { + setIsPaletteOpen(false); const e = new KeyboardEvent("keydown", { key: "q", }); @@ -517,6 +521,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { + setIsPaletteOpen(false); const e = new KeyboardEvent("keydown", { key: "m", }); @@ -534,6 +539,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { + setIsPaletteOpen(false); const e = new KeyboardEvent("keydown", { key: "v", }); @@ -551,6 +557,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal { + setIsPaletteOpen(false); const e = new KeyboardEvent("keydown", { key: "d", }); @@ -568,11 +575,12 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal {projectDetails && projectDetails.inbox_view && ( + onSelect={() => { + setIsPaletteOpen(false); redirect( `/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}` - ) - } + ); + }} className="focus:outline-none" >
From cd5e5b96da9fce8b2d6c02f34782267b22130328 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Fri, 11 Aug 2023 17:18:33 +0530 Subject: [PATCH 20/33] feat: Mobx integration, List and Kanban boards implementation in plane space (#1844) * feat: init mobx and issue filter * feat: Implemented list and kanban views in plane space and integrated mobx. * feat: updated store type check --- apps/space/.env.example | 2 +- .../[project_slug]/layout.tsx | 33 ++++ .../[workspace_slug]/[project_slug]/page.tsx | 84 ++++++++++ .../page.tsx | 6 +- apps/space/app/layout.tsx | 10 +- apps/space/app/page.tsx | 2 + apps/space/components/icons/index.ts | 5 + .../icons/issue-group/backlog-state-icon.tsx | 23 +++ .../issue-group/cancelled-state-icon.tsx | 74 +++++++++ .../issue-group/completed-state-icon.tsx | 65 ++++++++ .../icons/issue-group/started-state-icon.tsx | 73 +++++++++ .../issue-group/unstarted-state-icon.tsx | 55 +++++++ apps/space/components/icons/types.d.ts | 6 + .../issues/board-views/block-due-date.tsx | 32 ++++ .../issues/board-views/block-labels.tsx | 17 ++ .../issues/board-views/block-priority.tsx | 17 ++ .../issues/board-views/block-state.tsx | 18 +++ .../issues/board-views/calendar/index.tsx | 1 + .../issues/board-views/gantt/index.tsx | 1 + .../issues/board-views/kanban/block.tsx | 57 +++++++ .../issues/board-views/kanban/header.tsx | 31 ++++ .../issues/board-views/kanban/index.tsx | 44 +++++ .../issues/board-views/list/block.tsx | 59 +++++++ .../issues/board-views/list/header.tsx | 31 ++++ .../issues/board-views/list/index.tsx | 38 +++++ .../issues/board-views/spreadsheet/index.tsx | 1 + .../components/issues/filters-render/date.tsx | 38 +++++ .../issues/filters-render/index.tsx | 40 +++++ .../label/filter-label-block.tsx | 34 ++++ .../issues/filters-render/label/index.tsx | 37 +++++ .../priority/filter-priority-block.tsx | 33 ++++ .../issues/filters-render/priority/index.tsx | 36 +++++ .../state/filter-state-block.tsx | 38 +++++ .../issues/filters-render/state/index.tsx | 37 +++++ apps/space/components/issues/navbar/index.tsx | 54 +++++++ .../issues/navbar/issue-board-view.tsx | 54 +++++++ .../components/issues/navbar/issue-filter.tsx | 13 ++ .../components/issues/navbar/issue-view.tsx | 13 ++ .../space/components/issues/navbar/search.tsx | 13 ++ apps/space/components/issues/navbar/theme.tsx | 28 ++++ apps/space/constants/data.ts | 153 ++++++++++++++++++ apps/space/constants/helpers.ts | 13 ++ .../lib/{mobx-store/root.ts => index.ts} | 0 apps/space/lib/mobx/store-init.tsx | 35 ++++ apps/space/lib/mobx/store-provider.tsx | 28 ++++ apps/space/package.json | 2 + apps/space/public/plane-logo.webp | Bin 0 -> 566 bytes apps/space/services/api.service.ts | 100 ++++++++++++ apps/space/services/issue.service.ts | 20 +++ apps/space/services/project.service.ts | 20 +++ apps/space/services/user.service.ts | 20 +++ apps/space/store/issue.ts | 91 +++++++++++ apps/space/store/project.ts | 69 ++++++++ apps/space/store/root.ts | 26 ++- apps/space/store/theme.ts | 33 ++++ apps/space/store/types/index.ts | 4 + apps/space/store/types/issue.ts | 72 +++++++++ apps/space/store/types/project.ts | 39 +++++ apps/space/store/types/theme.ts | 4 + apps/space/store/types/user.ts | 4 + apps/space/store/user.ts | 43 +++++ apps/space/tailwind.config.js | 1 + yarn.lock | 100 ++++++++++++ 63 files changed, 2123 insertions(+), 7 deletions(-) create mode 100644 apps/space/app/[workspace_slug]/[project_slug]/layout.tsx create mode 100644 apps/space/app/[workspace_slug]/[project_slug]/page.tsx rename apps/space/app/{[workspace_project_slug] => [workspace_slug]}/page.tsx (60%) create mode 100644 apps/space/components/icons/index.ts create mode 100644 apps/space/components/icons/issue-group/backlog-state-icon.tsx create mode 100644 apps/space/components/icons/issue-group/cancelled-state-icon.tsx create mode 100644 apps/space/components/icons/issue-group/completed-state-icon.tsx create mode 100644 apps/space/components/icons/issue-group/started-state-icon.tsx create mode 100644 apps/space/components/icons/issue-group/unstarted-state-icon.tsx create mode 100644 apps/space/components/icons/types.d.ts create mode 100644 apps/space/components/issues/board-views/block-due-date.tsx create mode 100644 apps/space/components/issues/board-views/block-labels.tsx create mode 100644 apps/space/components/issues/board-views/block-priority.tsx create mode 100644 apps/space/components/issues/board-views/block-state.tsx create mode 100644 apps/space/components/issues/board-views/calendar/index.tsx create mode 100644 apps/space/components/issues/board-views/gantt/index.tsx create mode 100644 apps/space/components/issues/board-views/kanban/block.tsx create mode 100644 apps/space/components/issues/board-views/kanban/header.tsx create mode 100644 apps/space/components/issues/board-views/kanban/index.tsx create mode 100644 apps/space/components/issues/board-views/list/block.tsx create mode 100644 apps/space/components/issues/board-views/list/header.tsx create mode 100644 apps/space/components/issues/board-views/list/index.tsx create mode 100644 apps/space/components/issues/board-views/spreadsheet/index.tsx create mode 100644 apps/space/components/issues/filters-render/date.tsx create mode 100644 apps/space/components/issues/filters-render/index.tsx create mode 100644 apps/space/components/issues/filters-render/label/filter-label-block.tsx create mode 100644 apps/space/components/issues/filters-render/label/index.tsx create mode 100644 apps/space/components/issues/filters-render/priority/filter-priority-block.tsx create mode 100644 apps/space/components/issues/filters-render/priority/index.tsx create mode 100644 apps/space/components/issues/filters-render/state/filter-state-block.tsx create mode 100644 apps/space/components/issues/filters-render/state/index.tsx create mode 100644 apps/space/components/issues/navbar/index.tsx create mode 100644 apps/space/components/issues/navbar/issue-board-view.tsx create mode 100644 apps/space/components/issues/navbar/issue-filter.tsx create mode 100644 apps/space/components/issues/navbar/issue-view.tsx create mode 100644 apps/space/components/issues/navbar/search.tsx create mode 100644 apps/space/components/issues/navbar/theme.tsx create mode 100644 apps/space/constants/data.ts create mode 100644 apps/space/constants/helpers.ts rename apps/space/lib/{mobx-store/root.ts => index.ts} (100%) create mode 100644 apps/space/lib/mobx/store-init.tsx create mode 100644 apps/space/lib/mobx/store-provider.tsx create mode 100644 apps/space/public/plane-logo.webp create mode 100644 apps/space/services/api.service.ts create mode 100644 apps/space/services/issue.service.ts create mode 100644 apps/space/services/project.service.ts create mode 100644 apps/space/services/user.service.ts create mode 100644 apps/space/store/issue.ts create mode 100644 apps/space/store/project.ts create mode 100644 apps/space/store/theme.ts create mode 100644 apps/space/store/types/index.ts create mode 100644 apps/space/store/types/issue.ts create mode 100644 apps/space/store/types/project.ts create mode 100644 apps/space/store/types/theme.ts create mode 100644 apps/space/store/types/user.ts create mode 100644 apps/space/store/user.ts diff --git a/apps/space/.env.example b/apps/space/.env.example index 7cecf3739..4fb0e4df6 100644 --- a/apps/space/.env.example +++ b/apps/space/.env.example @@ -1 +1 @@ -NEXT_PUBLIC_VERCEL_ENV=local \ No newline at end of file +NEXT_PUBLIC_API_BASE_URL='' \ No newline at end of file diff --git a/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx b/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx new file mode 100644 index 000000000..8cc3ee8c8 --- /dev/null +++ b/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx @@ -0,0 +1,33 @@ +"use client"; + +// next imports +import Link from "next/link"; +import Image from "next/image"; +// components +import IssueNavbar from "components/issues/navbar"; +import IssueFilter from "components/issues/filters-render"; + +const RootLayout = ({ children }: { children: React.ReactNode }) => ( +
+
+ +
+ {/*
+ +
*/} +
{children}
+ +
+ +
+ plane logo +
+
+ Powered by Plane Deploy +
+ +
+
+); + +export default RootLayout; diff --git a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx new file mode 100644 index 000000000..0aa9b164d --- /dev/null +++ b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useEffect } from "react"; +// next imports +import { useRouter, useParams, useSearchParams } from "next/navigation"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { IssueListView } from "components/issues/board-views/list"; +import { IssueKanbanView } from "components/issues/board-views/kanban"; +import { IssueCalendarView } from "components/issues/board-views/calendar"; +import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet"; +import { IssueGanttView } from "components/issues/board-views/gantt"; +// mobx store +import { RootStore } from "store/root"; +import { useMobxStore } from "lib/mobx/store-provider"; +// types +import { TIssueBoardKeys } from "store/types"; + +const WorkspaceProjectPage = observer(() => { + const store: RootStore = useMobxStore(); + + const router = useRouter(); + const routerParams = useParams(); + const routerSearchparams = useSearchParams(); + + const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; + const board = routerSearchparams.get("board") as TIssueBoardKeys | ""; + + // updating default board view when we are in the issues page + useEffect(() => { + if (workspace_slug && project_slug) { + if (!board) { + store.issue.setCurrentIssueBoardView("list"); + router.replace(`/${workspace_slug}/${project_slug}?board=${store?.issue?.currentIssueBoardView}`); + } else { + if (board != store?.issue?.currentIssueBoardView) store.issue.setCurrentIssueBoardView(board); + } + } + }, [workspace_slug, project_slug, board, router, store?.issue]); + + useEffect(() => { + if (workspace_slug && project_slug) { + store?.project?.getProjectSettingsAsync(workspace_slug, project_slug); + store?.issue?.getIssuesAsync(workspace_slug, project_slug); + } + }, [workspace_slug, project_slug, store?.project, store?.issue]); + + return ( +
+ {store?.issue?.loader && !store.issue.issues ? ( +
Loading...
+ ) : ( + <> + {store?.issue?.error ? ( +
Something went wrong.
+ ) : ( + store?.issue?.currentIssueBoardView && ( + <> + {store?.issue?.currentIssueBoardView === "list" && ( +
+
+ +
+
+ )} + {store?.issue?.currentIssueBoardView === "kanban" && ( +
+ +
+ )} + {store?.issue?.currentIssueBoardView === "calendar" && } + {store?.issue?.currentIssueBoardView === "spreadsheet" && } + {store?.issue?.currentIssueBoardView === "gantt" && } + + ) + )} + + )} +
+ ); +}); + +export default WorkspaceProjectPage; diff --git a/apps/space/app/[workspace_project_slug]/page.tsx b/apps/space/app/[workspace_slug]/page.tsx similarity index 60% rename from apps/space/app/[workspace_project_slug]/page.tsx rename to apps/space/app/[workspace_slug]/page.tsx index 638d36e77..c35662f5a 100644 --- a/apps/space/app/[workspace_project_slug]/page.tsx +++ b/apps/space/app/[workspace_slug]/page.tsx @@ -1,9 +1,7 @@ -import React from "react"; +"use client"; const WorkspaceProjectPage = () => ( -
- Plane Workspace project Space -
+
Plane Workspace Space
); export default WorkspaceProjectPage; diff --git a/apps/space/app/layout.tsx b/apps/space/app/layout.tsx index 5c7de32ff..b63f748e8 100644 --- a/apps/space/app/layout.tsx +++ b/apps/space/app/layout.tsx @@ -1,10 +1,18 @@ +"use client"; + // root styles import "styles/globals.css"; +// mobx store provider +import { MobxStoreProvider } from "lib/mobx/store-provider"; +import MobxStoreInit from "lib/mobx/store-init"; const RootLayout = ({ children }: { children: React.ReactNode }) => ( -
{children}
+ + +
{children}
+
); diff --git a/apps/space/app/page.tsx b/apps/space/app/page.tsx index bbfe9c3ea..c1b2926b3 100644 --- a/apps/space/app/page.tsx +++ b/apps/space/app/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; const HomePage = () => ( diff --git a/apps/space/components/icons/index.ts b/apps/space/components/icons/index.ts new file mode 100644 index 000000000..5f23e0f3a --- /dev/null +++ b/apps/space/components/icons/index.ts @@ -0,0 +1,5 @@ +export * from "./issue-group/backlog-state-icon"; +export * from "./issue-group/unstarted-state-icon"; +export * from "./issue-group/started-state-icon"; +export * from "./issue-group/completed-state-icon"; +export * from "./issue-group/cancelled-state-icon"; diff --git a/apps/space/components/icons/issue-group/backlog-state-icon.tsx b/apps/space/components/icons/issue-group/backlog-state-icon.tsx new file mode 100644 index 000000000..f2f62d24a --- /dev/null +++ b/apps/space/components/icons/issue-group/backlog-state-icon.tsx @@ -0,0 +1,23 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const BacklogStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["backlog"], +}) => ( + + + +); diff --git a/apps/space/components/icons/issue-group/cancelled-state-icon.tsx b/apps/space/components/icons/issue-group/cancelled-state-icon.tsx new file mode 100644 index 000000000..e244c191a --- /dev/null +++ b/apps/space/components/icons/issue-group/cancelled-state-icon.tsx @@ -0,0 +1,74 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const CancelledStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["cancelled"], +}) => ( + + + + + + + + + + + + + +); diff --git a/apps/space/components/icons/issue-group/completed-state-icon.tsx b/apps/space/components/icons/issue-group/completed-state-icon.tsx new file mode 100644 index 000000000..417ebbf3f --- /dev/null +++ b/apps/space/components/icons/issue-group/completed-state-icon.tsx @@ -0,0 +1,65 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const CompletedStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["completed"], +}) => ( + + + + + + + + + + + + +); diff --git a/apps/space/components/icons/issue-group/started-state-icon.tsx b/apps/space/components/icons/issue-group/started-state-icon.tsx new file mode 100644 index 000000000..4ebd1771f --- /dev/null +++ b/apps/space/components/icons/issue-group/started-state-icon.tsx @@ -0,0 +1,73 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const StartedStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["started"], +}) => ( + + + + + + + + + + + + +); diff --git a/apps/space/components/icons/issue-group/unstarted-state-icon.tsx b/apps/space/components/icons/issue-group/unstarted-state-icon.tsx new file mode 100644 index 000000000..f79bc00fc --- /dev/null +++ b/apps/space/components/icons/issue-group/unstarted-state-icon.tsx @@ -0,0 +1,55 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const UnstartedStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["unstarted"], +}) => ( + + + + + + + + + + +); diff --git a/apps/space/components/icons/types.d.ts b/apps/space/components/icons/types.d.ts new file mode 100644 index 000000000..f82a18147 --- /dev/null +++ b/apps/space/components/icons/types.d.ts @@ -0,0 +1,6 @@ +export type Props = { + width?: string | number; + height?: string | number; + color?: string; + className?: string; +}; diff --git a/apps/space/components/issues/board-views/block-due-date.tsx b/apps/space/components/issues/board-views/block-due-date.tsx new file mode 100644 index 000000000..6d3cc3cc0 --- /dev/null +++ b/apps/space/components/issues/board-views/block-due-date.tsx @@ -0,0 +1,32 @@ +"use client"; + +// helpers +import { renderDateFormat } from "constants/helpers"; + +export const findHowManyDaysLeft = (date: string | Date) => { + const today = new Date(); + const eventDate = new Date(date); + const timeDiff = Math.abs(eventDate.getTime() - today.getTime()); + return Math.ceil(timeDiff / (1000 * 3600 * 24)); +}; + +const validDate = (date: any, state: string): string => { + if (date === null || ["backlog", "unstarted", "cancelled"].includes(state)) + return `bg-gray-500/10 text-gray-500 border-gray-500/50`; + else { + const today = new Date(); + const dueDate = new Date(date); + + if (dueDate < today) return `bg-red-500/10 text-red-500 border-red-500/50`; + else return `bg-green-500/10 text-green-500 border-green-500/50`; + } +}; + +export const IssueBlockDueDate = ({ due_date, state }: any) => ( +
+ {renderDateFormat(due_date)} +
+); diff --git a/apps/space/components/issues/board-views/block-labels.tsx b/apps/space/components/issues/board-views/block-labels.tsx new file mode 100644 index 000000000..90cc1629c --- /dev/null +++ b/apps/space/components/issues/board-views/block-labels.tsx @@ -0,0 +1,17 @@ +"use client"; + +export const IssueBlockLabels = ({ labels }: any) => ( +
+ {labels && + labels.length > 0 && + labels.map((_label: any) => ( +
+
+
{_label?.name}
+
+ ))} +
+); diff --git a/apps/space/components/issues/board-views/block-priority.tsx b/apps/space/components/issues/board-views/block-priority.tsx new file mode 100644 index 000000000..61ca50765 --- /dev/null +++ b/apps/space/components/issues/board-views/block-priority.tsx @@ -0,0 +1,17 @@ +"use client"; + +// types +import { TIssuePriorityKey } from "store/types/issue"; +// constants +import { issuePriorityFilter } from "constants/data"; + +export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorityKey | null }) => { + const priority_detail = priority != null ? issuePriorityFilter(priority) : null; + + if (priority_detail === null) return <>; + return ( +
+ {priority_detail?.icon} +
+ ); +}; diff --git a/apps/space/components/issues/board-views/block-state.tsx b/apps/space/components/issues/board-views/block-state.tsx new file mode 100644 index 000000000..87cd65938 --- /dev/null +++ b/apps/space/components/issues/board-views/block-state.tsx @@ -0,0 +1,18 @@ +"use client"; + +// constants +import { issueGroupFilter } from "constants/data"; + +export const IssueBlockState = ({ state }: any) => { + const stateGroup = issueGroupFilter(state.group); + + if (stateGroup === null) return <>; + return ( +
+ +
{state?.name}
+
+ ); +}; diff --git a/apps/space/components/issues/board-views/calendar/index.tsx b/apps/space/components/issues/board-views/calendar/index.tsx new file mode 100644 index 000000000..0edeca96c --- /dev/null +++ b/apps/space/components/issues/board-views/calendar/index.tsx @@ -0,0 +1 @@ +export const IssueCalendarView = () =>
; diff --git a/apps/space/components/issues/board-views/gantt/index.tsx b/apps/space/components/issues/board-views/gantt/index.tsx new file mode 100644 index 000000000..5da924b2c --- /dev/null +++ b/apps/space/components/issues/board-views/gantt/index.tsx @@ -0,0 +1 @@ +export const IssueGanttView = () =>
; diff --git a/apps/space/components/issues/board-views/kanban/block.tsx b/apps/space/components/issues/board-views/kanban/block.tsx new file mode 100644 index 000000000..304e05612 --- /dev/null +++ b/apps/space/components/issues/board-views/kanban/block.tsx @@ -0,0 +1,57 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueBlockPriority } from "components/issues/board-views/block-priority"; +import { IssueBlockState } from "components/issues/board-views/block-state"; +import { IssueBlockLabels } from "components/issues/board-views/block-labels"; +import { IssueBlockDueDate } from "components/issues/board-views/block-due-date"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssue } from "store/types/issue"; +import { RootStore } from "store/root"; + +export const IssueListBlock = ({ issue }: { issue: IIssue }) => { + const store: RootStore = useMobxStore(); + + return ( +
+ {/* id */} +
+ {store?.project?.project?.identifier}-{issue?.sequence_id} +
+ + {/* name */} +
{issue.name}
+ + {/* priority */} +
+ {issue?.priority && ( +
+ +
+ )} + {/* state */} + {issue?.state_detail && ( +
+ +
+ )} + {/* labels */} + {issue?.label_details && issue?.label_details.length > 0 && ( +
+ +
+ )} + {/* due date */} + {issue?.target_date && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/apps/space/components/issues/board-views/kanban/header.tsx b/apps/space/components/issues/board-views/kanban/header.tsx new file mode 100644 index 000000000..43c19f5f5 --- /dev/null +++ b/apps/space/components/issues/board-views/kanban/header.tsx @@ -0,0 +1,31 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// interfaces +import { IIssueState } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { + const store: RootStore = useMobxStore(); + + const stateGroup = issueGroupFilter(state.group); + + if (stateGroup === null) return <>; + + return ( +
+
+ +
+
{state?.name}
+
+ {store.issue.getCountOfIssuesByState(state.id)} +
+
+ ); +}); diff --git a/apps/space/components/issues/board-views/kanban/index.tsx b/apps/space/components/issues/board-views/kanban/index.tsx new file mode 100644 index 000000000..d716356ff --- /dev/null +++ b/apps/space/components/issues/board-views/kanban/index.tsx @@ -0,0 +1,44 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueListHeader } from "components/issues/board-views/kanban/header"; +import { IssueListBlock } from "components/issues/board-views/kanban/block"; +// interfaces +import { IIssueState, IIssue } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueKanbanView = observer(() => { + const store: RootStore = useMobxStore(); + + return ( +
+ {store?.issue?.states && + store?.issue?.states.length > 0 && + store?.issue?.states.map((_state: IIssueState) => ( +
+
+ +
+
+ {store.issue.getFilteredIssuesByState(_state.id) && + store.issue.getFilteredIssuesByState(_state.id).length > 0 ? ( +
+ {store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( + + ))} +
+ ) : ( +
+ No Issues are available. +
+ )} +
+
+ ))} +
+ ); +}); diff --git a/apps/space/components/issues/board-views/list/block.tsx b/apps/space/components/issues/board-views/list/block.tsx new file mode 100644 index 000000000..b9dfcc6ab --- /dev/null +++ b/apps/space/components/issues/board-views/list/block.tsx @@ -0,0 +1,59 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueBlockPriority } from "components/issues/board-views/block-priority"; +import { IssueBlockState } from "components/issues/board-views/block-state"; +import { IssueBlockLabels } from "components/issues/board-views/block-labels"; +import { IssueBlockDueDate } from "components/issues/board-views/block-due-date"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssue } from "store/types/issue"; +import { RootStore } from "store/root"; + +export const IssueListBlock = ({ issue }: { issue: IIssue }) => { + const store: RootStore = useMobxStore(); + + return ( +
+
+ {/* id */} +
+ {store?.project?.project?.identifier}-{issue?.sequence_id} +
+ {/* name */} +
{issue.name}
+
+ + {/* priority */} + {issue?.priority && ( +
+ +
+ )} + + {/* state */} + {issue?.state_detail && ( +
+ +
+ )} + + {/* labels */} + {issue?.label_details && issue?.label_details.length > 0 && ( +
+ +
+ )} + + {/* due date */} + {issue?.target_date && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/space/components/issues/board-views/list/header.tsx b/apps/space/components/issues/board-views/list/header.tsx new file mode 100644 index 000000000..e87cac6f7 --- /dev/null +++ b/apps/space/components/issues/board-views/list/header.tsx @@ -0,0 +1,31 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// interfaces +import { IIssueState } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { + const store: RootStore = useMobxStore(); + + const stateGroup = issueGroupFilter(state.group); + + if (stateGroup === null) return <>; + + return ( +
+
+ +
+
{state?.name}
+
+ {store.issue.getCountOfIssuesByState(state.id)} +
+
+ ); +}); diff --git a/apps/space/components/issues/board-views/list/index.tsx b/apps/space/components/issues/board-views/list/index.tsx new file mode 100644 index 000000000..7a7ec0de1 --- /dev/null +++ b/apps/space/components/issues/board-views/list/index.tsx @@ -0,0 +1,38 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueListHeader } from "components/issues/board-views/list/header"; +import { IssueListBlock } from "components/issues/board-views/list/block"; +// interfaces +import { IIssueState, IIssue } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueListView = observer(() => { + const store: RootStore = useMobxStore(); + + return ( + <> + {store?.issue?.states && + store?.issue?.states.length > 0 && + store?.issue?.states.map((_state: IIssueState) => ( +
+ + {store.issue.getFilteredIssuesByState(_state.id) && + store.issue.getFilteredIssuesByState(_state.id).length > 0 ? ( +
+ {store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( + + ))} +
+ ) : ( +
No Issues are available.
+ )} +
+ ))} + + ); +}); diff --git a/apps/space/components/issues/board-views/spreadsheet/index.tsx b/apps/space/components/issues/board-views/spreadsheet/index.tsx new file mode 100644 index 000000000..45ebf2792 --- /dev/null +++ b/apps/space/components/issues/board-views/spreadsheet/index.tsx @@ -0,0 +1 @@ +export const IssueSpreadsheetView = () =>
; diff --git a/apps/space/components/issues/filters-render/date.tsx b/apps/space/components/issues/filters-render/date.tsx new file mode 100644 index 000000000..e01d0ae58 --- /dev/null +++ b/apps/space/components/issues/filters-render/date.tsx @@ -0,0 +1,38 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; + +const IssueDateFilter = observer(() => { + const store = useMobxStore(); + + return ( + <> +
+
Due Date
+
+ {/*
+
+ close +
+
Backlog
+
+ close +
+
*/} +
+
+ close +
+
+ + ); +}); + +export default IssueDateFilter; diff --git a/apps/space/components/issues/filters-render/index.tsx b/apps/space/components/issues/filters-render/index.tsx new file mode 100644 index 000000000..366ae1030 --- /dev/null +++ b/apps/space/components/issues/filters-render/index.tsx @@ -0,0 +1,40 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import IssueStateFilter from "./state"; +import IssueLabelFilter from "./label"; +import IssuePriorityFilter from "./priority"; +import IssueDateFilter from "./date"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueFilter = observer(() => { + const store: RootStore = useMobxStore(); + + const clearAllFilters = () => {}; + + return ( +
+ {/* state */} + {store?.issue?.states && } + {/* labels */} + {store?.issue?.labels && } + {/* priority */} + + {/* due date */} + + {/* clear all filters */} +
+
Clear all filters
+
+
+ ); +}); + +export default IssueFilter; diff --git a/apps/space/components/issues/filters-render/label/filter-label-block.tsx b/apps/space/components/issues/filters-render/label/filter-label-block.tsx new file mode 100644 index 000000000..0606bfc95 --- /dev/null +++ b/apps/space/components/issues/filters-render/label/filter-label-block.tsx @@ -0,0 +1,34 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssueLabel } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; + +export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => { + const store = useMobxStore(); + + const removeLabelFromFilter = () => {}; + + return ( +
+
+
+
+
{label?.name}
+
+ close +
+
+ ); +}); diff --git a/apps/space/components/issues/filters-render/label/index.tsx b/apps/space/components/issues/filters-render/label/index.tsx new file mode 100644 index 000000000..7d313153a --- /dev/null +++ b/apps/space/components/issues/filters-render/label/index.tsx @@ -0,0 +1,37 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { RenderIssueLabel } from "./filter-label-block"; +// interfaces +import { IIssueLabel } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueLabelFilter = observer(() => { + const store: RootStore = useMobxStore(); + + const clearLabelFilters = () => {}; + + return ( + <> +
+
Labels
+
+ {store?.issue?.labels && + store?.issue?.labels.map((_label: IIssueLabel, _index: number) => )} +
+
+ close +
+
+ + ); +}); + +export default IssueLabelFilter; diff --git a/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx b/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx new file mode 100644 index 000000000..98173fd66 --- /dev/null +++ b/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx @@ -0,0 +1,33 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssuePriorityFilters } from "store/types/issue"; + +export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => { + const store = useMobxStore(); + + const removePriorityFromFilter = () => {}; + + return ( +
+
+ {priority?.icon} +
+
{priority?.title}
+
+ close +
+
+ ); +}); diff --git a/apps/space/components/issues/filters-render/priority/index.tsx b/apps/space/components/issues/filters-render/priority/index.tsx new file mode 100644 index 000000000..2253a0be2 --- /dev/null +++ b/apps/space/components/issues/filters-render/priority/index.tsx @@ -0,0 +1,36 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { RenderIssuePriority } from "./filter-priority-block"; +// interfaces +import { IIssuePriorityFilters } from "store/types/issue"; +// constants +import { issuePriorityFilters } from "constants/data"; + +const IssuePriorityFilter = observer(() => { + const store = useMobxStore(); + + return ( + <> +
+
Priority
+
+ {issuePriorityFilters.map((_priority: IIssuePriorityFilters, _index: number) => ( + + ))} +
+
+ close +
+
{" "} + + ); +}); + +export default IssuePriorityFilter; diff --git a/apps/space/components/issues/filters-render/state/filter-state-block.tsx b/apps/space/components/issues/filters-render/state/filter-state-block.tsx new file mode 100644 index 000000000..95a4f4c70 --- /dev/null +++ b/apps/space/components/issues/filters-render/state/filter-state-block.tsx @@ -0,0 +1,38 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssueState } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; + +export const RenderIssueState = observer(({ state }: { state: IIssueState }) => { + const store = useMobxStore(); + + const stateGroup = issueGroupFilter(state.group); + + const removeStateFromFilter = () => {}; + + if (stateGroup === null) return <>; + return ( +
+
+ +
+
{state?.name}
+
+ close +
+
+ ); +}); diff --git a/apps/space/components/issues/filters-render/state/index.tsx b/apps/space/components/issues/filters-render/state/index.tsx new file mode 100644 index 000000000..fc73af381 --- /dev/null +++ b/apps/space/components/issues/filters-render/state/index.tsx @@ -0,0 +1,37 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { RenderIssueState } from "./filter-state-block"; +// interfaces +import { IIssueState } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueStateFilter = observer(() => { + const store: RootStore = useMobxStore(); + + const clearStateFilters = () => {}; + + return ( + <> +
+
State
+
+ {store?.issue?.states && + store?.issue?.states.map((_state: IIssueState, _index: number) => )} +
+
+ close +
+
+ + ); +}); + +export default IssueStateFilter; diff --git a/apps/space/components/issues/navbar/index.tsx b/apps/space/components/issues/navbar/index.tsx new file mode 100644 index 000000000..0207aaee2 --- /dev/null +++ b/apps/space/components/issues/navbar/index.tsx @@ -0,0 +1,54 @@ +"use client"; + +// components +import { NavbarSearch } from "./search"; +import { NavbarIssueBoardView } from "./issue-board-view"; +import { NavbarIssueFilter } from "./issue-filter"; +import { NavbarIssueView } from "./issue-view"; +import { NavbarTheme } from "./theme"; +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueNavbar = observer(() => { + const store: RootStore = useMobxStore(); + + return ( +
+ {/* project detail */} +
+
+ {store?.project?.project && store?.project?.project?.icon ? store?.project?.project?.icon : "😊"} +
+
+ {store?.project?.project?.name || `...`} +
+
+ + {/* issue search bar */} +
+ +
+ + {/* issue views */} +
+ +
+ + {/* issue filters */} + {/*
+ + +
*/} + + {/* theming */} + {/*
+ +
*/} +
+ ); +}); + +export default IssueNavbar; diff --git a/apps/space/components/issues/navbar/issue-board-view.tsx b/apps/space/components/issues/navbar/issue-board-view.tsx new file mode 100644 index 000000000..57c8b27c1 --- /dev/null +++ b/apps/space/components/issues/navbar/issue-board-view.tsx @@ -0,0 +1,54 @@ +"use client"; + +// next imports +import { useRouter, useParams } from "next/navigation"; +// mobx react lite +import { observer } from "mobx-react-lite"; +// constants +import { issueViews } from "constants/data"; +// interfaces +import { TIssueBoardKeys } from "store/types"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarIssueBoardView = observer(() => { + const store: RootStore = useMobxStore(); + + const router = useRouter(); + const routerParams = useParams(); + + const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; + + const handleCurrentBoardView = (boardView: TIssueBoardKeys) => { + store?.issue?.setCurrentIssueBoardView(boardView); + router.replace(`/${workspace_slug}/${project_slug}?board=${boardView}`); + }; + + return ( + <> + {store?.project?.workspaceProjectSettings && + issueViews && + issueViews.length > 0 && + issueViews.map( + (_view) => + store?.project?.workspaceProjectSettings?.views[_view?.key] && ( +
handleCurrentBoardView(_view?.key)} + title={_view?.title} + > + + {_view?.icon} + +
+ ) + )} + + ); +}); diff --git a/apps/space/components/issues/navbar/issue-filter.tsx b/apps/space/components/issues/navbar/issue-filter.tsx new file mode 100644 index 000000000..10255882d --- /dev/null +++ b/apps/space/components/issues/navbar/issue-filter.tsx @@ -0,0 +1,13 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarIssueFilter = observer(() => { + const store: RootStore = useMobxStore(); + + return
Filter
; +}); diff --git a/apps/space/components/issues/navbar/issue-view.tsx b/apps/space/components/issues/navbar/issue-view.tsx new file mode 100644 index 000000000..0a8f5c860 --- /dev/null +++ b/apps/space/components/issues/navbar/issue-view.tsx @@ -0,0 +1,13 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarIssueView = observer(() => { + const store: RootStore = useMobxStore(); + + return
View
; +}); diff --git a/apps/space/components/issues/navbar/search.tsx b/apps/space/components/issues/navbar/search.tsx new file mode 100644 index 000000000..d1cafea6a --- /dev/null +++ b/apps/space/components/issues/navbar/search.tsx @@ -0,0 +1,13 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarSearch = observer(() => { + const store: RootStore = useMobxStore(); + + return
; +}); diff --git a/apps/space/components/issues/navbar/theme.tsx b/apps/space/components/issues/navbar/theme.tsx new file mode 100644 index 000000000..c122f8478 --- /dev/null +++ b/apps/space/components/issues/navbar/theme.tsx @@ -0,0 +1,28 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarTheme = observer(() => { + const store: RootStore = useMobxStore(); + + const handleTheme = () => { + store?.theme?.setTheme(store?.theme?.theme === "light" ? "dark" : "light"); + }; + + return ( +
+ {store?.theme?.theme === "light" ? ( + dark_mode + ) : ( + light_mode + )} +
+ ); +}); diff --git a/apps/space/constants/data.ts b/apps/space/constants/data.ts new file mode 100644 index 000000000..81ccae116 --- /dev/null +++ b/apps/space/constants/data.ts @@ -0,0 +1,153 @@ +// interfaces +import { + IIssueBoardViews, + // priority + TIssuePriorityKey, + // state groups + TIssueGroupKey, + IIssuePriorityFilters, + IIssueGroup, +} from "store/types/issue"; +// icons +import { + BacklogStateIcon, + UnstartedStateIcon, + StartedStateIcon, + CompletedStateIcon, + CancelledStateIcon, +} from "components/icons"; + +// all issue views +export const issueViews: IIssueBoardViews[] = [ + { + key: "list", + title: "List View", + icon: "format_list_bulleted", + className: "", + }, + { + key: "kanban", + title: "Board View", + icon: "grid_view", + className: "", + }, + // { + // key: "calendar", + // title: "Calendar View", + // icon: "calendar_month", + // className: "", + // }, + // { + // key: "spreadsheet", + // title: "Spreadsheet View", + // icon: "table_chart", + // className: "", + // }, + // { + // key: "gantt", + // title: "Gantt Chart View", + // icon: "waterfall_chart", + // className: "rotate-90", + // }, +]; + +// issue priority filters +export const issuePriorityFilters: IIssuePriorityFilters[] = [ + { + key: "urgent", + title: "Urgent", + className: "border border-red-500/50 bg-red-500/20 text-red-500", + icon: "error", + }, + { + key: "high", + title: "High", + className: "border border-orange-500/50 bg-orange-500/20 text-orange-500", + icon: "signal_cellular_alt", + }, + { + key: "medium", + title: "Medium", + className: "border border-yellow-500/50 bg-yellow-500/20 text-yellow-500", + icon: "signal_cellular_alt_2_bar", + }, + { + key: "low", + title: "Low", + className: "border border-green-500/50 bg-green-500/20 text-green-500", + icon: "signal_cellular_alt_1_bar", + }, + { + key: "none", + title: "None", + className: "border border-gray-500/50 bg-gray-500/20 text-gray-500", + icon: "block", + }, +]; + +export const issuePriorityFilter = (priorityKey: TIssuePriorityKey): IIssuePriorityFilters | null => { + const currentIssuePriority: IIssuePriorityFilters | undefined | null = + issuePriorityFilters && issuePriorityFilters.length > 0 + ? issuePriorityFilters.find((_priority) => _priority.key === priorityKey) + : null; + + if (currentIssuePriority === undefined || currentIssuePriority === null) return null; + return { ...currentIssuePriority }; +}; + +// issue group filters +export const issueGroupColors: { + [key: string]: string; +} = { + backlog: "#d9d9d9", + unstarted: "#3f76ff", + started: "#f59e0b", + completed: "#16a34a", + cancelled: "#dc2626", +}; + +export const issueGroups: IIssueGroup[] = [ + { + key: "backlog", + title: "Backlog", + color: "#d9d9d9", + className: `border-[#d9d9d9]/50 text-[#d9d9d9] bg-[#d9d9d9]/10`, + icon: BacklogStateIcon, + }, + { + key: "unstarted", + title: "Unstarted", + color: "#3f76ff", + className: `border-[#3f76ff]/50 text-[#3f76ff] bg-[#3f76ff]/10`, + icon: UnstartedStateIcon, + }, + { + key: "started", + title: "Started", + color: "#f59e0b", + className: `border-[#f59e0b]/50 text-[#f59e0b] bg-[#f59e0b]/10`, + icon: StartedStateIcon, + }, + { + key: "completed", + title: "Completed", + color: "#16a34a", + className: `border-[#16a34a]/50 text-[#16a34a] bg-[#16a34a]/10`, + icon: CompletedStateIcon, + }, + { + key: "cancelled", + title: "Cancelled", + color: "#dc2626", + className: `border-[#dc2626]/50 text-[#dc2626] bg-[#dc2626]/10`, + icon: CancelledStateIcon, + }, +]; + +export const issueGroupFilter = (issueKey: TIssueGroupKey): IIssueGroup | null => { + const currentIssueStateGroup: IIssueGroup | undefined | null = + issueGroups && issueGroups.length > 0 ? issueGroups.find((group) => group.key === issueKey) : null; + + if (currentIssueStateGroup === undefined || currentIssueStateGroup === null) return null; + return { ...currentIssueStateGroup }; +}; diff --git a/apps/space/constants/helpers.ts b/apps/space/constants/helpers.ts new file mode 100644 index 000000000..fd4dba217 --- /dev/null +++ b/apps/space/constants/helpers.ts @@ -0,0 +1,13 @@ +export const renderDateFormat = (date: string | Date | null) => { + if (!date) return "N/A"; + + var d = new Date(date), + month = "" + (d.getMonth() + 1), + day = "" + d.getDate(), + year = d.getFullYear(); + + if (month.length < 2) month = "0" + month; + if (day.length < 2) day = "0" + day; + + return [year, month, day].join("-"); +}; diff --git a/apps/space/lib/mobx-store/root.ts b/apps/space/lib/index.ts similarity index 100% rename from apps/space/lib/mobx-store/root.ts rename to apps/space/lib/index.ts diff --git a/apps/space/lib/mobx/store-init.tsx b/apps/space/lib/mobx/store-init.tsx new file mode 100644 index 000000000..2ba2f9024 --- /dev/null +++ b/apps/space/lib/mobx/store-init.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; +// next imports +import { useSearchParams } from "next/navigation"; +// interface +import { TIssueBoardKeys } from "store/types"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const MobxStoreInit = () => { + const store: RootStore = useMobxStore(); + + // search params + const routerSearchparams = useSearchParams(); + + const board = routerSearchparams.get("board") as TIssueBoardKeys; + + useEffect(() => { + // theme + const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light"; + if (_theme && store?.theme?.theme != _theme) store.theme.setTheme(_theme); + else localStorage.setItem("app_theme", _theme && _theme != "light" ? "dark" : "light"); + }, [store?.theme]); + + // updating default board view when we are in the issues page + useEffect(() => { + if (board && board != store?.issue?.currentIssueBoardView) store.issue.setCurrentIssueBoardView(board); + }, [board, store?.issue]); + + return <>; +}; + +export default MobxStoreInit; diff --git a/apps/space/lib/mobx/store-provider.tsx b/apps/space/lib/mobx/store-provider.tsx new file mode 100644 index 000000000..c6fde14ae --- /dev/null +++ b/apps/space/lib/mobx/store-provider.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { createContext, useContext } from "react"; +// mobx store +import { RootStore } from "store/root"; + +let rootStore: RootStore = new RootStore(); + +export const MobxStoreContext = createContext(rootStore); + +const initializeStore = () => { + const _rootStore: RootStore = rootStore ?? new RootStore(); + if (typeof window === "undefined") return _rootStore; + if (!rootStore) rootStore = _rootStore; + return _rootStore; +}; + +export const MobxStoreProvider = ({ children }: any) => { + const store: RootStore = initializeStore(); + return {children}; +}; + +// hook +export const useMobxStore = () => { + const context = useContext(MobxStoreContext); + if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider"); + return context; +}; diff --git a/apps/space/package.json b/apps/space/package.json index 7ace168aa..4af31d312 100644 --- a/apps/space/package.json +++ b/apps/space/package.json @@ -18,6 +18,8 @@ "eslint": "8.34.0", "eslint-config-next": "13.2.1", "js-cookie": "^3.0.1", + "mobx": "^6.10.0", + "mobx-react-lite": "^4.0.3", "next": "^13.4.13", "nprogress": "^0.2.0", "react": "^18.2.0", diff --git a/apps/space/public/plane-logo.webp b/apps/space/public/plane-logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..52e7c98da212b2a8e8c45d22bf22a7f1b15def4c GIT binary patch literal 566 zcmV-60?GYSNk&F40ssJ4MM6+kP&il$0000G0001*005c*06|PpNJIbt00B2Z5I~ah zrlUMT8W9n=-hfdgNs5;LD1deC9{4v>|B2}T1e_5;f3+iN3l$~W$ZBA;_)q*NeMm5w z#ch-pP6cVN0#;BsAjAOx01yxWodGJF0Gj|lkwTqHrK6&uryn=~uo4MnZs5QGW(=^u zfY*xY|C5|KWGVka=C}L?A&hpgB|-HBKA?xx5c+~2Ai^Yv7d%W+*Ii*74nP3@ zo*EZfoz@0}fNsXm<}Ht*v*sznAZM|(L$l&)^rw{S{to*zVuRP+#sARt`px8wJJeSE zLcExsU!SACool6k8)Y&+vKU~GeJJH$70|bdW*tA4b#C^)f-Z31u!uZF1KPX)D9B+t zZBCZYLz=ly|AmKHYK{9`u97EU33f#^-eJ7;)6^lO*`aas6Zp_-&*}Yk2uh%ZJ^q+x zrC8x}IfOCS>Azyp9ma0b*T*#D*XA46P%rWG;&Shn*9 { + return axios({ + method: "get", + url: this.baseURL + url, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + post(url: string, data = {}, config = {}): Promise { + return axios({ + method: "post", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + put(url: string, data = {}, config = {}): Promise { + return axios({ + method: "put", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + patch(url: string, data = {}, config = {}): Promise { + return axios({ + method: "patch", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + delete(url: string, data?: any, config = {}): Promise { + return axios({ + method: "delete", + url: this.baseURL + url, + data: data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + request(config = {}) { + return axios(config); + } +} + +export default APIService; diff --git a/apps/space/services/issue.service.ts b/apps/space/services/issue.service.ts new file mode 100644 index 000000000..4b40bdf5c --- /dev/null +++ b/apps/space/services/issue.service.ts @@ -0,0 +1,20 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +class IssueService extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async getPublicIssues(workspace_slug: string, project_slug: string): Promise { + return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/issues/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default IssueService; diff --git a/apps/space/services/project.service.ts b/apps/space/services/project.service.ts new file mode 100644 index 000000000..4d973051f --- /dev/null +++ b/apps/space/services/project.service.ts @@ -0,0 +1,20 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +class ProjectService extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async getProjectSettingsAsync(workspace_slug: string, project_slug: string): Promise { + return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default ProjectService; diff --git a/apps/space/services/user.service.ts b/apps/space/services/user.service.ts new file mode 100644 index 000000000..d724374b6 --- /dev/null +++ b/apps/space/services/user.service.ts @@ -0,0 +1,20 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +class UserService extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async currentUser(): Promise { + return this.get("/api/users/me/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default UserService; diff --git a/apps/space/store/issue.ts b/apps/space/store/issue.ts new file mode 100644 index 000000000..79ad4b910 --- /dev/null +++ b/apps/space/store/issue.ts @@ -0,0 +1,91 @@ +// mobx +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// service +import IssueService from "services/issue.service"; +// types +import { TIssueBoardKeys } from "store/types/issue"; +import { IIssueStore, IIssue, IIssueState, IIssueLabel } from "./types"; + +class IssueStore implements IIssueStore { + currentIssueBoardView: TIssueBoardKeys | null = null; + + loader: boolean = false; + error: any | null = null; + + states: IIssueState[] | null = null; + labels: IIssueLabel[] | null = null; + issues: IIssue[] | null = null; + + userSelectedStates: string[] = []; + userSelectedLabels: string[] = []; + // root store + rootStore; + // service + issueService; + + constructor(_rootStore: any) { + makeObservable(this, { + // observable + currentIssueBoardView: observable, + + loader: observable, + error: observable, + + states: observable.ref, + labels: observable.ref, + issues: observable.ref, + + userSelectedStates: observable, + userSelectedLabels: observable, + // action + setCurrentIssueBoardView: action, + getIssuesAsync: action, + // computed + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + } + + // computed + getCountOfIssuesByState(state_id: string): number { + return this.issues?.filter((issue) => issue.state == state_id).length || 0; + } + + getFilteredIssuesByState(state_id: string): IIssue[] | [] { + return this.issues?.filter((issue) => issue.state == state_id) || []; + } + + // action + setCurrentIssueBoardView = async (view: TIssueBoardKeys) => { + this.currentIssueBoardView = view; + }; + + getIssuesAsync = async (workspace_slug: string, project_slug: string) => { + try { + this.loader = true; + this.error = null; + + const response = await this.issueService.getPublicIssues(workspace_slug, project_slug); + + if (response) { + const _states: IIssueState[] = [...response?.states]; + const _labels: IIssueLabel[] = [...response?.labels]; + const _issues: IIssue[] = [...response?.issues]; + runInAction(() => { + this.states = _states; + this.labels = _labels; + this.issues = _issues; + this.loader = false; + }); + return response; + } + } catch (error) { + this.loader = false; + this.error = error; + return error; + } + }; +} + +export default IssueStore; diff --git a/apps/space/store/project.ts b/apps/space/store/project.ts new file mode 100644 index 000000000..e5ac58261 --- /dev/null +++ b/apps/space/store/project.ts @@ -0,0 +1,69 @@ +// mobx +import { observable, action, makeObservable, runInAction } from "mobx"; +// service +import ProjectService from "services/project.service"; +// types +import { IProjectStore, IWorkspace, IProject, IProjectSettings } from "./types"; + +class ProjectStore implements IProjectStore { + loader: boolean = false; + error: any | null = null; + + workspace: IWorkspace | null = null; + project: IProject | null = null; + workspaceProjectSettings: IProjectSettings | null = null; + // root store + rootStore; + // service + projectService; + + constructor(_rootStore: any | null = null) { + makeObservable(this, { + // observable + workspace: observable.ref, + project: observable.ref, + workspaceProjectSettings: observable.ref, + loader: observable, + error: observable.ref, + // action + getProjectSettingsAsync: action, + // computed + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + } + + getProjectSettingsAsync = async (workspace_slug: string, project_slug: string) => { + try { + this.loader = true; + this.error = null; + + const response = await this.projectService.getProjectSettingsAsync(workspace_slug, project_slug); + + if (response) { + const _project: IProject = { ...response?.project_details }; + const _workspace: IWorkspace = { ...response?.workspace_detail }; + const _workspaceProjectSettings: IProjectSettings = { + comments: response?.comments, + reactions: response?.reactions, + votes: response?.votes, + views: { ...response?.views }, + }; + runInAction(() => { + this.project = _project; + this.workspace = _workspace; + this.workspaceProjectSettings = _workspaceProjectSettings; + this.loader = false; + }); + } + return response; + } catch (error) { + this.loader = false; + this.error = error; + return error; + } + }; +} + +export default ProjectStore; diff --git a/apps/space/store/root.ts b/apps/space/store/root.ts index a10356821..dd6d620c0 100644 --- a/apps/space/store/root.ts +++ b/apps/space/store/root.ts @@ -1 +1,25 @@ -export const init = {}; +// mobx lite +import { enableStaticRendering } from "mobx-react-lite"; +// store imports +import UserStore from "./user"; +import ThemeStore from "./theme"; +import IssueStore from "./issue"; +import ProjectStore from "./project"; +// types +import { IIssueStore, IProjectStore, IThemeStore, IUserStore } from "./types"; + +enableStaticRendering(typeof window === "undefined"); + +export class RootStore { + user: IUserStore; + theme: IThemeStore; + issue: IIssueStore; + project: IProjectStore; + + constructor() { + this.user = new UserStore(this); + this.theme = new ThemeStore(this); + this.issue = new IssueStore(this); + this.project = new ProjectStore(this); + } +} diff --git a/apps/space/store/theme.ts b/apps/space/store/theme.ts new file mode 100644 index 000000000..809d56b97 --- /dev/null +++ b/apps/space/store/theme.ts @@ -0,0 +1,33 @@ +// mobx +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// types +import { IThemeStore } from "./types"; + +class ThemeStore implements IThemeStore { + theme: "light" | "dark" = "light"; + // root store + rootStore; + + constructor(_rootStore: any | null = null) { + makeObservable(this, { + // observable + theme: observable, + // action + setTheme: action, + // computed + }); + + this.rootStore = _rootStore; + } + + setTheme = async (_theme: "light" | "dark" | string) => { + try { + localStorage.setItem("app_theme", _theme); + this.theme = _theme === "light" ? "light" : "dark"; + } catch (error) { + console.error("setting user theme error", error); + } + }; +} + +export default ThemeStore; diff --git a/apps/space/store/types/index.ts b/apps/space/store/types/index.ts new file mode 100644 index 000000000..5a0a51eda --- /dev/null +++ b/apps/space/store/types/index.ts @@ -0,0 +1,4 @@ +export * from "./user"; +export * from "./theme"; +export * from "./project"; +export * from "./issue"; diff --git a/apps/space/store/types/issue.ts b/apps/space/store/types/issue.ts new file mode 100644 index 000000000..5feeba7bd --- /dev/null +++ b/apps/space/store/types/issue.ts @@ -0,0 +1,72 @@ +export type TIssueBoardKeys = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; + +export interface IIssueBoardViews { + key: TIssueBoardKeys; + title: string; + icon: string; + className: string; +} + +export type TIssuePriorityKey = "urgent" | "high" | "medium" | "low" | "none"; +export type TIssuePriorityTitle = "Urgent" | "High" | "Medium" | "Low" | "None"; +export interface IIssuePriorityFilters { + key: TIssuePriorityKey; + title: TIssuePriorityTitle; + className: string; + icon: string; +} + +export type TIssueGroupKey = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; +export type TIssueGroupTitle = "Backlog" | "Unstarted" | "Started" | "Completed" | "Cancelled"; + +export interface IIssueGroup { + key: TIssueGroupKey; + title: TIssueGroupTitle; + color: string; + className: string; + icon: React.FC; +} + +export interface IIssue { + id: string; + sequence_id: number; + name: string; + description_html: string; + priority: TIssuePriorityKey | null; + state: string; + state_detail: any; + label_details: any; + target_date: any; +} + +export interface IIssueState { + id: string; + name: string; + group: TIssueGroupKey; + color: string; +} + +export interface IIssueLabel { + id: string; + name: string; + color: string; +} + +export interface IIssueStore { + currentIssueBoardView: TIssueBoardKeys | null; + loader: boolean; + error: any | null; + + states: IIssueState[] | null; + labels: IIssueLabel[] | null; + issues: IIssue[] | null; + + userSelectedStates: string[]; + userSelectedLabels: string[]; + + getCountOfIssuesByState: (state: string) => number; + getFilteredIssuesByState: (state: string) => IIssue[]; + + setCurrentIssueBoardView: (view: TIssueBoardKeys) => void; + getIssuesAsync: (workspace_slug: string, project_slug: string) => Promise; +} diff --git a/apps/space/store/types/project.ts b/apps/space/store/types/project.ts new file mode 100644 index 000000000..a55f30be0 --- /dev/null +++ b/apps/space/store/types/project.ts @@ -0,0 +1,39 @@ +export interface IWorkspace { + id: string; + name: string; + slug: string; +} + +export interface IProject { + id: string; + identifier: string; + name: string; + icon: string; + cover_image: string | null; + icon_prop: string | null; + emoji: string | null; +} + +export interface IProjectSettings { + comments: boolean; + reactions: boolean; + votes: boolean; + views: { + list: boolean; + gantt: boolean; + kanban: boolean; + calendar: boolean; + spreadsheet: boolean; + }; +} + +export interface IProjectStore { + loader: boolean; + error: any | null; + + workspace: IWorkspace | null; + project: IProject | null; + workspaceProjectSettings: IProjectSettings | null; + + getProjectSettingsAsync: (workspace_slug: string, project_slug: string) => Promise; +} diff --git a/apps/space/store/types/theme.ts b/apps/space/store/types/theme.ts new file mode 100644 index 000000000..ca306be51 --- /dev/null +++ b/apps/space/store/types/theme.ts @@ -0,0 +1,4 @@ +export interface IThemeStore { + theme: string; + setTheme: (theme: "light" | "dark" | string) => void; +} diff --git a/apps/space/store/types/user.ts b/apps/space/store/types/user.ts new file mode 100644 index 000000000..0293c5381 --- /dev/null +++ b/apps/space/store/types/user.ts @@ -0,0 +1,4 @@ +export interface IUserStore { + currentUser: any | null; + getUserAsync: () => void; +} diff --git a/apps/space/store/user.ts b/apps/space/store/user.ts new file mode 100644 index 000000000..2f4782236 --- /dev/null +++ b/apps/space/store/user.ts @@ -0,0 +1,43 @@ +// mobx +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// service +import UserService from "services/user.service"; +// types +import { IUserStore } from "./types"; + +class UserStore implements IUserStore { + currentUser: any | null = null; + // root store + rootStore; + // service + userService; + + constructor(_rootStore: any) { + makeObservable(this, { + // observable + currentUser: observable, + // actions + // computed + }); + this.rootStore = _rootStore; + this.userService = new UserService(); + } + + getUserAsync = async () => { + try { + const response = this.userService.currentUser(); + if (response) { + runInAction(() => { + this.currentUser = response; + }); + } + } catch (error) { + console.error("error", error); + runInAction(() => { + // render error actions + }); + } + }; +} + +export default UserStore; diff --git a/apps/space/tailwind.config.js b/apps/space/tailwind.config.js index 145c65b5a..55aaa9a31 100644 --- a/apps/space/tailwind.config.js +++ b/apps/space/tailwind.config.js @@ -6,6 +6,7 @@ module.exports = { "./pages/**/*.{js,ts,jsx,tsx}", "./layouts/**/*.tsx", "./components/**/*.{js,ts,jsx,tsx}", + "./constants/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { diff --git a/yarn.lock b/yarn.lock index 01e9d2a84..578f10026 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3617,6 +3617,14 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + a11y-status@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/a11y-status/-/a11y-status-2.0.1.tgz#a7883105910b9e3cd09ea90e5acf8404dc01b47e" @@ -3641,6 +3649,11 @@ acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -5180,6 +5193,56 @@ eslint@8.34.0: strip-json-comments "^3.1.0" text-table "^0.2.0" +eslint-visitor-keys@^3.4.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" + integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== + +eslint@8.34.0: + version "8.34.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" + integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== + dependencies: + "@eslint/eslintrc" "^1.4.1" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + eslint@^7.23.0, eslint@^7.32.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -5397,6 +5460,17 @@ fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.0: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -6247,6 +6321,13 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -7569,6 +7650,15 @@ postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.23: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.21: + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -8619,6 +8709,11 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -9049,6 +9144,11 @@ tslib@~2.5.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== +tslib@^2.5.0, tslib@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" + integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From 7becec4ee955d8699c6ceee80dfadb703e93401d Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:42:47 +0530 Subject: [PATCH 21/33] feat: randomize color on label create (#1839) fix: create label state being persisted after edit label --- .../components/labels/create-label-modal.tsx | 7 + .../labels/create-update-label-inline.tsx | 325 +++++++++--------- apps/app/constants/label.ts | 17 + .../projects/[projectId]/settings/labels.tsx | 5 + 4 files changed, 199 insertions(+), 155 deletions(-) create mode 100644 apps/app/constants/label.ts diff --git a/apps/app/components/labels/create-label-modal.tsx b/apps/app/components/labels/create-label-modal.tsx index 7af86888d..190b9e832 100644 --- a/apps/app/components/labels/create-label-modal.tsx +++ b/apps/app/components/labels/create-label-modal.tsx @@ -20,6 +20,7 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline"; import type { ICurrentUserResponse, IIssueLabels, IState } from "types"; // constants import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; // types type Props = { @@ -52,10 +53,15 @@ export const CreateLabelModal: React.FC = ({ watch, control, reset, + setValue, } = useForm({ defaultValues, }); + useEffect(() => { + if (isOpen) setValue("color", getRandomLabelColor()); + }, [setValue, isOpen]); + const onClose = () => { handleClose(); reset(defaultValues); @@ -156,6 +162,7 @@ export const CreateLabelModal: React.FC = ({ render={({ field: { value, onChange } }) => ( { onChange(value.hex); close(); diff --git a/apps/app/components/labels/create-update-label-inline.tsx b/apps/app/components/labels/create-update-label-inline.tsx index ca4fdf3dc..6306d14ca 100644 --- a/apps/app/components/labels/create-update-label-inline.tsx +++ b/apps/app/components/labels/create-update-label-inline.tsx @@ -22,12 +22,14 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; type Props = { labelForm: boolean; setLabelForm: React.Dispatch>; isUpdating: boolean; labelToUpdate: IIssueLabels | null; + onClose?: () => void; }; const defaultValues: Partial = { @@ -35,167 +37,180 @@ const defaultValues: Partial = { color: "rgb(var(--color-text-200))", }; -type Ref = HTMLDivElement; +export const CreateUpdateLabelInline = forwardRef( + function CreateUpdateLabelInline(props, ref) { + const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; -export const CreateUpdateLabelInline = forwardRef(function CreateUpdateLabelInline( - { labelForm, setLabelForm, isUpdating, labelToUpdate }, - ref -) { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; - const { user } = useUserAuth(); + const { user } = useUserAuth(); - const { - handleSubmit, - control, - register, - reset, - formState: { errors, isSubmitting }, - watch, - setValue, - } = useForm({ - defaultValues, - }); + const { + handleSubmit, + control, + register, + reset, + formState: { errors, isSubmitting }, + watch, + setValue, + } = useForm({ + defaultValues, + }); - const handleLabelCreate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; + const handleClose = () => { + setLabelForm(false); + reset(defaultValues); + if (onClose) onClose(); + }; - await issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) - .then((res) => { - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => [res, ...(prevData ?? [])], - false + const handleLabelCreate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + + await issuesService + .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) + .then((res) => { + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => [res, ...(prevData ?? [])], + false + ); + handleClose(); + }); + }; + + const handleLabelUpdate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + + await issuesService + .patchIssueLabel( + workspaceSlug as string, + projectId as string, + labelToUpdate?.id ?? "", + formData, + user + ) + .then(() => { + reset(defaultValues); + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => + prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)), + false + ); + handleClose(); + }); + }; + + useEffect(() => { + if (!labelForm && isUpdating) return; + + reset(); + }, [labelForm, isUpdating, reset]); + + useEffect(() => { + if (!labelToUpdate) return; + + setValue( + "color", + labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000" + ); + setValue("name", labelToUpdate.name); + }, [labelToUpdate, setValue]); + + useEffect(() => { + if (labelToUpdate) { + setValue( + "color", + labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000" ); - reset(defaultValues); - setLabelForm(false); - }); - }; + return; + } - const handleLabelUpdate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; + setValue("color", getRandomLabelColor()); + }, [labelToUpdate, setValue]); - await issuesService - .patchIssueLabel( - workspaceSlug as string, - projectId as string, - labelToUpdate?.id ?? "", - formData, - user - ) - .then(() => { - reset(defaultValues); - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => - prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)), - false - ); - setLabelForm(false); - }); - }; - - useEffect(() => { - if (!labelForm && isUpdating) return; - - reset(); - }, [labelForm, isUpdating, reset]); - - useEffect(() => { - if (!labelToUpdate) return; - - setValue( - "color", - labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000" - ); - setValue("name", labelToUpdate.name); - }, [labelToUpdate, setValue]); - - return ( -
-
- - {({ open }) => ( - <> - - - - - - - ( - onChange(value.hex)} /> - )} - /> - - - - )} - -
-
- -
- { - reset(); - setLabelForm(false); - }} + return ( +
- Cancel - - {isUpdating ? ( - - {isSubmitting ? "Updating" : "Update"} - - ) : ( - - {isSubmitting ? "Adding" : "Add"} - - )} -
- ); -}); +
+ + {({ open }) => ( + <> + + + + + + + ( + onChange(value.hex)} + /> + )} + /> + + + + )} + +
+
+ +
+ handleClose()}>Cancel + {isUpdating ? ( + + {isSubmitting ? "Updating" : "Update"} + + ) : ( + + {isSubmitting ? "Adding" : "Add"} + + )} +
+ ); + } +); diff --git a/apps/app/constants/label.ts b/apps/app/constants/label.ts new file mode 100644 index 000000000..220e56209 --- /dev/null +++ b/apps/app/constants/label.ts @@ -0,0 +1,17 @@ +export const LABEL_COLOR_OPTIONS = [ + "#FF6900", + "#FCB900", + "#7BDCB5", + "#00D084", + "#8ED1FC", + "#0693E3", + "#ABB8C3", + "#EB144C", + "#F78DA7", + "#9900EF", +]; + +export const getRandomLabelColor = () => { + const randomIndex = Math.floor(Math.random() * LABEL_COLOR_OPTIONS.length); + return LABEL_COLOR_OPTIONS[randomIndex]; +}; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 3f4495ef5..dc845da68 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -133,6 +133,11 @@ const LabelsSettings: NextPage = () => { setLabelForm={setLabelForm} isUpdating={isUpdating} labelToUpdate={labelToUpdate} + onClose={() => { + setLabelForm(false); + setIsUpdating(false); + setLabelToUpdate(null); + }} ref={scrollToRef} /> )} From ac6d2b01393d61fa604fee26a8a8a4086142a94d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:29:24 +0530 Subject: [PATCH 22/33] chore: updated user permission for project and workspace settings (#1807) * fix: updated user permission for project and workspace settings * chore: workspace delete modal fix --- .../workspace/delete-workspace-modal.tsx | 4 +- .../projects/[projectId]/settings/index.tsx | 65 +++++++++++-------- .../pages/[workspaceSlug]/settings/index.tsx | 47 +++++++++----- 3 files changed, 71 insertions(+), 45 deletions(-) diff --git a/apps/app/components/workspace/delete-workspace-modal.tsx b/apps/app/components/workspace/delete-workspace-modal.tsx index 54ddedc3b..b896a87e7 100644 --- a/apps/app/components/workspace/delete-workspace-modal.tsx +++ b/apps/app/components/workspace/delete-workspace-modal.tsx @@ -50,8 +50,10 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace; const handleClose = () => { - onClose(); setIsDeleteLoading(false); + setConfirmWorkspaceName(""); + setConfirmDeleteMyWorkspace(false); + onClose(); }; const handleDeletion = async () => { diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 7de91c823..4287cee5d 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -34,7 +34,7 @@ import { truncateText } from "helpers/string.helper"; import { IProject, IWorkspace } from "types"; import type { NextPage } from "next"; // fetch-keys -import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; +import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys"; // constants import { NETWORK_CHOICES } from "constants/project"; @@ -62,6 +62,13 @@ const GeneralSettings: NextPage = () => { : null ); + const { data: memberDetails, error } = useSWR( + workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, + workspaceSlug && projectId + ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) + : null + ); + const { register, handleSubmit, @@ -157,6 +164,8 @@ const GeneralSettings: NextPage = () => { const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network); + const isAdmin = memberDetails?.role === 20; + return ( {
{projectDetails ? ( - + {isSubmitting ? "Updating Project..." : "Update Project"} ) : ( @@ -364,32 +373,34 @@ const GeneralSettings: NextPage = () => { )}
-
-
-

Danger Zone

-

- The danger zone of the project delete page is a critical area that requires careful - consideration and attention. When deleting a project, all of the data and resources - within that project will be permanently removed and cannot be recovered. -

+ {memberDetails?.role === 20 && ( +
+
+

Danger Zone

+

+ The danger zone of the project delete page is a critical area that requires + careful consideration and attention. When deleting a project, all of the data and + resources within that project will be permanently removed and cannot be recovered. +

+
+
+ {projectDetails ? ( +
+ setSelectedProject(projectDetails.id ?? null)} + outline + > + Delete Project + +
+ ) : ( + + + + )} +
-
- {projectDetails ? ( -
- setSelectedProject(projectDetails.id ?? null)} - outline - > - Delete Project - -
- ) : ( - - - - )} -
-
+ )}
diff --git a/apps/app/pages/[workspaceSlug]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/settings/index.tsx index af46e0884..a254c7a49 100644 --- a/apps/app/pages/[workspaceSlug]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/index.tsx @@ -28,7 +28,7 @@ import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import type { IWorkspace } from "types"; import type { NextPage } from "next"; // fetch-keys -import { WORKSPACE_DETAILS, USER_WORKSPACES } from "constants/fetch-keys"; +import { WORKSPACE_DETAILS, USER_WORKSPACES, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; // constants import { ORGANIZATION_SIZE } from "constants/workspace"; @@ -50,6 +50,11 @@ const WorkspaceSettings: NextPage = () => { const { user } = useUserAuth(); + const { data: memberDetails } = useSWR( + workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug.toString()) : null, + workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug.toString()) : null + ); + const { setToastAlert } = useToast(); const { data: activeWorkspace } = useSWR( @@ -142,6 +147,8 @@ const WorkspaceSettings: NextPage = () => { }); }; + const isAdmin = memberDetails?.role === 20; + return ( {
- + {isSubmitting ? "Updating..." : "Update Workspace"}
-
-
-

Danger Zone

-

- The danger zone of the workspace delete page is a critical area that requires - careful consideration and attention. When deleting a workspace, all of the data - and resources within that workspace will be permanently removed and cannot be - recovered. -

+ {memberDetails?.role === 20 && ( +
+
+

Danger Zone

+

+ The danger zone of the workspace delete page is a critical area that requires + careful consideration and attention. When deleting a workspace, all of the data + and resources within that workspace will be permanently removed and cannot be + recovered. +

+
+
+ setIsOpen(true)} outline> + Delete the workspace + +
-
- setIsOpen(true)} outline> - Delete the workspace - -
-
+ )}
) : (
From a3d99100ee04dd4cddeb2e7a4344d1f72d01154b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 19:05:13 +0530 Subject: [PATCH 23/33] fix: sub issue progress indicator fix (#1847) --- apps/app/components/issues/sub-issues-list.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx index 7d526d661..9ba920ff5 100644 --- a/apps/app/components/issues/sub-issues-list.tsx +++ b/apps/app/components/issues/sub-issues-list.tsx @@ -93,13 +93,14 @@ export const SubIssuesList: FC = ({ parentIssue, user, disabled = false } }); }; - const completedSubIssues = subIssuesResponse - ? subIssuesResponse.state_distribution.completed + - subIssuesResponse.state_distribution.cancelled - : 0; + const completedSubIssue = subIssuesResponse?.state_distribution.completed ?? 0; + const cancelledSubIssue = subIssuesResponse?.state_distribution.cancelled ?? 0; + + const totalCompletedSubIssues = completedSubIssue + cancelledSubIssue; + const totalSubIssues = subIssuesResponse ? subIssuesResponse.sub_issues.length : 0; - const completionPercentage = (completedSubIssues / totalSubIssues) * 100; + const completionPercentage = (totalCompletedSubIssues / totalSubIssues) * 100; const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled; From 5f5790ebf9d2003612afe8bd5888264132e2b0b3 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 19:14:54 +0530 Subject: [PATCH 24/33] chore: start date option added in spreadsheet view (#1824) --- .../core/views/spreadsheet-view/single-issue.tsx | 14 ++++++++++++++ .../components/issues/view-select/start-date.tsx | 2 +- apps/app/constants/spreadsheet.ts | 8 ++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx b/apps/app/components/core/views/spreadsheet-view/single-issue.tsx index 5d9eb7c31..7e8fd0d26 100644 --- a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx +++ b/apps/app/components/core/views/spreadsheet-view/single-issue.tsx @@ -12,6 +12,7 @@ import { ViewEstimateSelect, ViewIssueLabel, ViewPrioritySelect, + ViewStartDateSelect, ViewStateSelect, } from "components/issues"; import { Popover2 } from "@blueprintjs/popover2"; @@ -315,6 +316,19 @@ export const SingleSpreadsheetIssue: React.FC = ({
)} + {properties.start_date && ( +
+ +
+ )} + {properties.due_date && (
= ({ >
{ partialUpdateIssue( diff --git a/apps/app/constants/spreadsheet.ts b/apps/app/constants/spreadsheet.ts index 0dcab7220..df6cd5aef 100644 --- a/apps/app/constants/spreadsheet.ts +++ b/apps/app/constants/spreadsheet.ts @@ -43,6 +43,14 @@ export const SPREADSHEET_COLUMN = [ ascendingOrder: "labels__name", descendingOrder: "-labels__name", }, + { + propertyName: "start_date", + colName: "Start Date", + colSize: "128px", + icon: CalendarDaysIcon, + ascendingOrder: "-start_date", + descendingOrder: "start_date", + }, { propertyName: "due_date", colName: "Due Date", From c6eea9c7a9d74a7f24820e064a67aecf94aacaf6 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 11 Aug 2023 19:17:02 +0530 Subject: [PATCH 25/33] fix: empty groups not appearing in the kanban view (#1843) --- apps/app/hooks/my-issues/use-my-issues.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/app/hooks/my-issues/use-my-issues.tsx b/apps/app/hooks/my-issues/use-my-issues.tsx index 6d39e548f..aae05bc84 100644 --- a/apps/app/hooks/my-issues/use-my-issues.tsx +++ b/apps/app/hooks/my-issues/use-my-issues.tsx @@ -52,8 +52,23 @@ const useMyIssues = (workspaceSlug: string | undefined) => { allIssues: myIssues, }; + if (groupBy === "state_detail.group") { + return myIssues + ? Object.assign( + { + backlog: [], + unstarted: [], + started: [], + completed: [], + cancelled: [], + }, + myIssues + ) + : undefined; + } + return myIssues; - }, [myIssues]); + }, [groupBy, myIssues]); const isEmpty = Object.values(groupedIssues ?? {}).every((group) => group.length === 0) || From 079a5b28d84816eec4bb8348a8018add71c14a59 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 11 Aug 2023 19:17:59 +0530 Subject: [PATCH 26/33] style: custom theming color picker position (#1846) --- .../components/core/theme/color-picker-input.tsx | 16 ++++++++++++++-- .../core/theme/custom-theme-selector.tsx | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/app/components/core/theme/color-picker-input.tsx b/apps/app/components/core/theme/color-picker-input.tsx index f848dd59e..73d71e46b 100644 --- a/apps/app/components/core/theme/color-picker-input.tsx +++ b/apps/app/components/core/theme/color-picker-input.tsx @@ -21,13 +21,21 @@ import { ICustomTheme } from "types"; type Props = { name: keyof ICustomTheme; + position?: "left" | "right"; watch: UseFormWatch; setValue: UseFormSetValue; error: FieldError | Merge> | undefined; register: UseFormRegister; }; -export const ColorPickerInput: React.FC = ({ name, watch, setValue, error, register }) => { +export const ColorPickerInput: React.FC = ({ + name, + position = "left", + watch, + setValue, + error, + register, +}) => { const handleColorChange = (newColor: ColorResult) => { const { hex } = newColor; setValue(name, hex); @@ -104,7 +112,11 @@ export const ColorPickerInput: React.FC = ({ name, watch, setValue, error leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - + diff --git a/apps/app/components/core/theme/custom-theme-selector.tsx b/apps/app/components/core/theme/custom-theme-selector.tsx index 3f6752e99..27817c82a 100644 --- a/apps/app/components/core/theme/custom-theme-selector.tsx +++ b/apps/app/components/core/theme/custom-theme-selector.tsx @@ -83,6 +83,7 @@ export const CustomThemeSelector: React.FC = observer(({ preLoadedData }) = observer(({ preLoadedData }) Date: Fri, 11 Aug 2023 19:27:44 +0530 Subject: [PATCH 27/33] feat: project public boards (#1772) * feat: project public boards * dev: public issue and comment reactions * dev: public issue comments * dev: public comments * dev: inbox for public boards * dev: inbox issues for public board * dev: public inbox issue * dev: migrations * dev: update api endpoints * dev: project boards and views * dev: state and label details * dev: public issue voting * dev: issue voting * dev: workspace details * dev: project icon and emoji --- apiserver/plane/api/serializers/__init__.py | 2 + apiserver/plane/api/serializers/issue.py | 10 + apiserver/plane/api/serializers/project.py | 26 +- apiserver/plane/api/urls.py | 134 +++ apiserver/plane/api/views/__init__.py | 9 +- apiserver/plane/api/views/inbox.py | 269 ++++- apiserver/plane/api/views/issue.py | 371 +++++++ apiserver/plane/api/views/project.py | 263 ++++- ..._alter_analyticview_created_by_and_more.py | 965 ++++++++++++++++++ apiserver/plane/db/models/__init__.py | 2 + apiserver/plane/db/models/issue.py | 39 +- apiserver/plane/db/models/project.py | 49 +- 12 files changed, 2120 insertions(+), 19 deletions(-) create mode 100644 apiserver/plane/db/migrations/0041_alter_analyticview_created_by_and_more.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 34e235e38..bd49c9a6f 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -18,6 +18,7 @@ from .project import ( ProjectFavoriteSerializer, ProjectLiteSerializer, ProjectMemberLiteSerializer, + ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, ) from .state import StateSerializer, StateLiteSerializer @@ -42,6 +43,7 @@ from .issue import ( IssueSubscriberSerializer, IssueReactionSerializer, CommentReactionSerializer, + IssueVoteSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index b2fcb0a85..64ee2b8f7 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -31,6 +31,7 @@ from plane.db.models import ( IssueAttachment, IssueReaction, CommentReaction, + IssueVote, ) @@ -554,6 +555,14 @@ class CommentReactionSerializer(BaseSerializer): +class IssueVoteSerializer(BaseSerializer): + + class Meta: + model = IssueVote + fields = ["issue", "vote", "workspace_id", "project_id", "actor"] + read_only_fields = fields + + class IssueCommentSerializer(BaseSerializer): actor_detail = UserLiteSerializer(read_only=True, source="actor") issue_detail = IssueFlatSerializer(read_only=True, source="issue") @@ -573,6 +582,7 @@ class IssueCommentSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", + "access", ] diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 9e30976ec..af60be89c 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -14,6 +14,7 @@ from plane.db.models import ( ProjectMemberInvite, ProjectIdentifier, ProjectFavorite, + ProjectDeployBoard, ) @@ -80,7 +81,14 @@ class ProjectSerializer(BaseSerializer): class ProjectLiteSerializer(BaseSerializer): class Meta: model = Project - fields = ["id", "identifier", "name"] + fields = [ + "id", + "identifier", + "name", + "cover_image", + "icon_prop", + "emoji", + ] read_only_fields = fields @@ -116,7 +124,6 @@ class ProjectMemberAdminSerializer(BaseSerializer): project = ProjectLiteSerializer(read_only=True) member = UserAdminLiteSerializer(read_only=True) - class Meta: model = ProjectMember fields = "__all__" @@ -149,8 +156,6 @@ class ProjectFavoriteSerializer(BaseSerializer): ] - - class ProjectMemberLiteSerializer(BaseSerializer): member = UserLiteSerializer(read_only=True) is_subscribed = serializers.BooleanField(read_only=True) @@ -159,3 +164,16 @@ class ProjectMemberLiteSerializer(BaseSerializer): model = ProjectMember fields = ["member", "id", "is_subscribed"] read_only_fields = fields + + +class ProjectDeployBoardSerializer(BaseSerializer): + project_details = ProjectLiteSerializer(read_only=True, source="project") + workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + + class Meta: + model = ProjectDeployBoard + fields = "__all__" + read_only_fields = [ + "workspace", + "project" "anchor", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index b1231f1a4..b8743476e 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -86,6 +86,7 @@ from plane.api.views import ( IssueAttachmentEndpoint, IssueArchiveViewSet, IssueSubscriberViewSet, + IssueCommentPublicViewSet, IssueReactionViewSet, CommentReactionViewSet, ExportIssuesEndpoint, @@ -165,6 +166,15 @@ from plane.api.views import ( NotificationViewSet, UnreadNotificationEndpoint, ## End Notification + # Public Boards + ProjectDeployBoardViewSet, + ProjectDeployBoardIssuesPublicEndpoint, + ProjectDeployBoardPublicSettingsEndpoint, + IssueReactionPublicViewSet, + CommentReactionPublicViewSet, + InboxIssuePublicViewSet, + IssueVotePublicViewSet, + ## End Public Boards ) @@ -1481,4 +1491,128 @@ urlpatterns = [ name="unread-notifications", ), ## End Notification + # Public Boards + path( + "workspaces//projects//project-deploy-boards/", + ProjectDeployBoardViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-deploy-board", + ), + path( + "workspaces//projects//project-deploy-boards//", + ProjectDeployBoardViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-deploy-board", + ), + path( + "public/workspaces//project-boards//settings/", + ProjectDeployBoardPublicSettingsEndpoint.as_view(), + name="project-deploy-board-settings", + ), + path( + "public/workspaces//project-boards//issues/", + ProjectDeployBoardIssuesPublicEndpoint.as_view(), + name="project-deploy-board", + ), + path( + "public/workspaces//project-boards//issues//comments/", + IssueCommentPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-comments-project-board", + ), + path( + "public/workspaces//project-boards//issues//comments//", + IssueCommentPublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="issue-comments-project-board", + ), + path( + "public/workspaces//project-boards//issues//reactions/", + IssueReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-reactions-project-board", + ), + path( + "public/workspaces//project-boards//issues//reactions//", + IssueReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="issue-reactions-project-board", + ), + path( + "public/workspaces//project-boards//comments//reactions/", + CommentReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="comment-reactions-project-board", + ), + path( + "public/workspaces//project-boards//comments//reactions//", + CommentReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="comment-reactions-project-board", + ), + path( + "public/workspaces//project-boards//inboxes//inbox-issues/", + InboxIssuePublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox-issue", + ), + path( + "public/workspaces//project-boards//inboxes//inbox-issues//", + InboxIssuePublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox-issue", + ), + path( + "public/workspaces//project-boards//issues//votes/", + IssueVotePublicViewSet.as_view( + { + "get": "list", + "post": "create", + "delete": "destroy", + } + ), + name="issue-vote-project-board", + ), + ## End Public Boards ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index a02e22fe9..39940bcb5 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -12,6 +12,9 @@ from .project import ( ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, + ProjectDeployBoardIssuesPublicEndpoint, + ProjectDeployBoardViewSet, + ProjectDeployBoardPublicSettingsEndpoint, ProjectMemberEndpoint, ) from .user import ( @@ -75,8 +78,12 @@ from .issue import ( IssueAttachmentEndpoint, IssueArchiveViewSet, IssueSubscriberViewSet, + IssueCommentPublicViewSet, CommentReactionViewSet, IssueReactionViewSet, + IssueReactionPublicViewSet, + CommentReactionPublicViewSet, + IssueVotePublicViewSet, ExportIssuesEndpoint ) @@ -145,7 +152,7 @@ from .estimate import ( from .release import ReleaseNotesEndpoint -from .inbox import InboxViewSet, InboxIssueViewSet +from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet from .analytic import ( AnalyticsEndpoint, diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index ada76c9b3..4fbea5f87 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -15,7 +15,6 @@ from sentry_sdk import capture_exception from .base import BaseViewSet from plane.api.permissions import ProjectBasePermission, ProjectLitePermission from plane.db.models import ( - Project, Inbox, InboxIssue, Issue, @@ -23,6 +22,7 @@ from plane.db.models import ( IssueLink, IssueAttachment, ProjectMember, + ProjectDeployBoard, ) from plane.api.serializers import ( IssueSerializer, @@ -377,4 +377,269 @@ class InboxIssueViewSet(BaseViewSet): return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, - ) \ No newline at end of file + ) + + +class InboxIssuePublicViewSet(BaseViewSet): + serializer_class = InboxIssueSerializer + model = InboxIssue + + filterset_fields = [ + "status", + ] + + def get_queryset(self): + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id")) + if project_deploy_board is not None: + return self.filter_queryset( + super() + .get_queryset() + .filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + inbox_id=self.kwargs.get("inbox_id"), + ) + .select_related("issue", "workspace", "project") + ) + else: + return InboxIssue.objects.none() + + def list(self, request, slug, project_id, inbox_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + + filters = issue_filters(request.query_params, "GET") + issues = ( + Issue.objects.filter( + issue_inbox__inbox_id=inbox_id, + workspace__slug=slug, + project_id=project_id, + ) + .filter(**filters) + .annotate(bridge_id=F("issue_inbox__id")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels") + .order_by("issue_inbox__snoozed_till", "issue_inbox__status") + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .prefetch_related( + Prefetch( + "issue_inbox", + queryset=InboxIssue.objects.only( + "status", "duplicate_to", "snoozed_till", "source" + ), + ) + ) + ) + issues_data = IssueStateInboxSerializer(issues, many=True).data + return Response( + issues_data, + status=status.HTTP_200_OK, + ) + except ProjectDeployBoard.DoesNotExist: + return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def create(self, request, slug, project_id, inbox_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + + if not request.data.get("issue", {}).get("name", False): + return Response( + {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST + ) + + # Check for valid priority + if not request.data.get("issue", {}).get("priority", None) in [ + "low", + "medium", + "high", + "urgent", + None, + ]: + return Response( + {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST + ) + + # Create or get state + state, _ = State.objects.get_or_create( + name="Triage", + group="backlog", + description="Default state for managing all Inbox Issues", + project_id=project_id, + color="#ff7700", + ) + + # create an issue + issue = Issue.objects.create( + name=request.data.get("issue", {}).get("name"), + description=request.data.get("issue", {}).get("description", {}), + description_html=request.data.get("issue", {}).get( + "description_html", "

" + ), + priority=request.data.get("issue", {}).get("priority", "low"), + project_id=project_id, + state=state, + ) + + # Create an Issue Activity + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=None, + ) + # create an inbox issue + InboxIssue.objects.create( + inbox_id=inbox_id, + project_id=project_id, + issue=issue, + source=request.data.get("source", "in-app"), + ) + + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def partial_update(self, request, slug, project_id, inbox_id, pk): + try: + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + # Get the project member + if str(inbox_issue.created_by_id) != str(request.user.id): + return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST) + + # Get issue data + issue_data = request.data.pop("issue", False) + + + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + # viewers and guests since only viewers and guests + issue_data = { + "name": issue_data.get("name", issue.name), + "description_html": issue_data.get("description_html", issue.description_html), + "description": issue_data.get("description", issue.description) + } + + issue_serializer = IssueCreateSerializer( + issue, data=issue_data, partial=True + ) + + if issue_serializer.is_valid(): + current_instance = issue + # Log all the updates + requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) + if issue is not None: + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(current_instance).data, + cls=DjangoJSONEncoder, + ), + ) + issue_serializer.save() + return Response(issue_serializer.data, status=status.HTTP_200_OK) + return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except InboxIssue.DoesNotExist: + return Response( + {"error": "Inbox Issue does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def retrieve(self, request, slug, project_id, inbox_id, pk): + try: + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def destroy(self, request, slug, project_id, inbox_id, pk): + try: + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + + if str(inbox_issue.created_by_id) != str(request.user.id): + return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST) + + inbox_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except InboxIssue.DoesNotExist: + return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 11c459cf0..5ca14234a 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -48,6 +48,7 @@ from plane.api.serializers import ( ProjectMemberLiteSerializer, IssueReactionSerializer, CommentReactionSerializer, + IssueVoteSerializer, ) from plane.api.permissions import ( WorkspaceEntityPermission, @@ -70,6 +71,8 @@ from plane.db.models import ( ProjectMember, IssueReaction, CommentReaction, + ProjectDeployBoard, + IssueVote, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -1457,6 +1460,374 @@ class CommentReactionViewSet(BaseViewSet): ) +class IssueCommentPublicViewSet(BaseViewSet): + serializer_class = IssueCommentSerializer + model = IssueComment + + filterset_fields = [ + "issue__id", + "workspace__id", + ] + + def get_queryset(self): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.comments: + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(issue_id=self.kwargs.get("issue_id")) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .distinct() + ) + else: + return IssueComment.objects.none() + + def create(self, request, slug, project_id, issue_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: + return Response( + {"error": "Comments are not enabled for this project"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + access = ( + "INTERNAL" + if ProjectMember.objects.filter( + project_id=project_id, member=request.user + ).exists() + else "EXTERNAL" + ) + + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + actor=request.user, + access=access, + ) + issue_activity.delay( + type="comment.activity.created", + requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def partial_update(self, request, slug, project_id, issue_id, pk): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: + return Response( + {"error": "Comments are not enabled for this project"}, + status=status.HTTP_400_BAD_REQUEST, + ) + comment = IssueComment.objects.get( + workspace__slug=slug, pk=pk, actor=request.user + ) + serializer = IssueCommentSerializer( + comment, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="comment.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + IssueCommentSerializer(comment).data, + cls=DjangoJSONEncoder, + ), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist): + return Response( + {"error": "IssueComent Does not exists"}, + status=status.HTTP_400_BAD_REQUEST,) + + def destroy(self, request, slug, project_id, issue_id, pk): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: + return Response( + {"error": "Comments are not enabled for this project"}, + status=status.HTTP_400_BAD_REQUEST, + ) + comment = IssueComment.objects.get( + workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user + ) + issue_activity.delay( + type="comment.activity.deleted", + requested_data=json.dumps({"comment_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + IssueCommentSerializer(comment).data, + cls=DjangoJSONEncoder, + ), + ) + comment.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist): + return Response( + {"error": "IssueComent Does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class IssueReactionPublicViewSet(BaseViewSet): + serializer_class = IssueReactionSerializer + model = IssueReaction + + def get_queryset(self): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .order_by("-created_at") + .distinct() + ) + else: + return IssueReaction.objects.none() + + def create(self, request, slug, project_id, issue_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this project board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = IssueReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, issue_id=issue_id, actor=request.user + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except ProjectDeployBoard.DoesNotExist: + return Response( + {"error": "Project board does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this project board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + issue_id=issue_id, + reaction=reaction_code, + actor=request.user, + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except IssueReaction.DoesNotExist: + return Response( + {"error": "Issue reaction does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class CommentReactionPublicViewSet(BaseViewSet): + serializer_class = CommentReactionSerializer + model = CommentReaction + + def get_queryset(self): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .order_by("-created_at") + .distinct() + ) + else: + return CommentReaction.objects.none() + + def create(self, request, slug, project_id, comment_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, comment_id=comment_id, actor=request.user + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except ProjectDeployBoard.DoesNotExist: + return Response( + {"error": "Project board does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + comment_reaction = CommentReaction.objects.get( + project_id=project_id, + workspace__slug=slug, + comment_id=comment_id, + reaction=reaction_code, + actor=request.user, + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except CommentReaction.DoesNotExist: + return Response( + {"error": "Comment reaction does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class IssueVotePublicViewSet(BaseViewSet): + model = IssueVote + serializer_class = IssueVoteSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + ) + + def create(self, request, slug, project_id, issue_id): + try: + issue_vote, _ = IssueVote.objects.get_or_create( + actor_id=request.user.id, + project_id=project_id, + issue_id=issue_id, + vote=request.data.get("vote", 1), + ) + serializer = IssueVoteSerializer(issue_vote) + return Response(serializer.data, status=status.HTTP_201_CREATED) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def destroy(self, request, slug, project_id, issue_id): + try: + issue_vote = IssueVote.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + actor_id=request.user.id, + ) + issue_vote.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + class ExportIssuesEndpoint(BaseAPIView): permission_classes = [ diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 123f2d29f..6d570865d 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -5,7 +5,21 @@ from datetime import datetime # Django imports from django.core.exceptions import ValidationError from django.db import IntegrityError -from django.db.models import Q, Exists, OuterRef, Func, F, Min, Subquery +from django.db.models import ( + Q, + Exists, + OuterRef, + Func, + F, + Max, + CharField, + Func, + Subquery, + Prefetch, + When, + Case, + Value, +) from django.core.validators import validate_email from django.conf import settings @@ -13,6 +27,7 @@ from django.conf import settings from rest_framework.response import Response from rest_framework import status from rest_framework import serializers +from rest_framework.permissions import AllowAny from sentry_sdk import capture_exception # Module imports @@ -23,10 +38,16 @@ from plane.api.serializers import ( ProjectDetailSerializer, ProjectMemberInviteSerializer, ProjectFavoriteSerializer, + IssueLiteSerializer, + ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, ) -from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.api.permissions import ( + ProjectBasePermission, + ProjectEntityPermission, + ProjectMemberPermission, +) from plane.db.models import ( Project, @@ -49,9 +70,17 @@ from plane.db.models import ( IssueAssignee, ModuleMember, Inbox, + ProjectDeployBoard, + Issue, + IssueReaction, + IssueLink, + IssueAttachment, + Label, ) from plane.bgtasks.project_invitation_task import project_invitation +from plane.utils.grouper import group_results +from plane.utils.issue_filters import issue_filters class ProjectViewSet(BaseViewSet): @@ -993,6 +1022,63 @@ class ProjectFavoritesViewSet(BaseViewSet): ) +class ProjectDeployBoardViewSet(BaseViewSet): + permission_classes = [ + ProjectMemberPermission, + ] + serializer_class = ProjectDeployBoardSerializer + model = ProjectDeployBoard + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + .select_related("project") + ) + + def create(self, request, slug, project_id): + try: + comments = request.data.get("comments", False) + reactions = request.data.get("reactions", False) + inbox = request.data.get("inbox", None) + votes = request.data.get("votes", False) + views = request.data.get( + "views", + { + "list": True, + "kanban": True, + "calendar": True, + "gantt": True, + "spreadsheet": True, + }, + ) + + project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( + anchor=f"{slug}/{project_id}", + project_id=project_id, + ) + project_deploy_board.comments = comments + project_deploy_board.reactions = reactions + project_deploy_board.inbox = inbox + project_deploy_board.votes = votes + project_deploy_board.views = views + + project_deploy_board.save() + + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + class ProjectMemberEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, @@ -1011,3 +1097,176 @@ class ProjectMemberEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) + except ProjectDeployBoard.DoesNotExist: + return Response( + {"error": "Project Deploy Board does not exists"}, + status=status.HTTP_404_NOT_FOUND, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", None] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssueLiteSerializer(issue_queryset, many=True).data + + states = State.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("name", "group", "color", "id") + + labels = Label.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("id", "name", "color", "parent") + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + issues = group_results(issues, group_by) + + return Response( + { + "issues": issues, + "states": states, + "labels": labels, + }, + status=status.HTTP_200_OK, + ) + except ProjectDeployBoard.DoesNotExist: + return Response( + {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/db/migrations/0041_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0041_alter_analyticview_created_by_and_more.py new file mode 100644 index 000000000..bf0a4341f --- /dev/null +++ b/apiserver/plane/db/migrations/0041_alter_analyticview_created_by_and_more.py @@ -0,0 +1,965 @@ +# Generated by Django 4.2.3 on 2023-08-04 11:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import plane.db.models.project +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0040_projectmember_preferences_user_cover_image_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='analyticview', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='analyticview', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='apitoken', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='apitoken', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='cycle', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='cycle', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='cycle', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='cycle', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='cyclefavorite', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='cyclefavorite', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='cyclefavorite', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='cyclefavorite', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='cycleissue', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='cycleissue', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='cycleissue', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='cycleissue', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='estimate', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='estimate', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='estimate', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='estimate', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='estimatepoint', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='estimatepoint', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='estimatepoint', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='estimatepoint', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='fileasset', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='fileasset', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='githubcommentsync', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='githubcommentsync', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='githubcommentsync', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='githubcommentsync', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='githubissuesync', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='githubissuesync', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='githubissuesync', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='githubissuesync', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='githubrepository', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='githubrepository', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='githubrepository', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='githubrepository', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='githubrepositorysync', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='githubrepositorysync', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='githubrepositorysync', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='githubrepositorysync', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='importer', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='importer', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='importer', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='importer', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='inbox', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='inbox', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='inbox', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='inbox', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='inboxissue', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='inboxissue', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='inboxissue', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='inboxissue', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='integration', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='integration', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issue', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issue', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issue', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issue', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issueactivity', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issueactivity', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issueactivity', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issueactivity', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issueassignee', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issueassignee', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issueassignee', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issueassignee', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issueattachment', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issueattachment', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issueattachment', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issueattachment', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issueblocker', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issueblocker', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issueblocker', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issueblocker', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issuecomment', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issuecomment', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issuecomment', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issuecomment', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issuelabel', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issuelabel', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issuelabel', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issuelabel', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issuelink', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issuelink', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issuelink', + name='title', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='issuelink', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issuelink', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issueproperty', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issueproperty', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issueproperty', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issueproperty', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issuesequence', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issuesequence', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issuesequence', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issuesequence', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issueview', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issueview', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issueview', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issueview', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='issueviewfavorite', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='issueviewfavorite', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='issueviewfavorite', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='issueviewfavorite', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='label', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='label', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='label', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='label', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='module', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='module', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='module', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='module', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='modulefavorite', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='modulefavorite', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='modulefavorite', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='modulefavorite', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='moduleissue', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='moduleissue', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='moduleissue', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='moduleissue', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='modulelink', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='modulelink', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='modulelink', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='modulelink', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='modulemember', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='modulemember', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='modulemember', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='modulemember', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='page', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='page', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='page', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='page', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='pageblock', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='pageblock', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='pageblock', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='pageblock', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='pagefavorite', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='pagefavorite', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='pagefavorite', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='pagefavorite', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='pagelabel', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='pagelabel', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='pagelabel', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='pagelabel', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='project', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='project', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='projectfavorite', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='projectfavorite', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='projectfavorite', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='projectfavorite', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='projectidentifier', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='projectidentifier', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='projectmember', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='projectmember', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='projectmember', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='projectmember', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='projectmemberinvite', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='projectmemberinvite', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='projectmemberinvite', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='projectmemberinvite', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='slackprojectsync', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='slackprojectsync', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='slackprojectsync', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='slackprojectsync', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='socialloginconnection', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='socialloginconnection', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='state', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='state', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.AlterField( + model_name='state', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='state', + name='workspace', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), + ), + migrations.AlterField( + model_name='team', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='team', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='teammember', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='teammember', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='workspace', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='workspace', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='workspaceintegration', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='workspaceintegration', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='workspacemember', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='workspacemember', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='workspacememberinvite', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='workspacememberinvite', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AlterField( + model_name='workspacetheme', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AlterField( + model_name='workspacetheme', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.CreateModel( + name='ProjectDeployBoard', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)), + ('comments', models.BooleanField(default=False)), + ('reactions', models.BooleanField(default=False)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('inbox', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bord_inbox', to='db.inbox')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Project Deploy Board', + 'verbose_name_plural': 'Project Deploy Boards', + 'db_table': 'project_deploy_boards', + 'ordering': ('-created_at',), + 'unique_together': {('project', 'anchor')}, + }, + ), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 959dea5f7..a1bd49ac5 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -18,6 +18,7 @@ from .project import ( ProjectMemberInvite, ProjectIdentifier, ProjectFavorite, + ProjectDeployBoard, ) from .issue import ( @@ -36,6 +37,7 @@ from .issue import ( IssueSubscriber, IssueReaction, CommentReaction, + IssueVote, ) from .asset import FileAsset diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 1b85af797..7af9e6e14 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -301,6 +301,14 @@ class IssueComment(ProjectBaseModel): related_name="comments", null=True, ) + access = models.CharField( + choices=( + ("INTERNAL", "INTERNAL"), + ("EXTERNAL", "EXTERNAL"), + ), + default="INTERNAL", + max_length=100, + ) def save(self, *args, **kwargs): self.comment_stripped = ( @@ -416,13 +424,14 @@ class IssueSubscriber(ProjectBaseModel): class IssueReaction(ProjectBaseModel): - actor = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="issue_reactions", ) - issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_reactions") + issue = models.ForeignKey( + Issue, on_delete=models.CASCADE, related_name="issue_reactions" + ) reaction = models.CharField(max_length=20) class Meta: @@ -437,13 +446,14 @@ class IssueReaction(ProjectBaseModel): class CommentReaction(ProjectBaseModel): - actor = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="comment_reactions", ) - comment = models.ForeignKey(IssueComment, on_delete=models.CASCADE, related_name="comment_reactions") + comment = models.ForeignKey( + IssueComment, on_delete=models.CASCADE, related_name="comment_reactions" + ) reaction = models.CharField(max_length=20) class Meta: @@ -457,6 +467,27 @@ class CommentReaction(ProjectBaseModel): return f"{self.issue.name} {self.actor.email}" +class IssueVote(ProjectBaseModel): + issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="votes") + actor = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="votes" + ) + vote = models.IntegerField( + choices=( + (-1, "DOWNVOTE"), + (1, "UPVOTE"), + ) + ) + class Meta: + unique_together = ["issue", "actor"] + verbose_name = "Issue Vote" + verbose_name_plural = "Issue Votes" + db_table = "issue_votes" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.actor.email}" + # TODO: Find a better method to save the model @receiver(post_save, sender=Issue) diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 2cbd70369..0c2b5cb96 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -1,3 +1,6 @@ +# Python imports +from uuid import uuid4 + # Django imports from django.db import models from django.conf import settings @@ -31,12 +34,9 @@ def get_default_props(): "showEmptyGroups": True, } + def get_default_preferences(): - return { - "pages": { - "block_display": True - } - } + return {"pages": {"block_display": True}} class Project(BaseModel): @@ -157,7 +157,6 @@ class ProjectMember(ProjectBaseModel): preferences = models.JSONField(default=get_default_preferences) sort_order = models.FloatField(default=65535) - def save(self, *args, **kwargs): if self._state.adding: smallest_sort_order = ProjectMember.objects.filter( @@ -217,3 +216,41 @@ class ProjectFavorite(ProjectBaseModel): def __str__(self): """Return user of the project""" return f"{self.user.email} <{self.project.name}>" + + +def get_anchor(): + return uuid4().hex + + +def get_default_views(): + return { + "list": True, + "kanban": True, + "calendar": True, + "gantt": True, + "spreadsheet": True, + } + + +class ProjectDeployBoard(ProjectBaseModel): + anchor = models.CharField( + max_length=255, default=get_anchor, unique=True, db_index=True + ) + comments = models.BooleanField(default=False) + reactions = models.BooleanField(default=False) + inbox = models.ForeignKey( + "db.Inbox", related_name="bord_inbox", on_delete=models.SET_NULL, null=True + ) + votes = models.BooleanField(default=False) + views = models.JSONField(default=get_default_views) + + class Meta: + unique_together = ["project", "anchor"] + verbose_name = "Project Deploy Board" + verbose_name_plural = "Project Deploy Boards" + db_table = "project_deploy_boards" + ordering = ("-created_at",) + + def __str__(self): + """Return project and anchor""" + return f"{self.anchor} <{self.project.name}>" From 1a9faa025a6cf629f549e4eea19e1ff3f43390b8 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:39:52 -0400 Subject: [PATCH 28/33] fix: export issues in CSV, JSON and XLSX (#1794) * fix: file name change * feat: added xml json and csv export * chore: added openpyxl package * fix: added initiated_by field * fix: added initiated by details * dev: refactoring * fix: rendering assignee name and labels in sheet * fix: handeled exception in label * feat: implemented link expiration scheduler(8 days) * fix: removed the expired field --------- Co-authored-by: NarayanBavisetti Co-authored-by: pablohashescobar --- apiserver/plane/api/serializers/__init__.py | 2 + apiserver/plane/api/serializers/exporter.py | 26 ++ apiserver/plane/api/views/__init__.py | 7 +- apiserver/plane/api/views/exporter.py | 99 +++++ apiserver/plane/api/views/issue.py | 1 - apiserver/plane/bgtasks/export_task.py | 357 ++++++++++++++++++ .../plane/bgtasks/exporter_expired_task.py | 38 ++ .../plane/bgtasks/project_issue_export.py | 191 ---------- apiserver/plane/celery.py | 4 + apiserver/plane/db/models/__init__.py | 4 +- apiserver/plane/db/models/exporter.py | 56 +++ apiserver/plane/settings/common.py | 2 +- apiserver/requirements/base.txt | 3 +- 13 files changed, 593 insertions(+), 197 deletions(-) create mode 100644 apiserver/plane/api/serializers/exporter.py create mode 100644 apiserver/plane/api/views/exporter.py create mode 100644 apiserver/plane/bgtasks/export_task.py create mode 100644 apiserver/plane/bgtasks/exporter_expired_task.py delete mode 100644 apiserver/plane/bgtasks/project_issue_export.py create mode 100644 apiserver/plane/db/models/exporter.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index bd49c9a6f..5855f0413 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -81,3 +81,5 @@ from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSeriali from .analytic import AnalyticViewSerializer from .notification import NotificationSerializer + +from .exporter import ExporterHistorySerializer diff --git a/apiserver/plane/api/serializers/exporter.py b/apiserver/plane/api/serializers/exporter.py new file mode 100644 index 000000000..5c78cfa69 --- /dev/null +++ b/apiserver/plane/api/serializers/exporter.py @@ -0,0 +1,26 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import ExporterHistory +from .user import UserLiteSerializer + + +class ExporterHistorySerializer(BaseSerializer): + initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True) + + class Meta: + model = ExporterHistory + fields = [ + "id", + "created_at", + "updated_at", + "project", + "provider", + "status", + "url", + "initiated_by", + "initiated_by_detail", + "token", + "created_by", + "updated_by", + ] + read_only_fields = fields diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 39940bcb5..11223f90a 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -84,7 +84,6 @@ from .issue import ( IssueReactionPublicViewSet, CommentReactionPublicViewSet, IssueVotePublicViewSet, - ExportIssuesEndpoint ) from .auth_extended import ( @@ -162,4 +161,8 @@ from .analytic import ( DefaultAnalyticsEndpoint, ) -from .notification import NotificationViewSet, UnreadNotificationEndpoint \ No newline at end of file +from .notification import NotificationViewSet, UnreadNotificationEndpoint + +from .exporter import ( + ExportIssuesEndpoint, +) \ No newline at end of file diff --git a/apiserver/plane/api/views/exporter.py b/apiserver/plane/api/views/exporter.py new file mode 100644 index 000000000..f158f783d --- /dev/null +++ b/apiserver/plane/api/views/exporter.py @@ -0,0 +1,99 @@ +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from sentry_sdk import capture_exception + +# Module imports +from . import BaseAPIView +from plane.api.permissions import WorkSpaceAdminPermission +from plane.bgtasks.export_task import issue_export_task +from plane.db.models import Project, ExporterHistory, Workspace + +from plane.api.serializers import ExporterHistorySerializer + + +class ExportIssuesEndpoint(BaseAPIView): + permission_classes = [ + WorkSpaceAdminPermission, + ] + model = ExporterHistory + serializer_class = ExporterHistorySerializer + + def post(self, request, slug): + try: + # Get the workspace + workspace = Workspace.objects.get(slug=slug) + + provider = request.data.get("provider", False) + multiple = request.data.get("multiple", False) + project_ids = request.data.get("project", []) + + if provider in ["csv", "xlsx", "json"]: + if not project_ids: + project_ids = Project.objects.filter( + workspace__slug=slug + ).values_list("id", flat=True) + project_ids = [str(project_id) for project_id in project_ids] + + exporter = ExporterHistory.objects.create( + workspace=workspace, + project=project_ids, + initiated_by=request.user, + provider=provider, + ) + + issue_export_task.delay( + provider=exporter.provider, + workspace_id=workspace.id, + project_ids=project_ids, + token_id=exporter.token, + multiple=multiple, + ) + return Response( + { + "message": f"Once the export is ready you will be able to download it" + }, + status=status.HTTP_200_OK, + ) + else: + return Response( + {"error": f"Provider '{provider}' not found."}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Workspace.DoesNotExist: + return Response( + {"error": "Workspace does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug): + try: + exporter_history = ExporterHistory.objects.filter( + workspace__slug=slug + ).select_related("workspace","initiated_by") + + if request.GET.get("per_page", False) and request.GET.get("cursor", False): + return self.paginate( + request=request, + queryset=exporter_history, + on_results=lambda exporter_history: ExporterHistorySerializer( + exporter_history, many=True + ).data, + ) + else: + return Response( + {"error": "per_page and cursor are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 5ca14234a..77432e1e0 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -77,7 +77,6 @@ from plane.db.models import ( from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters -from plane.bgtasks.project_issue_export import issue_export_task class IssueViewSet(BaseViewSet): diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py new file mode 100644 index 000000000..de15bcdb8 --- /dev/null +++ b/apiserver/plane/bgtasks/export_task.py @@ -0,0 +1,357 @@ +# Python imports +import csv +import io +import json +import boto3 +import zipfile +from datetime import datetime, date, timedelta + +# Django imports +from django.conf import settings +from django.utils import timezone + +# Third party imports +from celery import shared_task +from sentry_sdk import capture_exception +from botocore.client import Config +from openpyxl import Workbook +from openpyxl.styles import NamedStyle +from openpyxl.utils.datetime import to_excel + +# Module imports +from plane.db.models import Issue, ExporterHistory, Project + + +class DateTimeEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, (datetime, date)): + return obj.isoformat() + return super().default(obj) + + +def create_csv_file(data): + csv_buffer = io.StringIO() + csv_writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) + + for row in data: + csv_writer.writerow(row) + + csv_buffer.seek(0) + return csv_buffer.getvalue() + + +def create_json_file(data): + return json.dumps(data, cls=DateTimeEncoder) + + +def create_xlsx_file(data): + workbook = Workbook() + sheet = workbook.active + + no_timezone_style = NamedStyle(name="no_timezone_style") + no_timezone_style.number_format = "yyyy-mm-dd hh:mm:ss" + + for row in data: + sheet.append(row) + + for column_cells in sheet.columns: + for cell in column_cells: + if isinstance(cell.value, datetime): + cell.style = no_timezone_style + cell.value = to_excel(cell.value.replace(tzinfo=None)) + + xlsx_buffer = io.BytesIO() + workbook.save(xlsx_buffer) + xlsx_buffer.seek(0) + return xlsx_buffer.getvalue() + + +def create_zip_file(files): + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf: + for filename, file_content in files: + zipf.writestr(filename, file_content) + + zip_buffer.seek(0) + return zip_buffer + + +def upload_to_s3(zip_file, workspace_id, token_id): + s3 = boto3.client( + "s3", + region_name="ap-south-1", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + file_name = f"{workspace_id}/issues-{datetime.now().date()}.zip" + + s3.upload_fileobj( + zip_file, + settings.AWS_S3_BUCKET_NAME, + file_name, + ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, + ) + + expires_in = 7 * 24 * 60 * 60 + presigned_url = s3.generate_presigned_url( + "get_object", + Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name}, + ExpiresIn=expires_in, + ) + + exporter_instance = ExporterHistory.objects.get(token=token_id) + + if presigned_url: + exporter_instance.url = presigned_url + exporter_instance.status = "completed" + exporter_instance.key = file_name + else: + exporter_instance.status = "failed" + + exporter_instance.save(update_fields=["status", "url","key"]) + + +def generate_table_row(issue): + return [ + f"""{issue["project__identifier"]}-{issue["sequence_id"]}""", + issue["project__name"], + issue["name"], + issue["description_stripped"], + issue["state__name"], + issue["priority"], + f"{issue['created_by__first_name']} {issue['created_by__last_name']}" + if issue["created_by__first_name"] and issue["created_by__last_name"] + else "", + f"{issue['assignees__first_name']} {issue['assignees__last_name']}" + if issue["assignees__first_name"] and issue["assignees__last_name"] + else "", + issue["labels__name"], + issue["issue_cycle__cycle__name"], + issue["issue_cycle__cycle__start_date"], + issue["issue_cycle__cycle__end_date"], + issue["issue_module__module__name"], + issue["issue_module__module__start_date"], + issue["issue_module__module__target_date"], + issue["created_at"], + issue["updated_at"], + issue["completed_at"], + issue["archived_at"], + ] + + +def generate_json_row(issue): + return { + "ID": f"""{issue["project__identifier"]}-{issue["sequence_id"]}""", + "Project": issue["project__name"], + "Name": issue["name"], + "Description": issue["description_stripped"], + "State": issue["state__name"], + "Priority": issue["priority"], + "Created By": f"{issue['created_by__first_name']} {issue['created_by__last_name']}" + if issue["created_by__first_name"] and issue["created_by__last_name"] + else "", + "Assignee": f"{issue['assignees__first_name']} {issue['assignees__last_name']}" + if issue["assignees__first_name"] and issue["assignees__last_name"] + else "", + "Labels": issue["labels__name"], + "Cycle Name": issue["issue_cycle__cycle__name"], + "Cycle Start Date": issue["issue_cycle__cycle__start_date"], + "Cycle End Date": issue["issue_cycle__cycle__end_date"], + "Module Name": issue["issue_module__module__name"], + "Module Start Date": issue["issue_module__module__start_date"], + "Module Target Date": issue["issue_module__module__target_date"], + "Created At": issue["created_at"], + "Updated At": issue["updated_at"], + "Completed At": issue["completed_at"], + "Archived At": issue["archived_at"], + } + + +def update_json_row(rows, row): + matched_index = next( + ( + index + for index, existing_row in enumerate(rows) + if existing_row["ID"] == row["ID"] + ), + None, + ) + + if matched_index is not None: + existing_assignees, existing_labels = ( + rows[matched_index]["Assignee"], + rows[matched_index]["Labels"], + ) + assignee, label = row["Assignee"], row["Labels"] + + if assignee is not None and assignee not in existing_assignees: + rows[matched_index]["Assignee"] += f", {assignee}" + if label is not None and label not in existing_labels: + rows[matched_index]["Labels"] += f", {label}" + else: + rows.append(row) + + +def update_table_row(rows, row): + matched_index = next( + (index for index, existing_row in enumerate(rows) if existing_row[0] == row[0]), + None, + ) + + if matched_index is not None: + existing_assignees, existing_labels = rows[matched_index][7:9] + assignee, label = row[7:9] + + if assignee is not None and assignee not in existing_assignees: + rows[matched_index][7] += f", {assignee}" + if label is not None and label not in existing_labels: + rows[matched_index][8] += f", {label}" + else: + rows.append(row) + + +def generate_csv(header, project_id, issues, files): + """ + Generate CSV export for all the passed issues. + """ + rows = [ + header, + ] + for issue in issues: + row = generate_table_row(issue) + update_table_row(rows, row) + csv_file = create_csv_file(rows) + files.append((f"{project_id}.csv", csv_file)) + + +def generate_json(header, project_id, issues, files): + rows = [] + for issue in issues: + row = generate_json_row(issue) + update_json_row(rows, row) + json_file = create_json_file(rows) + files.append((f"{project_id}.json", json_file)) + + +def generate_xlsx(header, project_id, issues, files): + rows = [header] + for issue in issues: + row = generate_table_row(issue) + update_table_row(rows, row) + xlsx_file = create_xlsx_file(rows) + files.append((f"{project_id}.xlsx", xlsx_file)) + + +@shared_task +def issue_export_task(provider, workspace_id, project_ids, token_id, multiple): + try: + exporter_instance = ExporterHistory.objects.get(token=token_id) + exporter_instance.status = "processing" + exporter_instance.save(update_fields=["status"]) + + workspace_issues = ( + ( + Issue.objects.filter( + workspace__id=workspace_id, project_id__in=project_ids + ) + .select_related("project", "workspace", "state", "parent", "created_by") + .prefetch_related( + "assignees", "labels", "issue_cycle__cycle", "issue_module__module" + ) + .values( + "id", + "project__identifier", + "project__name", + "project__id", + "sequence_id", + "name", + "description_stripped", + "priority", + "state__name", + "created_at", + "updated_at", + "completed_at", + "archived_at", + "issue_cycle__cycle__name", + "issue_cycle__cycle__start_date", + "issue_cycle__cycle__end_date", + "issue_module__module__name", + "issue_module__module__start_date", + "issue_module__module__target_date", + "created_by__first_name", + "created_by__last_name", + "assignees__first_name", + "assignees__last_name", + "labels__name", + ) + ) + .order_by("project__identifier","sequence_id") + .distinct() + ) + # CSV header + header = [ + "ID", + "Project", + "Name", + "Description", + "State", + "Priority", + "Created By", + "Assignee", + "Labels", + "Cycle Name", + "Cycle Start Date", + "Cycle End Date", + "Module Name", + "Module Start Date", + "Module Target Date", + "Created At", + "Updated At", + "Completed At", + "Archived At", + ] + + EXPORTER_MAPPER = { + "csv": generate_csv, + "json": generate_json, + "xlsx": generate_xlsx, + } + + files = [] + if multiple: + for project_id in project_ids: + issues = workspace_issues.filter(project__id=project_id) + exporter = EXPORTER_MAPPER.get(provider) + if exporter is not None: + exporter( + header, + project_id, + issues, + files, + ) + + else: + exporter = EXPORTER_MAPPER.get(provider) + if exporter is not None: + exporter( + header, + workspace_id, + workspace_issues, + files, + ) + + zip_buffer = create_zip_file(files) + upload_to_s3(zip_buffer, workspace_id, token_id) + + except Exception as e: + exporter_instance = ExporterHistory.objects.get(token=token_id) + exporter_instance.status = "failed" + exporter_instance.reason = str(e) + exporter_instance.save(update_fields=["status", "reason"]) + + # Print logs if in DEBUG mode + if settings.DEBUG: + print(e) + capture_exception(e) + return diff --git a/apiserver/plane/bgtasks/exporter_expired_task.py b/apiserver/plane/bgtasks/exporter_expired_task.py new file mode 100644 index 000000000..799904347 --- /dev/null +++ b/apiserver/plane/bgtasks/exporter_expired_task.py @@ -0,0 +1,38 @@ +# Python imports +import boto3 +from datetime import timedelta + +# Django imports +from django.conf import settings +from django.utils import timezone +from django.db.models import Q + +# Third party imports +from celery import shared_task +from botocore.client import Config + +# Module imports +from plane.db.models import ExporterHistory + + +@shared_task +def delete_old_s3_link(): + # Get a list of keys and IDs to process + expired_exporter_history = ExporterHistory.objects.filter( + Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8)) + ).values_list("key", "id") + + s3 = boto3.client( + "s3", + region_name="ap-south-1", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + + for file_name, exporter_id in expired_exporter_history: + # Delete object from S3 + if file_name: + s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name) + + ExporterHistory.objects.filter(id=exporter_id).update(url=None) diff --git a/apiserver/plane/bgtasks/project_issue_export.py b/apiserver/plane/bgtasks/project_issue_export.py deleted file mode 100644 index 75088be9d..000000000 --- a/apiserver/plane/bgtasks/project_issue_export.py +++ /dev/null @@ -1,191 +0,0 @@ -# Python imports -import csv -import io - -# Django imports -from django.core.mail import EmailMultiAlternatives -from django.template.loader import render_to_string -from django.utils.html import strip_tags -from django.conf import settings -from django.utils import timezone - -# Third party imports -from celery import shared_task -from sentry_sdk import capture_exception - -# Module imports -from plane.db.models import Issue - -@shared_task -def issue_export_task(email, data, slug, exporter_name): - try: - - project_ids = data.get("project_id", []) - issues_filter = {"workspace__slug": slug} - - if project_ids: - issues_filter["project_id__in"] = project_ids - - issues = ( - Issue.objects.filter(**issues_filter) - .select_related("project", "workspace", "state", "parent", "created_by") - .prefetch_related( - "assignees", "labels", "issue_cycle__cycle", "issue_module__module" - ) - .values_list( - "project__identifier", - "sequence_id", - "name", - "description_stripped", - "priority", - "start_date", - "target_date", - "state__name", - "project__name", - "created_at", - "updated_at", - "completed_at", - "archived_at", - "issue_cycle__cycle__name", - "issue_cycle__cycle__start_date", - "issue_cycle__cycle__end_date", - "issue_module__module__name", - "issue_module__module__start_date", - "issue_module__module__target_date", - "created_by__first_name", - "created_by__last_name", - "assignees__first_name", - "assignees__last_name", - "labels__name", - ) - ) - - # CSV header - header = [ - "Issue ID", - "Project", - "Name", - "Description", - "State", - "Priority", - "Created By", - "Assignee", - "Labels", - "Cycle Name", - "Cycle Start Date", - "Cycle End Date", - "Module Name", - "Module Start Date", - "Module Target Date", - "Created At" - "Updated At" - "Completed At" - "Archived At" - ] - - # Prepare the CSV data - rows = [header] - - # Write data for each issue - for issue in issues: - ( - project_identifier, - sequence_id, - name, - description, - priority, - start_date, - target_date, - state_name, - project_name, - created_at, - updated_at, - completed_at, - archived_at, - cycle_name, - cycle_start_date, - cycle_end_date, - module_name, - module_start_date, - module_target_date, - created_by_first_name, - created_by_last_name, - assignees_first_names, - assignees_last_names, - labels_names, - ) = issue - - created_by_fullname = ( - f"{created_by_first_name} {created_by_last_name}" - if created_by_first_name and created_by_last_name - else "" - ) - - assignees_names = "" - if assignees_first_names and assignees_last_names: - assignees_names = ", ".join( - [ - f"{assignees_first_name} {assignees_last_name}" - for assignees_first_name, assignees_last_name in zip( - assignees_first_names, assignees_last_names - ) - ] - ) - - labels_names = ", ".join(labels_names) if labels_names else "" - - row = [ - f"{project_identifier}-{sequence_id}", - project_name, - name, - description, - state_name, - priority, - created_by_fullname, - assignees_names, - labels_names, - cycle_name, - cycle_start_date, - cycle_end_date, - module_name, - module_start_date, - module_target_date, - start_date, - target_date, - created_at, - updated_at, - completed_at, - archived_at, - ] - rows.append(row) - - # Create CSV file in-memory - csv_buffer = io.StringIO() - writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) - - # Write CSV data to the buffer - for row in rows: - writer.writerow(row) - - subject = "Your Issue Export is ready" - - context = { - "username": exporter_name, - } - - html_content = render_to_string("emails/exports/issues.html", context) - text_content = strip_tags(html_content) - - csv_buffer.seek(0) - msg = EmailMultiAlternatives( - subject, text_content, settings.EMAIL_FROM, [email] - ) - msg.attach(f"{slug}-issues-{timezone.now().date()}.csv", csv_buffer.read(), "text/csv") - msg.send(fail_silently=False) - - except Exception as e: - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) - capture_exception(e) - return diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py index ed0dc419e..15fe8af52 100644 --- a/apiserver/plane/celery.py +++ b/apiserver/plane/celery.py @@ -20,6 +20,10 @@ app.conf.beat_schedule = { "task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues", "schedule": crontab(hour=0, minute=0), }, + "check-every-day-to-delete_exporter_history": { + "task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link", + "schedule": crontab(hour=0, minute=0), + }, } # Load task modules from all registered Django app configs. diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index a1bd49ac5..659eea3eb 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -74,4 +74,6 @@ from .inbox import Inbox, InboxIssue from .analytic import AnalyticView -from .notification import Notification \ No newline at end of file +from .notification import Notification + +from .exporter import ExporterHistory \ No newline at end of file diff --git a/apiserver/plane/db/models/exporter.py b/apiserver/plane/db/models/exporter.py new file mode 100644 index 000000000..fce31c8e7 --- /dev/null +++ b/apiserver/plane/db/models/exporter.py @@ -0,0 +1,56 @@ +import uuid + +# Python imports +from uuid import uuid4 + +# Django imports +from django.db import models +from django.conf import settings +from django.contrib.postgres.fields import ArrayField + +# Module imports +from . import BaseModel + +def generate_token(): + return uuid4().hex + +class ExporterHistory(BaseModel): + workspace = models.ForeignKey( + "db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_exporters" + ) + project = ArrayField(models.UUIDField(default=uuid.uuid4), blank=True, null=True) + provider = models.CharField( + max_length=50, + choices=( + ("json", "json"), + ("csv", "csv"), + ("xlsx", "xlsx"), + ), + ) + status = models.CharField( + max_length=50, + choices=( + ("queued", "Queued"), + ("processing", "Processing"), + ("completed", "Completed"), + ("failed", "Failed"), + ), + default="queued", + ) + reason = models.TextField(blank=True) + key = models.TextField(blank=True) + url = models.URLField(max_length=800, blank=True, null=True) + token = models.CharField(max_length=255, default=generate_token, unique=True) + initiated_by = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="workspace_exporters" + ) + + class Meta: + verbose_name = "Exporter" + verbose_name_plural = "Exporters" + db_table = "exporters" + ordering = ("-created_at",) + + def __str__(self): + """Return name of the service""" + return f"{self.provider} <{self.workspace.name}>" \ No newline at end of file diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index e3a918c18..59e0bd31b 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -214,4 +214,4 @@ SIMPLE_JWT = { CELERY_TIMEZONE = TIME_ZONE CELERY_TASK_SERIALIZER = 'json' CELERY_ACCEPT_CONTENT = ['application/json'] -CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task",) +CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task","plane.bgtasks.exporter_expired_task") diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 76c3dace9..ca9d881ef 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -32,4 +32,5 @@ celery==5.3.1 django_celery_beat==2.5.0 psycopg-binary==3.1.9 psycopg-c==3.1.9 -scout-apm==2.26.1 \ No newline at end of file +scout-apm==2.26.1 +openpyxl==3.1.2 \ No newline at end of file From 70e2509d527795ed9d726b39037ad05b9769a7e3 Mon Sep 17 00:00:00 2001 From: ryota-murakami Date: Sat, 12 Aug 2023 20:30:01 +0900 Subject: [PATCH 29/33] package.json; fix license --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87ff12855..804fb7b64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "repository": "https://github.com/makeplane/plane.git", - "license": "MIT", + "license": "AGPL-3.0", "private": true, "workspaces": [ "apps/*", From ddd3301d172fc88162e865f0dc326571baadc1f1 Mon Sep 17 00:00:00 2001 From: srinivas pendem <65014795+srinivaspendem@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:44:17 +0530 Subject: [PATCH 30/33] feat: csv, json and, xlsx exporter (#1840) * feat : csv, jason and, xlxs exporter * handeling the export fail * adding expired state to exports * typo update * header change * improvement: added validation for the expired date --------- Co-authored-by: srinivaspendem --- .../components/command-palette/command-k.tsx | 13 +- apps/app/components/exporter/export-modal.tsx | 185 ++++++++++++++++++ apps/app/components/exporter/guide.tsx | 171 ++++++++++++++++ apps/app/components/exporter/index.tsx | 4 + .../app/components/exporter/single-export.tsx | 81 ++++++++ .../components/integration/github/root.tsx | 4 +- apps/app/components/integration/guide.tsx | 4 +- apps/app/components/integration/jira/root.tsx | 4 +- .../integration-and-import-export-banner.tsx | 17 +- apps/app/constants/fetch-keys.ts | 4 + apps/app/constants/workspace.ts | 27 +++ apps/app/layouts/settings-navbar.tsx | 10 +- .../{import-export.tsx => exports.tsx} | 8 +- .../[workspaceSlug]/settings/imports.tsx | 58 ++++++ apps/app/public/services/csv.png | Bin 0 -> 5155 bytes apps/app/public/services/excel.png | Bin 0 -> 60019 bytes apps/app/public/services/json.png | Bin 0 -> 56722 bytes apps/app/services/integration/csv.services.ts | 42 ++++ apps/app/services/integration/index.ts | 17 ++ apps/app/services/track-event.service.ts | 23 +++ apps/app/types/importer/index.ts | 24 +++ apps/app/types/index.d.ts | 1 + 22 files changed, 673 insertions(+), 24 deletions(-) create mode 100644 apps/app/components/exporter/export-modal.tsx create mode 100644 apps/app/components/exporter/guide.tsx create mode 100644 apps/app/components/exporter/index.tsx create mode 100644 apps/app/components/exporter/single-export.tsx rename apps/app/pages/[workspaceSlug]/settings/{import-export.tsx => exports.tsx} (85%) create mode 100644 apps/app/pages/[workspaceSlug]/settings/imports.tsx create mode 100644 apps/app/public/services/csv.png create mode 100644 apps/app/public/services/excel.png create mode 100644 apps/app/public/services/json.png create mode 100644 apps/app/services/integration/csv.services.ts diff --git a/apps/app/components/command-palette/command-k.tsx b/apps/app/components/command-palette/command-k.tsx index 6f3a0e5ef..a1525a348 100644 --- a/apps/app/components/command-palette/command-k.tsx +++ b/apps/app/components/command-palette/command-k.tsx @@ -739,12 +739,21 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal
redirect(`/${workspaceSlug}/settings/import-export`)} + onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none" >
- Import/Export + Import +
+
+ redirect(`/${workspaceSlug}/settings/exports`)} + className="focus:outline-none" + > +
+ + Export
diff --git a/apps/app/components/exporter/export-modal.tsx b/apps/app/components/exporter/export-modal.tsx new file mode 100644 index 000000000..df4729265 --- /dev/null +++ b/apps/app/components/exporter/export-modal.tsx @@ -0,0 +1,185 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// services +import CSVIntegrationService from "services/integration/csv.services"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { SecondaryButton, PrimaryButton, CustomSearchSelect } from "components/ui"; +// types +import { ICurrentUserResponse, IImporterService } from "types"; +// fetch-keys +import useProjects from "hooks/use-projects"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + data: IImporterService | null; + user: ICurrentUserResponse | undefined; + provider: string | string[]; + mutateServices: () => void; +}; + +export const Exporter: React.FC = ({ + isOpen, + handleClose, + user, + provider, + mutateServices, +}) => { + const [exportLoading, setExportLoading] = useState(false); + const router = useRouter(); + const { workspaceSlug } = router.query; + const { projects } = useProjects(); + const { setToastAlert } = useToast(); + + const options = projects?.map((project) => ({ + value: project.id, + query: project.name + project.identifier, + content: ( +
+ {project.identifier} + {project.name} +
+ ), + })); + + const [value, setValue] = React.useState([]); + const [multiple, setMultiple] = React.useState(false); + const onChange = (val: any) => { + setValue(val); + }; + const ExportCSVToMail = async () => { + setExportLoading(true); + if (workspaceSlug && user && typeof provider === "string") { + const payload = { + provider: provider, + project: value, + multiple: multiple, + }; + await CSVIntegrationService.exportCSVService(workspaceSlug as string, payload, user) + .then(() => { + mutateServices(); + router.push(`/${workspaceSlug}/settings/exports`); + setExportLoading(false); + setToastAlert({ + type: "success", + title: "Export Successful", + message: `You will be able to download the exported ${ + provider === "csv" + ? "CSV" + : provider === "xlsx" + ? "Excel" + : provider === "json" + ? "JSON" + : "" + } from the previous export.`, + }); + }) + .catch(() => { + setExportLoading(false); + setToastAlert({ + type: "error", + title: "Error!", + message: "Export was unsuccessful. Please try again.", + }); + }); + } + }; + + return ( + + + +
+ + +
+
+ + +
+
+ +

+ Export to{" "} + {provider === "csv" + ? "CSV" + : provider === "xlsx" + ? "Excel" + : provider === "json" + ? "JSON" + : ""} +

+
+
+
+ onChange(val)} + options={options} + input={true} + label={ + value && value.length > 0 + ? projects && + projects + .filter((p) => value.includes(p.id)) + .map((p) => p.identifier) + .join(", ") + : "All projects" + } + optionsClassName="min-w-full" + multiple + /> +
+
setMultiple(!multiple)} + className="flex items-center gap-2 max-w-min cursor-pointer" + > + setMultiple(!multiple)} + /> +
+ Export the data into separate files +
+
+
+ Cancel + + {exportLoading ? "Exporting..." : "Export"} + +
+
+
+
+
+
+
+
+ ); +}; diff --git a/apps/app/components/exporter/guide.tsx b/apps/app/components/exporter/guide.tsx new file mode 100644 index 000000000..82a4fd453 --- /dev/null +++ b/apps/app/components/exporter/guide.tsx @@ -0,0 +1,171 @@ +import { useState } from "react"; + +import Link from "next/link"; +import Image from "next/image"; +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// hooks +import useUserAuth from "hooks/use-user-auth"; +// services +import IntegrationService from "services/integration"; +// components +import { Exporter, SingleExport } from "components/exporter"; +// ui +import { Icon, Loader, PrimaryButton } from "components/ui"; +// icons +import { ArrowPathIcon } from "@heroicons/react/24/outline"; +// fetch-keys +import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; +// constants +import { EXPORTERS_LIST } from "constants/workspace"; + +const IntegrationGuide = () => { + const [refreshing, setRefreshing] = useState(false); + const per_page = 10; + const [cursor, setCursor] = useState(`10:0:0`); + + const router = useRouter(); + const { workspaceSlug, provider } = router.query; + + const { user } = useUserAuth(); + + const { data: exporterServices } = useSWR( + workspaceSlug && cursor + ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) + : null, + workspaceSlug && cursor + ? () => IntegrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page) + : null + ); + + const handleCsvClose = () => { + router.replace(`/plane/settings/exports`); + }; + + return ( + <> +
+ <> +
+ {EXPORTERS_LIST.map((service) => ( +
+
+
+ {`${service.title} +
+
+

{service.title}

+

{service.description}

+
+ +
+
+ ))} +
+
+

+
+
Previous Exports
+ +
+
+ + +
+

+ {exporterServices && exporterServices?.results ? ( + exporterServices?.results?.length > 0 ? ( +
+
+ {exporterServices?.results.map((service) => ( + + ))} +
+
+ ) : ( +

No previous export available.

+ ) + ) : ( + + + + + + + )} +
+ + {provider && ( + handleCsvClose()} + data={null} + user={user} + provider={provider} + mutateServices={() => + mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)) + } + /> + )} +
+ + ); +}; + +export default IntegrationGuide; diff --git a/apps/app/components/exporter/index.tsx b/apps/app/components/exporter/index.tsx new file mode 100644 index 000000000..ff15c1192 --- /dev/null +++ b/apps/app/components/exporter/index.tsx @@ -0,0 +1,4 @@ +//layout +export * from "./single-export"; +// csv +export * from "./export-modal"; diff --git a/apps/app/components/exporter/single-export.tsx b/apps/app/components/exporter/single-export.tsx new file mode 100644 index 000000000..34eb1510a --- /dev/null +++ b/apps/app/components/exporter/single-export.tsx @@ -0,0 +1,81 @@ +import React from "react"; +// next imports +import Link from "next/link"; +// ui +import { PrimaryButton } from "components/ui"; // icons +// helpers +import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; +// types +import { IExportData } from "types"; + +type Props = { + service: IExportData; + refreshing: boolean; +}; + +export const SingleExport: React.FC = ({ service, refreshing }) => { + const provider = service.provider; + const [isLoading, setIsLoading] = React.useState(false); + + const checkExpiry = (inputDateString: string) => { + const currentDate = new Date(); + const expiryDate = new Date(inputDateString); + expiryDate.setDate(expiryDate.getDate() + 7); + return expiryDate > currentDate; + }; + + return ( +
+
+

+ + Export to{" "} + + {provider === "csv" + ? "CSV" + : provider === "xlsx" + ? "Excel" + : provider === "json" + ? "JSON" + : ""} + {" "} + + + {refreshing ? "Refreshing..." : service.status} + +

+
+ {renderShortDateWithYearFormat(service.created_at)}| + Exported by {service?.initiated_by_detail?.display_name} +
+
+ {checkExpiry(service.created_at) ? ( + <> + {service.status == "completed" && ( +
+ + + {isLoading ? "Downloading..." : "Download"} + + +
+ )} + + ) : ( +
Expired
+ )} +
+ ); +}; diff --git a/apps/app/components/integration/github/root.tsx b/apps/app/components/integration/github/root.tsx index fea02bb86..2e5d9e0c4 100644 --- a/apps/app/components/integration/github/root.tsx +++ b/apps/app/components/integration/github/root.tsx @@ -163,7 +163,7 @@ export const GithubImporterRoot: React.FC = ({ user }) => { await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload, user) .then(() => { - router.push(`/${workspaceSlug}/settings/import-export`); + router.push(`/${workspaceSlug}/settings/imports`); mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)); }) .catch(() => @@ -178,7 +178,7 @@ export const GithubImporterRoot: React.FC = ({ user }) => { return (
- +
Cancel import & go back
diff --git a/apps/app/components/integration/guide.tsx b/apps/app/components/integration/guide.tsx index 8c8d05671..0b06e997e 100644 --- a/apps/app/components/integration/guide.tsx +++ b/apps/app/components/integration/guide.tsx @@ -58,7 +58,7 @@ const IntegrationGuide = () => { user={user} />
- {!provider && ( + {(!provider || provider === "csv") && ( <>
@@ -100,7 +100,7 @@ const IntegrationGuide = () => {
diff --git a/apps/app/components/integration/jira/root.tsx b/apps/app/components/integration/jira/root.tsx index b9d08550c..b5c086431 100644 --- a/apps/app/components/integration/jira/root.tsx +++ b/apps/app/components/integration/jira/root.tsx @@ -92,7 +92,7 @@ export const JiraImporterRoot: React.FC = ({ user }) => { .createJiraImporter(workspaceSlug.toString(), data, user) .then(() => { mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString())); - router.push(`/${workspaceSlug}/settings/import-export`); + router.push(`/${workspaceSlug}/settings/imports`); }) .catch((err) => { console.log(err); @@ -109,7 +109,7 @@ export const JiraImporterRoot: React.FC = ({ user }) => { return (
- +
diff --git a/apps/app/components/ui/integration-and-import-export-banner.tsx b/apps/app/components/ui/integration-and-import-export-banner.tsx index fd24acaab..9173a630e 100644 --- a/apps/app/components/ui/integration-and-import-export-banner.tsx +++ b/apps/app/components/ui/integration-and-import-export-banner.tsx @@ -2,18 +2,17 @@ import { ExclamationIcon } from "components/icons"; type Props = { bannerName: string; + description?: string; }; -export const IntegrationAndImportExportBanner: React.FC = ({ bannerName }) => ( +export const IntegrationAndImportExportBanner: React.FC = ({ bannerName, description }) => (

{bannerName}

-
- -

- Integrations and importers are only available on the cloud version. We plan to open-source - our SDKs in the near future so that the community can request or contribute integrations as - needed. -

-
+ {description && ( +
+ +

{description}

+
+ )}
); diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 295d55f77..97e8dfc90 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -237,6 +237,10 @@ export const JIRA_IMPORTER_DETAIL = (workspaceSlug: string, params: IJiraMetadat export const IMPORTER_SERVICES_LIST = (workspaceSlug: string) => `IMPORTER_SERVICES_LIST_${workspaceSlug.toUpperCase()}`; +//export +export const EXPORT_SERVICES_LIST = (workspaceSlug: string, cursor: string, per_page: string) => + `EXPORTER_SERVICES_LIST_${workspaceSlug.toUpperCase()}_${cursor.toUpperCase()}_${per_page.toUpperCase()}`; + // github-importer export const GITHUB_REPOSITORY_INFO = (workspaceSlug: string, repoName: string) => `GITHUB_REPO_INFO_${workspaceSlug.toString().toUpperCase()}_${repoName.toUpperCase()}`; diff --git a/apps/app/constants/workspace.ts b/apps/app/constants/workspace.ts index 76b099ec5..eeb5e2730 100644 --- a/apps/app/constants/workspace.ts +++ b/apps/app/constants/workspace.ts @@ -1,6 +1,9 @@ // services images import GithubLogo from "public/services/github.png"; import JiraLogo from "public/services/jira.png"; +import CSVLogo from "public/services/csv.png"; +import ExcelLogo from "public/services/excel.png"; +import JSONLogo from "public/services/json.png"; export const ROLE = { 5: "Guest", @@ -40,3 +43,27 @@ export const IMPORTERS_EXPORTERS_LIST = [ logo: JiraLogo, }, ]; + +export const EXPORTERS_LIST = [ + { + provider: "csv", + type: "export", + title: "CSV", + description: "Export issues to a CSV file.", + logo: CSVLogo, + }, + { + provider: "xlsx", + type: "export", + title: "Excel", + description: "Export issues to a Excel file.", + logo: ExcelLogo, + }, + { + provider: "json", + type: "export", + title: "JSON", + description: "Export issues to a JSON file.", + logo: JSONLogo, + }, +]; diff --git a/apps/app/layouts/settings-navbar.tsx b/apps/app/layouts/settings-navbar.tsx index d6962940d..462a4b6a0 100644 --- a/apps/app/layouts/settings-navbar.tsx +++ b/apps/app/layouts/settings-navbar.tsx @@ -30,8 +30,12 @@ const SettingsNavbar: React.FC = ({ profilePage = false }) => { href: `/${workspaceSlug}/settings/integrations`, }, { - label: "Import/Export", - href: `/${workspaceSlug}/settings/import-export`, + label: "Imports", + href: `/${workspaceSlug}/settings/imports`, + }, + { + label: "Exports", + href: `/${workspaceSlug}/settings/exports`, }, ]; @@ -103,7 +107,7 @@ const SettingsNavbar: React.FC = ({ profilePage = false }) => {
{ link={`/${workspaceSlug}`} linkTruncate /> - + } >
- - + +
); diff --git a/apps/app/pages/[workspaceSlug]/settings/imports.tsx b/apps/app/pages/[workspaceSlug]/settings/imports.tsx new file mode 100644 index 000000000..a0a46f0bc --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/settings/imports.tsx @@ -0,0 +1,58 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import workspaceService from "services/workspace.service"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { SettingsHeader } from "components/workspace"; +// components +import IntegrationGuide from "components/integration/guide"; +import { IntegrationAndImportExportBanner } from "components/ui"; +// ui +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// types +import type { NextPage } from "next"; +// fetch-keys +import { WORKSPACE_DETAILS } from "constants/fetch-keys"; +// helper +import { truncateText } from "helpers/string.helper"; + +const ImportExport: NextPage = () => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { data: activeWorkspace } = useSWR( + workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, + () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) + ); + + return ( + + + + + } + > +
+ + + +
+
+ ); +}; + +export default ImportExport; diff --git a/apps/app/public/services/csv.png b/apps/app/public/services/csv.png new file mode 100644 index 0000000000000000000000000000000000000000..3c35eb9f7c3ad9d30ab57d103160e5f7d7e6bc8d GIT binary patch literal 5155 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 literal 0 HcmV?d00001 diff --git a/apps/app/public/services/excel.png b/apps/app/public/services/excel.png new file mode 100644 index 0000000000000000000000000000000000000000..d271880f68b50cd560d55b17f7ca8caf31c1f2c6 GIT binary patch literal 60019 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 KpUXO@geCxxPT-pW literal 0 HcmV?d00001 diff --git a/apps/app/public/services/json.png b/apps/app/public/services/json.png new file mode 100644 index 0000000000000000000000000000000000000000..a5d0dfd229907ecedb176debb7bc1ca3d717ec7d GIT binary patch literal 56722 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
F}XsoF;4p#0dR;Yn8Q+ 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

+U-XT?f(e`XJh~mtWw92E7ZAZ$zdpz=X%Zk1z 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 { + return this.post(`/api/workspaces/${workspaceSlug}/export-issues/`, data) + .then((response) => { + if (trackEvent) + trackEventServices.trackExporterEvent( + { + workspaceSlug, + }, + "CSV_EXPORTER_CREATE", + user + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} + +export default new CSVIntegrationService(); diff --git a/apps/app/services/integration/index.ts b/apps/app/services/integration/index.ts index 9cc146866..2b32a5bd0 100644 --- a/apps/app/services/integration/index.ts +++ b/apps/app/services/integration/index.ts @@ -7,6 +7,7 @@ import { ICurrentUserResponse, IImporterService, IWorkspaceIntegration, + IExportServiceResponse, } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -52,6 +53,22 @@ class IntegrationService extends APIService { throw error?.response?.data; }); } + async getExportsServicesList( + workspaceSlug: string, + cursor: string, + per_page: number + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/export-issues`, { + params: { + per_page, + cursor, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } async deleteImporterService( workspaceSlug: string, diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index 3da8b8436..a87a0ff07 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -98,6 +98,8 @@ type ImporterEventType = | "JIRA_IMPORTER_CREATE" | "JIRA_IMPORTER_DELETE"; +type ExporterEventType = "CSV_EXPORTER_CREATE"; + type AnalyticsEventType = | "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" | "WORKSPACE_CUSTOM_ANALYTICS" @@ -776,6 +778,27 @@ class TrackEventServices extends APIService { }); } + // track exporter function\ + async trackExporterEvent( + data: any, + eventName: ExporterEventType, + user: ICurrentUserResponse | undefined + ): Promise { + const payload = { ...data }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + // TODO: add types to the data async trackInboxEvent( data: any, diff --git a/apps/app/types/importer/index.ts b/apps/app/types/importer/index.ts index 134248a71..81e1bb22f 100644 --- a/apps/app/types/importer/index.ts +++ b/apps/app/types/importer/index.ts @@ -32,3 +32,27 @@ export interface IImporterService { token: string; workspace: string; } + +export interface IExportData { + id: string; + created_at: string; + updated_at: string; + project: string[]; + provider: string; + status: string; + url: string; + token: string; + created_by: string; + updated_by: string; + initiated_by_detail: IUserLite; +} +export interface IExportServiceResponse { + count: number; + extra_stats: null; + next_cursor: string; + next_page_results: boolean; + prev_cursor: string; + prev_page_results: boolean; + results: IExportData[]; + total_pages: number; +} diff --git a/apps/app/types/index.d.ts b/apps/app/types/index.d.ts index dbd314826..8c9592917 100644 --- a/apps/app/types/index.d.ts +++ b/apps/app/types/index.d.ts @@ -19,6 +19,7 @@ export * from "./notifications"; export * from "./waitlist"; export * from "./reaction"; + export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object ? ObjectType[Key] extends { pop: any; push: any } From dc2438b2d3dd71932c3fad7589413a94f4fcf342 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:06:00 +0530 Subject: [PATCH 31/33] style: image upload modal aspect ratio (#1853) --- apps/app/components/core/modals/image-upload-modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/components/core/modals/image-upload-modal.tsx b/apps/app/components/core/modals/image-upload-modal.tsx index 113c5f98d..14e4844de 100644 --- a/apps/app/components/core/modals/image-upload-modal.tsx +++ b/apps/app/components/core/modals/image-upload-modal.tsx @@ -131,10 +131,10 @@ export const ImageUploadModal: React.FC = ({ Upload Image

-
+
Date: Mon, 14 Aug 2023 13:26:16 +0530 Subject: [PATCH 32/33] refactor: using webp image format instead of svg (#1852) --- apps/app/components/onboarding/tour/root.tsx | 10 +++++----- apps/app/public/onboarding/cycles.webp | Bin 0 -> 35952 bytes apps/app/public/onboarding/issues.webp | Bin 0 -> 52228 bytes apps/app/public/onboarding/modules.webp | Bin 0 -> 45818 bytes apps/app/public/onboarding/pages.webp | Bin 0 -> 54686 bytes apps/app/public/onboarding/views.webp | Bin 0 -> 40132 bytes 6 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 apps/app/public/onboarding/cycles.webp create mode 100644 apps/app/public/onboarding/issues.webp create mode 100644 apps/app/public/onboarding/modules.webp create mode 100644 apps/app/public/onboarding/pages.webp create mode 100644 apps/app/public/onboarding/views.webp diff --git a/apps/app/components/onboarding/tour/root.tsx b/apps/app/components/onboarding/tour/root.tsx index e75486718..a28d93d97 100644 --- a/apps/app/components/onboarding/tour/root.tsx +++ b/apps/app/components/onboarding/tour/root.tsx @@ -12,11 +12,11 @@ import { PrimaryButton, SecondaryButton } from "components/ui"; import { XMarkIcon } from "@heroicons/react/24/outline"; // images import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg"; -import IssuesTour from "public/onboarding/issues.svg"; -import CyclesTour from "public/onboarding/cycles.svg"; -import ModulesTour from "public/onboarding/modules.svg"; -import ViewsTour from "public/onboarding/views.svg"; -import PagesTour from "public/onboarding/pages.svg"; +import IssuesTour from "public/onboarding/issues.webp"; +import CyclesTour from "public/onboarding/cycles.webp"; +import ModulesTour from "public/onboarding/modules.webp"; +import ViewsTour from "public/onboarding/views.webp"; +import PagesTour from "public/onboarding/pages.webp"; type Props = { onComplete: () => void; diff --git a/apps/app/public/onboarding/cycles.webp b/apps/app/public/onboarding/cycles.webp new file mode 100644 index 0000000000000000000000000000000000000000..b76f346524695e423ba6da3d91e39d095748d67c GIT binary patch literal 35952 zcmdqIW0WLqv-jJ!ZQHhO+qP}noVM-mY1_70?P*Ti(>n9)``LTHXYcpR`FgTeuBu!s zE3e3i`2QlVy0TQHB_yokfPgf`MU*s@xU^t^fPj$x?%!ZQ^B_RNa!OJG1VBK**rWEO z)YFlln7yy@XZ>4k_E?*fYx4sJW(FU>z~6)&th!oy`WTcA`%Q%c%u9msM(ww`el< zbQ<&{GXr;VFmP^)@`ZJH zLl}&JaXH~iC;QE?CjYpGcCp#^xH|d$?hF+SP8O_03BeH=M)|}Mx&jGQv<@)cX@2Qb zEE~5a5y7zM6{hW^3r`x69p3?u>Uk^QkVqkuhACJ!98WF9uVj0f*h1X!B@MwKmG;1m zj{OyKM2x;F^s?BuE=bP#_skUQkMG$sq0DOYJd3I{QL%{-drk-3Op+!(zG7J>7rLtH z3ikdlh5LdJ%J0d*|Br8MrmEDaN76L{@Ckd-J!k=Qqz1)%}hgYwCX3&m8`~9n} zSHy{oZ2oGku>D$pv>+(YE2rMg!u5DtbI~z^R}v8bOZ+{y0?2TGF*FTy(^=TY)6lVo z@da|}MTQ8HsCirxi)BN%+;b(`Ch=o~UU+<^rs7r+RYg{OP2nvoEMh-g>yj7s%;u{Q zz_xhI*tjt!CUWV#Qr$CG#c2^K5?-DtvO0OkERB~N#yO3dk%$utr=?uJ7>fdckoSyF zb1g3b$ek&5;XNz{ubmFh0COjD8NV5IaFXQx$=%TP-P%XsTip*h^pf4vdq=00 z?Y}zdDLw&t6av&zOyYoc*K6iX(}aYO;b1*KQ<&pjY2z%&tb`e*&8!j~M%m`% z`1Y5Md{U!ugP(I|I`UdSfRN;9G1QHp1?tXj1n4lGCLXvY>n$B0*T%_6lnI1g5@n^l z!|=t6W4Jm|KOrf-jt}J|_JN><%?g}v+(v8AM;@l+z zUl84raW0uQhMtJwx9?ilhe^~7a8QVA!J-=jo(GduuA7InDmQhF`GCYoctFz4veM#4{sLF#)1BNs31u)5u4X9~9TES~`UAjr^~9ZTS(#yOHf zltZxW=XeHBzgsj2?q~3JHgK*EMx59Ikp6upOA+xtsHz?$0$e3_KVkOsOd=K?1Xau# zw7klM&ub@F0PyR?j&pvPCEJE>e!oJ==5=61!k03*_+^AVp?baglF~2oN%^HO#Ch{E z@sDyhX8SwEhrXjD@40oMEGMjG1H>qRFy4R*C@tHiNGmLzUi>Z);fjOt`WlYe zO!caJQj8v z)_%FL{yV*&PU%18{?Ko1JvnO&Rhy;LwO&gd}QSNmU5yd3qzm$hR(= z-<}l?!ALV+XY>{Z zAgIcULv0qOQkVMaq%MhI6^Fdh5S~SByA!B@++sO4)oL(IQt6*9v}-vFQAo*@%WpA9rFa{E|;$=Nwp1fC?~L;4ufICvcn*q*6zJ_|N8S8P3r^TyTqi zt?l6mgI&Im>Cf0Kccn>q7R0>KqskyZJmlDbvn--{#?+TczjOuMT`cDaQW1u# zuC7mat{;643!a;ph{P0mfE)q6Gz`uam)Yl3k8(eaXB6`XDYKsrb~-;;dn=LaCSg?- z`T}|x3mIFGQx)n1UfO*SaR}zX{rZ~5@Knpn^I)@yPs~+r7)51$xA?{vqv@LwaK^S* z?LUp?;B0GuO}jocG7zGY0P{}5kfgfP&@--sM^WIb$c9N>gj#LfU51F#PU*s2+iiO9 z3CM6=z%?X~(<7JUc)v2Tw{~nuWt6L|3$~$MVA!ySIKsNeUbP3+`^QUQd1mPADmMGI zkfNxw$}M-eju?9snypFOA{ggW?poFi^>p7T*m@8BTt-${Ge$}727~dyHYitf8A$M) zy^P|S;kos$KBvX>4tB9aFVux4mTYKkh5%(W+_cpXSd+(OppO*hYdsU=wX_JkD&Nx4 z^4xgq2&dg^3_V}{IM_8kK=>?yI!6~LxL7jLVV(Ahl;#XS{XH0rep%kBO}kl{!`F8| zgmQJeF4$3negg2qEYE5u=;%7l^t27~0mJf{%ZeRt6t&3swHx}<%LI61xy1N2J{xH# zR;gPuyBq0*nY9hyGqWT6Nk8#;blsQg%5}+fDW_gQ@6gNoJE%c2eA#cHnBjB4SIhFO z*eN&6g1vE?y1kwvu3K!~lW3bB);;%yazSEu88aoYF0UnEV-sM-A2X~Bb1{&Qpa$Qf z>p^dhJd`&9lX0JU+dM}=*sF12qtz}m?y>49-aTD^FE7iT` zP;9xXYM~K#U59IN4*&ch(%{*>ylTVRSt74tg|tx5D{{&b9)!`VU>?s;{`fQ4Rl&Xq z=g>S{(A0xFj<>^|LwC>tHy%Z`&HiByw{a9AIjl#BM?XB%V;q7%uh1A!92C$8h3l?b zx=+G%OMy2a%p309Gutf})z1fr>K4}Ww}ke5$qTy#C-4AiPOsJ@SnI6BW^v2WW?;JE& zl{=hH#Zh=fdO&?pkWV3cV#VCkZ?D%(Uo5t)yAA|KNjwSxVj%f~bWI4Q&os?Hv6dNT z-`W&~tux@;O5*FZ+I`#|qIvU@WabYUO3tR?=y@sfc|F*?+pAw%mKJ(&$C{;y1;d%I z_4GVUijDQ3)jg7WCL-d*Jhikh}oSH$pcY}wo z=kc5|1lTs1FV~hSa<-y9OO+PgSz>rl%WdtUjguXI>TTGMAzJ{*&^I)G&eet5N9ezJ zQUH)&HTp|nbvtBjv3>E^@Qcj?hDq1NLloUpaTgfEtTfvTCsy7aBc1OS9f-24RlvIa ze7r5~DJ=jptii6g4yZRabL=9&0REIZLt*vu=$A=%>b#1Fvpg7J74;-ioQnoL5J2yp z3}e866+c`(o~GR+K{3Q1-_Kt#F)GG&{ofDK_p9V+dEx%2Gl*1-jniIjMAn5&jE@nu zFYxLeIOuB4nmmt#(mH4k!9EPr!#wOS+nA4rY5>ZgpR*6*Y>KN3LIJ5M*TWfax-kLX zG#0^)#QlBto?Qs?Qzn)IKbD2?McowZ4=OkyoEpV=1( zX$iMCxq^&W)^BKSpUQs9m+5&0=;`EcJ?G@Y34ThRq^d~~I;AEtcvaK>Jk@NkxvY-<^y}mNfsYeSylzYF{F@32E?MKm*Jn4k8fn^=A=n_D-6S7?g2Du`Z6z(=e zWFKR89*;7|5x;xzL8Fk>HI9-yS;O%g4=mh^LQIHS%DE&=wdfIZ90m;^9mXk(8VnAI zu%&UXR@fM3O6O%$RTvX_pZr`?<%OjPmqbA5x}cey+N1IhwmRQbgR8y1?#zS6a`>N% zJnJGPQqQf{RTXGT@|b3B**RJ z=O);c81$Nv0LH*KBKR8io?ZW!S0{hzB|o=RoOc;4S}qNDUHF^q`)z+*dQzzu-Lj~5 z3)>gda|9c-!LlDqJ*mI2YV@$kk1xCp!kU)_)D_S)w$qi7_ycR*m0I83#)SEzmk)iE zdc7Pkv0d=k=RNbMd;^J-xq*Do87I;I>+!(i=m zcY24BH2b_OvngYe8E4}0h*^t;ZQc@vO44%et$ISlgprq4szXdwRY@%px~5G}VYO09 ziq$cdn>8n`h%1XM{wI9plQqg&4AP6K!*Q8;)Y_$R>}*p4szxlb&fB>2#A}Pw|HA$% z6W*QcHpg?H27~CAUQjzz63z)y*;Di_Hz>p+44ROXx4Y4Mk8#|oKV%LQ_>N%4!r=`H7T_7|X- zypFsdwi(+Kg^a6<_nH+6X(W|nUm?XbGTQ%#Rt1B`NeXJS=tfio%8LQd4>TZf$ZKCR zNg<$oHY7BBtk2A(6x|`SEheS3gZmg5s0r~YD9!L4&Q>oXFi}E$-zt#>`FyCx>s=5i zs8CBKwT{+D`eZle*q>IeRrM+`=UgFbiuYglnkV9gbT9e+_l_=aW1+`WcI2vwSIGG4 z=@H#%<0ZXz%ZKPfIK`nWdF1rjCBmDDNMGO|%TEFFY5vaCm)I$JWk_K3hwI=(TsS%D z*-LwC+~Y044@h^wBQws6umq50b#e>G1pufqN|o)+E~Oo35Pc9B z`e@}xuOZpmPK4OYq2drRB>wRF)k;>xjMh7#NCM&o=BkS;cOe%E;lylc@U^%ON=WHv zJbNpeVhOc$8uuudGihRnemKc^J`4*t+hX>HaLH!@p5d`wsul)d)&tfH@`nvlOW)+i z^_FQhR+VF1V3XD2O74`BoWPmD<%fj^`?Z__J%&i!O-%I+rHZS5+kY;DN^wS`Ii-5A z#m7753mC}h8n$*WStHRDVu>;k&vag9io_VMW38a)b;?Ad?U>`2!rF&c@|3(6iNNu~ ziZnhujwERUg^$&=e=c92)M}VW6Qhpfp^PVJ3r`cH>DxMAUhp$oh=spS(>CLu+K}c5 zc~8692>0v*mUpj%ew?S|f80nwRr&6A0|T?`BEGev*2yYgVT#SwuKZffaF|mZOfW>Iu1`g+X-V(B`KR&%H%6tdF*b)QJ>( zEyL#`%E*_CKZ5a%@t9=wQaSEzHV0Zp`%F)QFMnZt7zfEeCI>R$S-1@560aT-2!=5q z#}-`rPL(iWKavN9X5Q5?Uj%ALo;W6adHn<6ZjTp_KAq}puW|hvdn}v%^GI zw=C6hEDwU)mK_W}F2@QlHlMtNow5&0JOiGzg@xukDn`F_df5r%VK$WuBUmqjp@~fa zo^^@`=LAGv7Tv{em_Di3>2W!jO37%@`*!;)@QmD<8lA{x z0_>>W@g{)TH1?uPVW_vh!7#w@B?G;y4stE`y*rg}m;a+Cf5q*det9(*8?qx%RlTUf zyNjzL;5+U|3l32HD0kPk;!4kYlz6S~wE&Wnk$%f?*TO2}>@1$-z7VI^$$CWjJ~v73*x z3C5Gyc)P{l4`h}{*A9K!Y}a%J7>R&$oNmP&cN8Rbup1NB139vIIGDGw^ubJ3c#d0s zDmfK~UEqM-_U8jVBJr4Ks#IKYv4g90}sU2r>}08|o3C(GZ@ zdJD{2mTKrX^3h$?s*N(^dGGh9;*rI9hpe;>!xVW;xUWQ|`eG&PJl2E<3}%{$0+~XV zB4jQL1c`uuRG@k3)R*m8q_x#|tWvO4f{1+mewYg)(>cUv&cmsU{1U4-{|D zku&(sT9V1%jaxt)>biA)9Hpt37+h= zYS)K36LX1H=L{=sx37Nu#~~alaYjO-Tl5 zXDC&|6s}%Rrd;bp6p!A|(~Yd733I3QU(VW9<$+r(D?lVL0|4`S*a(zK^MRDX^TP8i zpKa8@0Pphf;M_*JkJYz(lLucXv2x0_NKrngjmB8uYAZvWi(zil?c_0Tk&7}abNx1l zIKdj|KAH<4Cz>OV@{&9~YwOV+T4gxGKA5wDIjr&*o?NvtN5{X2YGigt?u>CuRsB6G z(k*2;)ck->nJ4W(=dy-Gun_G9`%8gnclz&4)fjW_o_}_@r~5iP+Dll7F}6;w&B-#U zM6+}ect6+fs~H+TO$t-VKM*d{eSN$1?l~6a7GtiSfLCcVh`o|kGO-Pjm0}J0JT*Q% zGS2{fy_Xi6^AhUe%H!FnRfZ>R<7EY0+taH=oAbB%k=!jZ@`aF7%hvumUr~arfXDAN zP%mT6esTij1u@(XXV^=Mu25TOc=uS7k}>HPTsL0ZBulhMIc3^PQ#d?bl8w!?t$lvC zEq+g&$?t2FW^GL2J@48h-6Rl$;uPL`VZ7_!+_XmBDwmhw9 zLjO#6kaPRCcX(9AXoi}L!8eSZ2>oJQf`pI(gtCpQ4~oC@)FW{!>o&A~fHR5>MYBRG zCywlXF2PbK$umP%I@elf_k6&s1L-B){2WgQKF1YYuSBg&A94kL`qBm220VE~cfWP!ST8jMvvjcF(dn1d!?0P*p7w+8xAtQvH_>IFSf4;!+)~ z$JZ@2M+|Mc2IgVl-6PKU3W1p|rqpewt;(F?Wfz0{z5wBpdf@ksbA}-IWS4G_hc)A^ z-sQgPO7+Sdx+ew%Soc32dgH$K*j~ttY$8R`?$?M=k)VI4>Si;lRSC&MJYGv?a+Ro; z9m`zZbt=Po)c7ddjS8xwb<*lRISDUYWtraX1ej>#CUj} zi%4#=mP@?kC6)rTBJsBStWp!)Bk&LnJJ_~5**>Xdm%X-x@UtSl*YTgNEqKiEEsIM< z6&735kkWGtkZA#@Y%ypp>NM>k#p=}D>h!D!x{YwF;?eSMSKCL~cOGG|!Ct|}|7la4 zmnQ^SkOp%}Rsh**^$rd>Urp`RKW$>Cm1A=SDkC{%{m*-BOT@%3L4F$ZP&h8 z>I31P^@3gQ{%Eqc$%AKai+SAXr_`(&v3_&2Z!AvbJ`muW5l;G`uK<6ZcZSx6ZdkoL5hAT+RsZVxvrvedfnMu` za^9nnAu(WV**M=F#SrRYB)Pjkw{g{3XxcbeNwb@S;;ve5l%Bx*@@qa8L7aEgQo|rp zp2wKuR#>V#N}|DiNnl8SqMpEyDR?eS>bQ`fpc|aGJ>YIfeQ@q-gtxjf-Cgv2RT=D@ zLb;1W2Jb>mM^kkd29A#!!k^hO#W9czb`*c4D<>Iy(gx~M#dnw?*bUia?*002mIBu- z+eSvA%;#aJ*F(CC!6mqC7vt{m`;TEJstF%2mT{Zx8uAWWFClFDZR55uSF%Pa{Th*N z*`8(4@rzL}WktecbbnW_IKiIa5bx=w>A|@XI|x(}WZM8=_PvGnV0Lf!m-faCUa}7z z!njzQ7?|w-f^;=~$Q|v3zmj{kVBHPaE(=m1%g`DnwL-{qGgh9M86{Dq{%MACf6eV| zAItCRcADch*R$oLX9R!!gSDD0RG)}PitZ}xy-H$5{%a|kWP%SoItWvNFFhz*O`N$r zm-bCDJ5;)7`a{|iehV2~y~e2)HCZ25jNno(Npt3)C@&_hL)JKNL1MdokXMSEeT0-| z%f%M*hhN|giG4SDz69PzD6j{}AEj+c=2}>#t}KBE@?&yZ7$~)w+6OLceE5zqk2q-C zdhCVhH@t3HtHHdg81Q&0O0aOp-ryZr4^*9!&SroB=|byQl+iN|2{ocXTRXgusrjY| z%7%Qlc`kL%a~E>!dtQA_6CiSXxF0;i%6o=XwBKXEAhNt^A{`dK-J#Xk{NdTYJOQ0$ zU7CAhEQFMm5JODuZ}=Af40@Tr|6Ugy{C4~7dPsa|sQG4n$NlE~pq$;?>puug{aQbk z-TUr*2z(3N6e${XCZ`u#f;BpKtyI?)6Ux0Q_pcN53LJ1+O_<1#7;qz6-wj-<_YKKPvw0eH7dV zo_yO2czj>_`hQM*cYX`*&p*w7DW2|q3?lAeb}%{_?Dr1(hP;4Z!~Xy6@QVJ9%u1OD zzr7Jdoa$yN8mk{sGq%59bu4`-UaJrh`UKh|=+0a@Yt7jjiG7zI?*x)P-+f)*>(}({ z;5&5~p5^P{OXVvr+HQChiQ91rrNEyDqMM23tDgh*g_>S!!m zR1c!w3s316+&59kLKo{OKeotvZkx2F#k$FD4z?*HlPD;T5C1>=?Q zR$gWUnE70cDF~_v2O_y28yqmFRk6XK11L4Yn>$HJ3b~1q3%b!*BM=^{TiG56!6vAw zH!7-P$heyU-$Vt>Th1#BT*To8(T3A#gsoLT8Qodfq>t;Q_rg44h834oSsYJQ$_c0{ zWz&h^=II7n4wN}8z}*OGo`n1A3!pw2Vakmigrj0^Gs37iLaygExpdn^CB96JRBYs7 zB-DjYqOgJ}{XeJ#ZB>97Jy}>J&+8?RL)@bJ71xwm>@QVH@F>b;GYDX&X?hxW6gkX5 zJ@Bcn1qW&hAs*?WN{mPxZshHo(Oa)29b26!j5}<(0YMGUmI40%NA@{x_hJ5WoU9E! zYF5d&1*J=Oae=;hm6^T|uw>~Z5)=3O*GCdiusb$$^EW{}$t(|S#JTX$PhLC~HK4Mu zyF?CUBZD2u?(*5sUp48@8Ge#CqYXc!)E$eIKtwPQX1T2pSmu; zq`HQ25KM7ntj7tvyfv2C`-du>VZ%3W?B@FmfVMrY$w04!iH(aslS`@p=hgpXFvP7O zNTp>gGN}5bT}6+et32@ROc2{yG)SD;11>U%sP(@Ej~B7y7d9qtHEsVQ-N}bkqDWfu8!N z5aGMr+uZ|G5b-Zu}jBgjC#Eid<~GW2;OiLQT(AM;)Ee~dRJ$o#0v7-|20s`h>!oEmyCKi z62Idzm_nq*B)yW>zHi%D2NZHRj5IG$``KxYc9x-gMO*)beyE61jsq>mJjHmmr>2Bu#8*qr!)Bf)CUX<rX{egaG6WF3*QYons9?~hN3XI%`tVa=r-#HU#tbx>_ z!{p+Hy;Q%GIz}gNz%7M@DoK>HMe{EhiPD(K40H>!#P)h{{^tLM=D$`r_`&9oLHwAx zm(J}U&QZiuXM@fUR{CIii5P`|+=-fy6Ci-Ti^N5xW8hDjAkWsI!VKK`8$lGH^cQlT zKmlZYO8U{fxBtr>4j=zC5OvKkJsS1;=O(NC-_Gj8e2R~qh2w22i!rnWu`nnPuY#Bt zqH#1)0!41rO+X1bcMFQ#8RqOY7~532CcnINPc z5^m?I0D4L(EU*2i#HkcUUE6%{{sKR{a24C5JTc37n|Onr5?She(TU3mMKRkWJw(=9 zu1iAsU63^+*0@2ebPa>^+5u+F#ryP%wlJkS@vl1hTuI^j3^WMY$=#?GQH<}m+*4@^ z|EeER1T=s4oS`*qWGojmTM?aK)wbE~Qo`3Yb#XJJ4k_9!+wJg9fcMe>TF1ZR048y( zhR(;T_;(I1)nj-0>~Z>J>>CQPM6opJ=c51ZwB0zlJ)T|Zb>~6Y^SaLc>@`+O&L7$7 z`8k?*$Y7F7c(B$0+%_9L9S{P61iCJ~nNK*fLgyqy9=gre^DQ&d2RF-jEtXTQCK}7x zQ|ICnlHK?()9d4^M(*)Vhox%+@OQFeSQC-VfZQ5)bk*3b_`1!>)Mq7sXaDupa4%>* z@xWni9}_c#Um&GJX){#=QP%Qa>#^N_RBnbOk1MOCoG@IY7d&?ir6K3RJB;$;RoQM5y9Q05* z^a8If_rUJpIMJoxb7w|%kZTToFoqjZNtt$NkTLJjqF{kz{5x@U{K43L0}dvdj%i~v z#<46W)MJQ8^FBBKA{51djXuSc`be44Tf6)3$p6xdA%wy|?tTG|-nyrT-8WkquqUf- z#z#c|0CSaFw9*?pc4UR8jQQ*8ldq0{SZkbrV?rnxM3f|v=+2hq80730U6@c}jqy)y zJhXIMbt*FL7x<4(Z7IQ6$}V$;Dqu#Qh-o0Ia6kfBL)8!7t9d#9>@?<;Ld7CI09=T%-`NTM)^p#+S0Sb^K%s-36*qxrKRIPeu)!H~Lf zD*>fuP~XXJZ@NGvm3HhUjb2^>waqC~F9ZUpGG2-45Nv4^!txIh*LA(aRl7`AuKf%P zjayppPv2*8TMhqDVC{{do^V!uEMAU%4*CXGYk<^EFssik5PTZQMVFcgK_S`-e*T%0 zek@sy)L?}s3vC4_E}NLDubWnXqJD;p)l=dUn~-H82dD6C{O>k(ix_2e_K`S@@7+FR zuJvy*YdeGcj!#4J+WHj|;!!3R5p&n5@`B-cCgfrJpN9PPoR^!c$dRLE^^zlO6!~8z z{lh*iW{Lmm$NoEXe>=48*WmBjvB4dCoWI$6=YBvmNu2Cw>LJrX9_M-b?@sytpHli< zvDje>+?;hY%@N~SS@L$xzLY@i8_WCfJ+JtU6Nugqmnj;)c!2s}##F)*sK7lsJqq4> z*C@zYQSKGneR5czQ;A5RNU*YWj%fvlSn9u!I6FTEo`8^(wYcAedk~ z8QiY2@>e0(0Qtz(>k1qa4x%tEwT$z?f7j%@pIkbVho+BxUNi!ORlPT+KUa&Nt0x5x z@1URxVT85R>n`1v9TP&;`U3WQheDG*Yzi!B!s>aRsb&g*x69=IA>90ssL7i$8 z$r#6emfnke=u~FoV+Zvp@90VCYGedCCMq0gCE4LSwWUDMsuJbc6*r$l{1=}?(Wrld z!S+%3@Li$gw$?tX=ArY6!Ea2S!=9oQM5gaf#ExywN;(pNGzA&7ap78DW6}UCOwDwM z+Eygran4p6{1P*q|M9QTv~oiKP`P>6wvz1$NDbLyUHq%mpru>1f9blNVt6romif0Z z-I?=yv9M6Mj(S#bN5`o8X%C?&oL#Ts!;9IRH~oeR6(0VIxoW|Dh9et9NB{x2qn%$g zS}4D@{ijEw%$NJ;4ANbonX1lm9+Bko5g&jl@)R${V?pNak6KdNAIo&<7F zqV~!h$oLwXM*2(>OfvxLA?@QG&HX{gx}is`np62;gsUt5c=N^*N$P*ntLcOgOq5Zm*KZ%6BWM(s?8p2QLIn^cXvt|=IUJ<4@hvZ#TLVrzo9b)ZzneFcH> zI1YD*!FMPV9muZgiaW^!w0tbjWdN95_z8bq_*}rjhc?Jx2?jKP6BiPpJc2FO4pEtu zCY5m7=DeS_tpNtGR376?KJE*TS|wx`tK?LWj{oJ#Zg&={NCYvJ7pZ&B&Ouk|%+-Ws zA1ojE4n;S?%YSgG{%d(TG|l_)Z+RGk{RQ*C+pB)&ScOpydpbjfmJ8El zd1UvvuwLlpQzX%G&vyKJ)~kL`N7KS}Ne97D_p&#NF_Y7-Zk$oIoTX55#yDHX--Et_ zmf)DbkJtfV7QxEN?%Y2M75(gXd5U1323;QDKwmSa_kl(r|1bcg;<>-JDkqBnvzx9? z1kZW{Z|MPt{&x*w;xTraeVA~Ke4|AL+JKxE5i&vs71@M|?n0IVJ<<5S1)w)l%nS^c z&%NRv1GU}{%h$l2Q$sk3t~u%+(*CrjSu3d%f8|3`lW;;m<2?T+TT!T-4$tcPQTa+k zJ)5I)xaM9-1O)W;k?QpKVf+8RLGkDE@8z2Q@GR~nDuRd1HMe%&!WBf{&dwxTcg3rV zPT=tTx~{7i;3L{juJ+V^mmSR{T>V9Xf`POM$|kj)DdK|~v8k2SG3bNM`$oyLv6xg~ zKX`gWSS0cd9rB1UNFnj&(l;UITtqx-fGr`k@3d`{t!J<7)<@S>`2E%q6C2=eF(+@^ z8sj)i_QyZ%-Bb~nA!HVc|DzanB1;P&N?-`zeF7mGEiO&9#}C>XGMY9*0IdslZgt{p zyjMpRSrfr|?dStY7kQz$9$Ss;v49y~>Bbf%;oZ|lPVIbfPpM?KG4O06ih!^}a=Vj| znnw?5vvJh1HvYb}0M$AmXdq5*EQ|Y-^rl;mTgR{6JKmK!fR=;;50=#m(Yz}bx=ynK z%0fm{qxR1BMt#Xx464p1;^%W1UECP4Fo!yau2S96EV!K^OO!vFb%-u!-0{>;YdPwa_}6 z-3JGaDS2YbPLrb}(}AaC0IeVPfnb&tbR*nRsNcRv`{lX;l5*yGmn|S%B=Ps-1FEQQ zq^sCcxkZu&)o1>jA0R_~g&{6J2S@EL{{xiSTHHM{1s()r4Z( z{=-Y+Lhg`?Tk{57lv+Q7=BAanJb7q^ZWAEPU@>#r3XMMpMGDTk9-*I!>H*VyBbJY$ zONU3`oBxq!Z1eE=-s+w@pSHC~!l|?CnrpLwrBBg2=h{yf8-5_kISo%d;rk-fr|U2Lv->!M@v03;hGz45bO83nmeJ^i?PlG8 z*&19l5WN1>&&yxZb1w9FaTWq*if;~iDp~PYGj{3IudJ?&hEUKN;k>T(m`p5GeXJf` znK-J6{&Fn2+n?{--ftt%CBtP#>5+$pxsE)H;|thxbsq&9(n^MQ&B1ue_#iY}S~!~| z&B5!&+l-#C?=#^8Qj=;9a_iinvlj)BE;H} zASy5rStEyu*D8pYRY*g)YyNU$#1}p^Y_UcB1e?RCe%m zj~69{f#}WC1LIMn=eY)2QBU2(lQ8-zLm9IEY?Jl za#LVV*{l7=eq093yxQn}^Fgd4-8#J4eohL1P+3!+u_k-H#@Hz6Ncu1#j6V3O&9R&5 zZXI=0O(fT)^A`|()S0p-Ts5ALVrE38XKS>ie|IYdjobcBrSi#!;G>xsBI0t%)lOns z9s`u!hDrOH#G8s@bW6XGkpzXQT<%x$3!ogYPC&P3tTVG8J-O(KcMncYd-s#YC3wI5 zf%QZ-2h2?8eI+~Wv6+ZuKRpT+;B9`2uV$ng1ZU*@};j^&@&G2&C#GvfPwnps0<&GomjClX(!$g*Uez zPsW3`JHchshy%lq6rgeC(t(9l@kQfBMf7`s>nIe;Ul%4*`Y_zbaePu3icbRGX$8Z-$6Kr-TT|-!EB3 zQmvMZfEuIa1s#i=i0zeuykNieu)#J z1@^5TYR#xYx?vg#s4SshiFI`S`>>%1_YP*Ue?SLye;bH8bi&@#4aH^4xJG=bkChaO z=dKNK&4D0zd{I`I;e$jN!Z6;)AIaXg5ZE-siPMWjm6C2-uf3Z3En0u2rkDHIDGIa3 zzA-Leunoy5mssllDZ_h*v>Ty;vtBcQ*z;@hC1u$*ozqgNAT0# zxI36x&dsnC1F#3#Qa!Kmp4`2&V%6qyod|99ub#(0tA?0B*&)tX#)y@l!}AI;@A+c) zx2&1t5eeZDLbq_BS7me;WpBS1o(MO)-P+t(aY}yq2+YVLE6xvN;~nT5@_Ya3CiNcy zR5lNPtk;pP(Hz@n2)9jVBwdo*G$Bf$Dl5g+&Y$~~>kg7*j@_1TG+%UUC7g!~1EnB) zm-=eaXnQhQDBLOKg5K-5h}pOhHrJ8Z9yF`?j-09L>kRY+ z%I_+sBJmVr#wyEQY!L^6AbsJ;AL_JPk#V-GWMCESbzK&%mtP|7m7Y&vfezv`IHY71V~?7n8;czseR$y;)GK7uf`-Oo01ewJOO z)%290e{h7QBh{Z^ffFb)DbHXeoyJ`pySr#{)Slj9#a_Uw@@-zEenestw8i|=#^j14 zir@dYtQJ~CY&bq#y{h)z2g5y%TdZ%iovWLi8Ys}ao~n(Zp_Ev;;W6SC(^-P6P*tVf z4a3EcJxRpw88`woH{XGD?wa<@6p)F5haRn{Rt8o)Ha#!TM9FMM?u4yfPz3iol%*vI_DK#`xQ}0>5jKS2h;W87!P{d<~Ka_c9$v*`J zRkHX(?{(?RvH`tpS>_|v7Pq!-id6-td~n26_CMRnMmNX7oY>NqB$k!>LnZGw9JUnU znZ-fJ;dO~9MXZ24dMLW|PP_}3OLAPflF3Edgj=Z=#5q$tCy}T?#6WIR-2`joTqw=| zRE!+z!bJ@~v*~JQV$;A87)NX<)>ACj>lgaZ77m-;eZkbM-N>4Jzd+g^y;k6K{Gw>F z2^Yy*F!{;T%MWWLbEk6{boT8$opR9I9&xpTD!pwp%PFX6Q^YEe`Ma`Bnb*F2Uu^F_ z6@dn}V$P8rPSn9&RmT?8Od3jZA)p^op5G%iaYj^2e;VX9H*^vKN>c;n!2Mr5iJA>( zwdWlsz_iCihJG!vC~sg!ZCrNsI>vJPfea653O;@)=e*n6s01*>#VrnhlM-)a@*2dT z-Mx7JR!a;gYgsWF;Mz7&mG|3tu^Rbeh)L!a#{yVLCq9L6fCh^ zf^CGVRoC)hZBOE`qP@$FHjr7BW2IUa$v7es1pP={thjap~kv$c(GlQg!MB0#pEUC0AmdvDoLgoTUl)7T-lL}i=G=vY7am0S6K&%KuOd-Qg`z& z+Geph*VmFgdf=17L;w1zRNeLv(A=`wc98x+JjRAh+}pYmtPN(>bKeB+Xvil1n~j*Y zp7d&;RrPDc0&RCkh+k-4ZxJoI*C-2aY0fIE1UNLsCuiPhhu!+uAI>z)Y$5~RDicl0 zibdenx%dP86$4+0Yg-sK;zw7{(0kBrEk{nx1sEyHs>|+_V5oIi_0D0sIHpgRCXPXU z0++LR80C*pu2#*)+`!BW$W4o7DBEfOPIPM@bqQpaQx{1aZ+IMzP_<3I>RWF1dnGag z<=cmY$o;E5oP&o4d9^yNxABHsXSjN@J;@gM0pepy%_ef0JY@-oGdE{lS>0A##cUb@ z*M*uU_2t(;*AO)+T7FW{uv4(4gJW&%x@wZ&nw>kH zYbEXv|NX;4^r<$PUE=&oxgycWk7;X*c1L*%ZD*RS9-OPx3(L28P$(H(yShnc59dKV zcyhMnmKDbR#g8`Jw$x!cWud!ajS1+5Y#{sw==u!qd@Fn9vv^kzWS9oLUx5~k01j5P}gnNY|~<2KaG=oZ@yD?(Q*rcJ)Ukm{%I zrH%g2!L8RX>alum={lSur3yE!+Jpv0kqwj{jgCSd!5x?54oCZgZg;RHkF93;`Q@!j zx%qNl8u~PaZR-wX&X3aJn2_^_oLNz%?L4)$?14=b5#&e0hT&yJ{nDccq%};Jfj&y2 zd(jZ|)vykIwaV+QLsnzCZH3$M@NMxCkpQaAs+t+G-)uX(+z@!=VWOUPM&5kq@gE*- zG@Npxq-;*#6B8a329r2CA4pq-C8E7Ubt5zD*NHnkufLGgAD{1*$os@bn z_E!^L{k@P;o&iE|o=0MPC1vfH%g~(0R(QHgHrH1MFM{Fe@aG8m))i-jtz=adAwBM@ zge1P?(-7g<(nk(T<7lavb{>w&&?R7C$-Cf?P29~)RAH8{U@}ke!4puCn3Pj74)G$x@qGX0X3-LSU2;!K#AYY)>Tj6e^+1Lyoj1MV)qezP z@aP_kbq^nguNk&^?dt_1!z3^lC3-^Yb7I$m++?I)&s<3y&<|O+*5LMrzMf@!%bWvq z2DR;jAntDxh@cz3BE5ILH}r@YwJMfVOVUKcrK=(t&Sb#zwR+~XZB7|@@ZomhTh%;9 zvaE5!?tWLHuHjbS{hrS6H0YRjQXXC@Zt~TI&UQoyb`iGkZOL7e!VIhPRQL#o&DI!trX!e+MzZeVJ5} zvxAM8Z2;p#z)7FJo*mqedX@nm)x;`6#7~|D_np5-*34q4k&f-0I}8s3)o2IK-B`?i zHM*NZ;DU2*$eS0p3~XN8QLBY6req6zwmCG){n#>~dQt3c8d&R5 zqkcB5G6UOYKmPGHr^KXp-9Z+8QTr?EW6!XXH`3E33M4rGh+cU6a@Ed!^n^!N-Jbhw zi>CDl8?3u)o@MtWfH&qObaFlcxM?bRej$rc16d8x&gf%bdEM%loUx^>R{Ogqrrw8J z+Gmx7+~i0b;CZ%>_sEgh&id5l^VLFp^&KMR;PtC~vqy4)m>j4+5is^v{|CMGhfOV& z=(8k3m-&`y$QA~M1(xj1o!MhfB!Q6{BNKb(cz*Vbz4Q{V#4HdnTP5s}auqX7&Rx%0 zsxa=QVd5OHW5I(&|(&YH0m#6mBMz0B~9lrHtbq~Eg4-g*3*U2&65MfI!Bz){|7%n zz`tF-G%g+Eh0(f>K`2cuPucSOee2*2!hY*khmK3mWyr4x70LObvO z)YJ;c4UHvT+4B`cv5G8VQFDFzZWJ1{?hUA1b<+9@%RZVKH;`*wiRwoo2~vE>vndtg zgLf^6Sg*-2kxfYNpGr2B_%9$`K+O~iIPO~$3gPT>_z}w|jWJtl2lB-Pcs?IiLc;p0 z2(?^q11Vek)-d9U;rV>86S#G}V*QeDNE^L-77FukaeX%d@R7%!H#a7U{uy_mnIY-VW`TT$&nXWkFC1K9t6gkY03N-Oa?6Tw|(O3 zb6UaU@I!q#^z!Kd>l~jr2V~@7)e0=0W4tt5*Qi_{+h6CRMqXWfk8#mh<) z1z-QkqN#9wzM!g~`BB?!!%wFNU9zx_9xguODpCj1%)}{&a1@ld$2uLKa1QkK8Ghbb zg3Sa`0GuQ6e=;UZK#b75Es(irrPOg|avH}x1Lm>zA5O+mUtGu$J=kQa3a&O2$+ZOq zsQJWgOw@Y5VgQI&~4+Uq^wXU3i;aB*0#Q}T3=X`&x?>vRbqMU zjr`D?k+}IgP`L>Bq)0_)U=5%z5mD4(E<8c*K|}%&y`(D;UE*Bm-x|m})`c@Rq3tc< z@yMdc6{0jNqmNuZ0F`NGK2%KUOd4~MhyviM0VpCutgzRfF0_B6PLv@Cyo?hym|M;u z7z-nCpmE_HQWRCPy6Nd2nv8U~(*QFv&&_3OAq&*$G5`Q46v&BJ+e8!|FE)Xtd4A7@ z*=eDF<+&OFj0z}FP8Dz=LXvMHK3tCw6A-bpumuzZ*XJ|noSg^q?+?cGEx5tY?9OQv z1&Gcn=~qSY)TGNmp?Jm6kW$d3V5Del0^1yZvX*?(w8wk#xDTw)!vOjn*)7Xqo5jjgbG2d(lV;wDEC36cgf03TO4iu=>CXV?1mjCjbFJk@@a6pbXoIKT6-{>9iEpBJLLMxz@P6g)CfD zt()T0)F}q}15%%Y>dfQlqf({9UZNEM?B}NO;uoNk;VNps>`d$7$?F$n_1I6CPPKo= zyw&hbMkkS`TO{LIqbXq5&M(n}N<8u)0q>Bg?gQ~~|61RG`fj*ogOJEmEA&jNnnI?m-la7dL2)Uy z#n}R^BsIE=a{~|9*c6`GfiKdh$J?j$jn(XlMpuQzgQpC2a`LJgyMk}@@ z2kcS|9gCn~oU1UvT_H#w0wRANB0fGH@h(*%{sG9NU)lM~FFT}oOthooK##P3W$p?e zHEr*r&^}vYPM(rPEvzpm0)z&wGXe%Pl&kppSgM}|KYi@0Mdj_W)i+N~BMMM8p8pXm z9p5xKtMxl=t))`jEbm|6h0x4TRd2GWQqMjT%fEdiklXfQC%ZmQZy&4yHrpgxDbo+D zKiBlejBxffEsKsWcCIX%m90Lx)OZOgNLs-@*pPny+ITpm>WDw0ZNcrY{B4lbX!^%;Ha!WV?l=yJ3cd}NnDOjzjMD-;OS!akDca78sGJ6=t4<%lT z^43aeKKP{z*Y6hp6Q-URLHwzUNlqb$8g9(+)r3oru5E-DLPUgdd;QSSTY*7gI8T-LI#wuD9L zIopp9_>(Ch)SDhMyEM2IEz_6GGXOv&!-PGQi?Zd05QdTLT zN+|k#QWE)Zi@BCuiB_4X9(-v==)tCe_4PL137T`l2cT*4S_DC;!9}`6TM1@a@YO;d z@a+HM_iGc}-~kMLbhwR+3i_t!{V{ct^Pg3|vKVOUWv(=_mu6RL0a*F&EKZgQ0x}$p zZu`fk=(!@E^{CF9JO3e)D{N5v9pYc``5Eqy~u|zMXUtHO(k#;Ip z*wg=8j<8dhl{n=VK7t|=RFi;=4pW<#iLIc_)}!}*R}(&P*xe3ALzmdhHQq*`T9UpC zs(XpXm)k_eS&naqS-IrH+i{~#L-k_^c}pnkeYd^We_8pU>jS7e(oa@eer6j-dn*I> zIYSBc?9S@uA6`KMb;*ZkCr|E^^_iu>LmWnn=OKh|&2{dZ!PUrcQ;#ta{rX4FpX0T? z6=SG=aH(;DHgUr%BS&UvCqn%(VN!6H30Cjp`@Oi?;yITsv9OFLHUcpDvt8Hz>HxZa zi#-FPoMNypTagSQ*!}Fz+^7tCDxpU;sKOA|RR zCTW8?AGgBv)$JjH;y_I%xw08N=i9CSz%`A7b-Vv|O=)pFS-+`@vR?v-(r2Uwf-8p< zeAh-#P?pE?o_*_+TtM?{F(MDDO-evd-;Lj8Ym=rJbw$tP#Otx2_NKh$%nP^xsqw+; zLiqg&X6pq;fd<^Ly#OChQnkxj%<*Ais&Py`rd-9OAzc>a3qM$Bx6R+YyQG?;UO^SV zQ!o=s5{sVU4R_u3@d6-y@=x~faoE@g6<(F~VWo^c&mV`I`vxAp99Jb?apK1-ke@!5 z#JSF?Zd~wf>Q83SVimynQB~j@8OR&SGA~amRmCcsV=03QgPBCVbftAz_5t94wz|ol$m2Y1!{=reKZR(Se z=t1y%why;k%4n(v2_R+LC&@wkxEH5>G0T6=8`Y1(UBcx$PuY4+#&N{dDkYcGE`tkf zMlukZ@vUbJHobi&0-G2mlDiI)0pR&mX~);4^SP_$uO^E0W;gSEpH%mJ z^o7?`*Bjs9`U|uZ6HP8&!acw6YLQtE@24|rc@jg@eCe!!eH~5`BoL@Z60EGK%-2Oi z%T@J(jOqXYYr3l;bji)6W|8Fp7Ws4x>$dTaonDf0EX>)pqoX7pgyQ)@)xTKA*I?lk7jT(*;A|(a=+IlGl z*3e${i{Sq_*-OG*!U^gHlb058&hRZ9L!vdVIr4>3j7kgwOMnxzPUgN>iJwjd}(7$4a}hQeTD%LI9~Z(VQB7A!2QBqs3J=J7e_>WK9}SmfCY zrk!glUvx3Cg)di-NKm&<0}ude3$8!sJw@4HQ9T$UKgjEU50nH!l8{%ndfS*#^Gv)hr2QED8S?vLtB0#?rHkBp&Y4sKz`UT zV4Es5N9ti+yX{^IshjU5`|!lbMk|6;)(gMy=Q$5iO-&lbbSOxD+H*H9FX~qw`f6>S zc!+T=kGloME>yiLmmGs%yVZT50yTOmfC`V?l!X~J8sj!s0dmM2I{L}7ZN9x|fdUzb zaIII&j8o&CRN}2tFo@3G%xb@fp!%gj`m#Quuld|u0;h{L_90!%N#z-))f^8gyM;kj z5@x;c$e%{;!pH=r2!yi*yO6j^RA85mpyxS;+#!1$Q`}|FHKeAd68G6iA z!7A?keQ*w|V-q7Zc1#!@0wzu{Pm?-)Ds^)iRgoXmfcj<$JMUW+gw7nb|7C|+7!TZ2 zCU@%^#QT~niLGuKqFTWM6nMIoPa#jJ$F;tKGnQeaL^u|{8}|k5q5szLNrX=5bv9E= z%NX3eJt#0dPqV}P$tniZB-ao@m~UVl-6?p&NcR|ZFc9>iJJm6J49;*6VkYR2N4Aqm zEI)U^Z|I#lZ3t(+J2`I!t=W7ngpW8hD1x@RF=+)*P}lCoLa-*UU=Xm1|Z>xXmgUtk}py17Q{NEK0;&ye=Jzw7dScT8u>sWo60 zQpu_Z!-%~UUYCa13R^1=5wnVq9vSk(;#Hp{2tWg~nVnN!%on%4tr}HQu?1wsrv@l(1b@X`pr)V4Xh!O8f}?f!5V09JwxI~=4|>Hz zT!^DS7Rx%_AzHYY!f|-0m%aWOP|rCTK8tMU6%0ylSL=5yk|WRWumOkX3>!|y(TPXn$hJC zbfZ)T<8ya7;K1fHG?`N~9OQghbEW&)uC0!*W}L>ZNI_|4HRv%mIS7BDKS6sHgUGP6 z=>E@cco_H{D@m?>nPRkMj%W^_&6ioeCJEmRLQH$_8C-?vP zbSn3KKVln6ZB2rF(nxWLtZ*1+(W<3qH{wK4q1jk1Z^^b)ctm4t+T~*i%W^8bwoa1Q zcHg663moNxMVdc5JL0B$cMf-g`d1>%E66~QSB}oDS?65Iq|U?rDQ8xzK6sjpCT`a`ec*FN zME!Mgyvp{TH{<3~Da^HFZ!tX5WAXPIVK#r@#73H?DV~--N{?~XR&o8e}u(?%rFffVL%|e4phki4wP4d-# z_eUb(8AA3IR4iD$Vusj4)}6ayPqJ+*mmn1r51AE>netA%v+;TSc@ z{E4ASdKt}m3m>0L=ul~gP{J5JcwTt<^t~mHfk?EO$;bsBm%bk;*IZ#*N>~6WhY~C< z7No$V_Q^cd^OX=}(>R{XU}?_ga7Opp!rJBR!061e-5<;O!$xA~8Viz@z#df)}E+3v`K)O0}UMIO6vifhh>3_mOiQ7J9#NZVT1Im+X6 z<9JEg(YRMV3Z;NYA9SIW(Z7X=>#y};-NPRnH@7D6ECA3_oQJf8ZB8?%KZb5LuCBv} zbv?XSVMT!g=xoq+QJHEvY1sIivCRHqh_`HXU0z=E{FAD?HLRR(M^n$4dKutBOQ5^L z#$1ryka!xZy2OJGA-S$UYU?SdPo`^k6do}>YgStt&qG3;zhKdn(KsYz2nvXZnvD@M zQhmZwavn%CO2`^HNA|q-#|%7ixKst2K%0+bF_@0uoH71jvfw0 zYypxZ^;DiQ!R?0Le!}TTMmLO5l^#(=-kxPr^RLRC%%Hsj9?(T--$uv2*bvmnZtCZg zH*Or?e{!|g!s{VuQL$M4#gY&>o_yCn9mua_yQkUK1p(FY^GbvY9=;hE+({BGRXTxI zKPRdZC_X#pAjlPmrv7^tLY0u(mg5 zp&YFsy{!izhJ=SZ)pN32hbNsp9gc6RJL^1W5$r4*Pn-B1uf>ewzsR}lT7A2eWPpMd z>i0UNmN&Gn@QCRK=-5AJTS8$a$L4&T0ewu+Gy;N@#4Z}^$$2515;Vrj$Xwjtb?ZLV zxmlssg$UX`fmhzThVkaZ+8GkVahqE51vT7z-(_}%N^77gHE({>$DO>|Yl!>n_hQG$ z7G`>W3Lg2yQ2XO=mA>pz|71gXSRc<+9lEK$X_%vDD?LCCQLfYq)r6A{xLJXd%SyjuT%D=UD=j;OX$~ z`a=+4r^WwI8RcMM9grFhKZ%; zQh9P9-H`OG3LI^+b@nkf-O;N6E*QfmN}l`Yr_MLwSs-b(wGT~7k&1njn-u6-LJUs% z5MCU;>82!~g9O)!>gz-#fQ?7ZcjA`U$4Ig{`*p&lj!3`6i-O4#-_mi)YIUdlxLNtH zth1RD@Ct@yjFg{g-O`iw#A-+Fy7{BkQ3_EnRHotiolM~9Y1$Ik$p_rE0V`%>D$%?o znafO07}Y-!UkS-eFKDX}ms3BztF4(32J2N_O8#LGC!gKEXqetl0_lhj|G;u2pj`a- zB8U8UK4wJ4hn^2KI5NG@*zdc!s4AR^^5s&eh*7*sP{y`EN^_1^y>`9$tt~5Dmn)?* zaAF$T`2}-WaBmcHtYkJ#&tpEazp}4U58?*|VC|Ye;Vx#z0~d?E=}<`9F4T#FP%s4xO7~$%Mfs&r3H~(ZF;wu?X6`)cmi`8fym-5z@f_7yTOI4egQ!z zk?!eKSe$l`sgf_ChhPt9k^pfLQ)Hz*?4BQ!PtJk(zP_0jvL^IjKvEz4*b1V|7AS#U zYY0CIfT+sXFsG->i|(v;vU1))f@%~8Qrsy9^jb>PCC;OmY49DCEcP<|0VeZ`d}a(5 z7cC~+FEql&)Y-KD7LfP~8Ek5dwhrB&z!eoZGG0TPPyfU=fO#mD;=EQD@1_Tz5@%a% zM7O@nR;{N#iDt`lD?u1R@K&P}UB77-(i*@dzWWTvYu~K#-d{w+E7Z68W5VU2S_McB zhp6l8O>QR8nCmf%k)}*Vsy-*ik=tm|WTjr(fD=vA!T=ZY?F<@=eH93A|1>{htG~E7 zrf>JPK8Dhw#aIsBB)ps~7(W3ZZosw&CnEE{b}8fq(>?#gJFo%2nRjSnBJm81&IoN{ zOpmAhS>Rl}Gt;30Y&nn9#!8RJ8=%vJcn-th{5m!@T{gs9wYaH(uB$kMNZFPH91$JO z1|eaAmm_@Bls($|)T=5FUK>)|>`_*CUvrmq00CqBxppgXA18(~MnAL1CG#2f4^lOm z_+snq4yY55ARE&8!vl9(cq8GM8AR`Ke7Gh1o7-43oQ#Wb{VAIdaBD}pS+amH?J~Ec zXMqf8*rlO1JU4hiZqV%7d}ENzd@ME6{LO|gREs{ zX31@K@==zR)#BIeg@f7qFR(Yn98Xi(GQGt;#$dt06kFCmJal_?6Z2Ah8{H`{~<+eL4|V8=&x+ zg>ucgWqAC%q}+kf$o=N;go_;LLTCo=5gX%>cx6oNq?If*cRp zC1!o=dYf{jUd;yL%-*dvNzx<3N!nhNa0Z~U?4bkB-JUYNP2=rcGo)zUDz6k3qe|FG zG$#!@wEz!}bE8r{CsnSiqcjf3%K+KP2Y^*R(wa!r<|Rr5iExe_9mnW`YzSYx`!1;d zCg_2G_?;VLS}%i%?5F{_k(lF{(3>#gfCI?fV>^|nGlmc!8n5W)*np^M)Cu+0nPy4j zUhsX`q{65H?hA6w)~hrN=J9-anx(?KSHA;*Pfa>oZ;WR*&-Oyc*Iel|Il-yMswCd~ zxaEDib~k-s?|gKvWM%|K^*qYDzx+i#F;yhalcnkBa-9c%E5$rge?S~vxX#4VI|w7= zp27@(5WzVTK#+zht*zb@Zp@CdW7A1K77Px03{7FfSX7OounhMLBI?voVCL9-t8ER* z<&QTBiPo2kiEf&%Eymbe#>!)4X}bS?qjNw zLoM5UAw1cTKenquTi56*B*zmIX=*jJ>yu{apJTcos%gmtl1~ms($tGYH}^ix92IY` zzOD25?tG$L128-1=pY}O*FRb%{x9!c;c95TiLbvQ}iNhKqstuG!QSfLR$RJOUCm7Fb)z3rHIGMV; zY{ug9>k+p>v ziHS6>E3tByE$?c8)>oiC@I-)sG1%Sww~ zTQXjRlC(kW`XjrgRT&quz;br-PoM?&S1$f!vI}&ced>90Yy9FHzX}D@K|vUZqvT(L zAfOANo4r5!$l6^f@&AFIxq0(xiyvNkhlILSO^x*+~XWT zB4`i73-%1-08{{+rKp!u05M{kY-bleA6`U8JFKheiRBtNgOlmIk{(bBB?E7&-syJ7 z7@$H~>&;2H21x$}5>H2Xv=e$Y-~WGg}l`ProA&RSK|g~zZocx1bSXASy2LWhw`rBbvi z@uI5>I;aA=I!lcgq%T^4ff8rk4-V268MZ4|QV+0hv54c(^h0AI#9B6?s#))-W*mKf zeisq~Z+t?Skm`;JV1a)L8rT@2Ob1H9zofbtc;uerOQiRW@4T;p7dRW!+?=kMxC6sm z{y(4z1-^``G-h`A%wOJum^%{+6?fn+(eVckgNTcXuvt|r1jRB7f87RbeUxF)m&)mg z5@zbwGy9|9#_2Fy8SkI~1b<&u?B}fJ@Z$CSFDgz*i|R=8#)J25be)ya{7HL4R@-$? zWUrjEY#j@@#e>UK!>MX7=v34hmd~W^;aH!sCa6xapd0iaKLYt5D8FS`Ig?Vj>EGaK z8WS9VWF`3Sh;v2a5(?QnX%PSM$oADQ*ZqsU+e+(LtzNP&JOVGUr(hC-IUd7=HS*21wa~EVrzhBqtjw6{ zPo3X+lwXP><5J1-7B5jinRo~864O0?Bys8Hug>_jfXjX!KucERM>R0C{DW$%+P-%j z$}4J;pwVpJlU+zqD{N`&a03vc0JI2X00+<=69mq;+%}A%cD9_zvUVGH-^mH+X-~0J6p;Mxq11CgF*FmR>-OR*7EM3>R%8Gk+84Bd z161OU^HY-A74<&FOe8~@E3Tvj^9KW}H%LVPzuv{j&?v=^_Z!)?D5&A1`q^~6s0Frh z53L3(c?gY9E|6av?x#vz##~Ug_1j>vkdrAjT(o4?SgmN;#9QJ+%Rzi-RxW!o>R34N zG?Jn42R!N@IqMHiVy|>4dfd$#uKwlOEC%a=98FW269P%u;D!{cr9y{#NBq2&TNYA{ z6B$GqL2syUapDGt>4^j31QD*7vY4J>f#A>>5jLj~Y&WOADVMWS!hYioV*{`-7bFG~$+S3XAe%nZfF{RXtWE zI;3G+y3VjUVVQK#tM9{VkzSK4p^B_77E*HFuHxhVfu*=a!j@}q-`#NGwv)8(@j3?& zS@NMw4Pg4%|7Fq19$lP@a7HYE63)AJ2HP)S{Q~t-Izb9*O>cBehnXbO5+$2y8MvzT zwRt!G3i2RPA+%SjqSG?A&ypHq_`G$M(ODh>lhjNSQ9m<0_XVgil1+z8jhGsHMJmmb zh@1F3h${>afoDJ)y33x)orBzH21^3N7}XooGpBM^1=Q`h|Cpdjk$;N^B>(PDp=`^p zXx=#Ky=8n|57Lj*D+CE`1vrj<2b(hiQ)Q7OR?72_b8v{iBJ-=jnG$0CW#Ux3*I6zZ zfCL8#i*Bx(I30sq&@NTN+H?gfHtT_cz=g@7(CukyAh>S`2AK0+(#_ z%L%|?yae4SnO&SKnnKRiZzvxTVMwi{?kD8Mz!iCr!ABLqK`3B$NbpJC9QFgNGIAD} zEm&YFbr!|`#ST%BB7$z&}dl!HH$5EI3MtS(sk^yoe!t?;;; zh7Y8?|FStJ>6Sb6CC>TuEzJUub@kI-${zrL0IpQ5>b(DjAYrVYM+!K9!`Y)aGVBrR z6J4C26j|#E*Ss^SAM1M_#O3Z~n$dfBCP24)85@gPqvS!$g{}lumv!f*oxuy3O34An zhSwKK80Vpup@ys6cSa?Y7O{kD7b%v0 zV||yC4^deXb90Joitn)bG@#+x&i0cmUE0LxY6$Y2y>u9DJh8udgreuuc$g76?PHHu zww*)7X1ze=9TQ-({fnL9O{O_LCi?ng)a&hW0UqH>fdQ9S(YC{%M5nay(sl)_sOSRX zMs9nj*+Efd~C}LVs5yQftjnL4Es2~4m1QGwy+vy82W7DQ!gx0 z4^A|k`br_;8vOtM4Qo#K(812Y2>!wT#IAbVH!lER#)|se>wcxWdaF-`LX{?vAlN1y zN|4#WJJrhU2@hH7I=8dbzdH|!8uX`WOQF6~h2(oliF!r1)Aab7P7Il;RpaXBp9ku0?3+urulfAX*nbHt`CfcysNKw z#HV58etA9J{<9t_F-i^NnJ0-GDg5BHHM<0lVlM>s_bwW zfoLY4$`y{sWD>XA-(U9pQ4(iATgtL%?k}uyLiYGSl6;*>;lF+_!wq%6$1$UM^WfOh zs48Vgz&F+l@b3ZF>QN6YO8(28c=gK$k?z0m!R+S;t$?s*c<9Ctd#ALMICe$l1o;+F znyz&lk*fz2$w(d%jk;r|I9*G9%MR&}hI0kvZXZByIxgGXU|z-@R8y(0#6(TU5r}pOTC0nH(X42WO=kh4G@gQZrOl{=>1_+u!Zu6A z1-*2+`&rCgW}{>E&B2+27%3%SnNlWVeP+Z7qnGCHH5{XZuNv+v|zTAX5c0;H#qlH{A5@L2SccOm5)ZX z=Oi7&@mLb<8y!)>AJ`+FPCd*3>{g1EE}HYGY1VX^bNF9asXnt-Kw4fv8A7)<9Qob#V8G9J{ovXazZyWuC==O@Lb}3Tx?fN=c3z zO-@psw&jE;evFFsaM#QY1bWk2ArvF5Pz_x7Fh{@2y@n z>Yr{xQdUb$tb>jVksu?({c3;!469-JZo7-j`ULt5398tl7sIjH%n?W%HNXQT1xOuR zMSW8cMdXDg0xCy0{mL9;fK4hEgbeg(%8Q`}3{~s%`dM0knCj6uh-QqGBXpsUyh-6_ zQN?k%aGX04_WxCx=LeoXSZd}x3?CBz=u;@bKhF`o9;V+in_zAEZVZuTwM18i(u=wLc}*@rImJ>In!MCml{3w|_qUcm!}!!(o!S zl2#B_{Y*(sDO5z{QSO&4Gq>J10{y-ajM-QUDkDU>pWva$TPaYTGmfD}&CYj~2bMmc zt3U>P_nWlr-7@~VRMBSlL>F!UrXBKqv2iI}j^5CJ}t7tXpd&zXt) zv>w? z7+NfG^gSVjVgZ^)b2e)q3fO)$8(XW_Kn#`CZe(R2i6DX$a4_rIhdn|5I2*4CGlQ^K z^KsFsMkF00uFe`@TW?kS0J%BS$!)L5vTu2uItMZD9#0B`%m;Q7clpqR9<4t-!zwBl z=>zN&Ah@nRNBPv1LzXWVLyC9pM~}bU0!f81N`HD?faqV|hD>|=Y}3T#&>+3jU|xzB z167DUtmT(CtFQYyF-N>>gIQvnA?Nn=f+x-aj*LPsT}ly@b;Qlu?vEs*;CvRoPM}x6 zi7eCa&Mh7#0Zr>$nvL$$vk4SW)StSUAShVNl5PEs49Rd;gh&E~)`C(6?^A(3x~h&! zeAFTxry;Q)It$fY!E0nNApYd1J#sa?z{Oh!-stp+^*kCLDOMH?@_?HI>8@=68YyC+ z7))46HxQrN?7jpvY~`p?!VelA>-ba{u-P|0S>e4%6s*}lrAiv-bE&R?WWM%oH3my> zMvh96;a;tO^Pn)n#9o~?%E$w|I>9)RBOf>XM1gEK8X5TC^RP77py=jtwfytPy;9NhwnV5q})EC$<6 zFNg;Sl(F9J^Z}iO*}^HkKY2cY_*Z_+E|eUL2{9aP)2W`ZmBQun^j{5?7O}Vg zeD}k2jgRtv!uM~_w>H5@o=LH+FH!GLQDtj!)9)&C0AzPm2mWq=aRbNLP`{l={CF^q zwI9dGV<@{(fDTNCR{Z=1BoG|yQMJZ9^O_H=?Xh7^ZB5 zk-Q|7e;9;#AXo%4%IV8QJgKC5JocC06!A*X;Y@v zs9Jk~DAX8>r&Pd65F)<7O^o+vffxjgWyK)~&PkF!N%&=vYe=x7;dO8v*;_i={Jnf`=dwNjmMBl%4YuJYU1mY|kFCTVF>wZvX0IzRH`j^=OH z$;uSe@a|+EdJV}C|7kFMH!=WaCZmNB2uMMX_@^7*P`=MVztt>|BXclcSL?loi3NQl zQ*Rn{z%Vc{FfcGMFfj>CuyOyAke9R7F@dJCnX5yzAOH&h=geM|sJ{U% z=QfQtGKZH5{oBC#N7KDJdkfW zu5Rk(q$BeW5;nDOe-et(eoSf6wch%kRay*6ga93qlZ&|sjN>|f}-5#@-6!~>z+ z&Nt$y=bDr(dL$*EFk2gd67>4I2oGI(f`8h5W7es$9UM(}xUG*lt%xZ=mHNw$Ib^+m z4$3ArTx7Uyp!MC$-?LI@_O3ZM-T5eFg&uD{mvaH)*EeX|eKG>ubn2;uQ_xCV27RtB z#Efx@YzAOr_ciRugtRD`mE^(EnD3YJ;6~X(aNr(n0vBWaeYm2IxDNLleCOOuGXXCO zgEjn{<_~b1Fe{*t~(yLB-zJrZ7 z)WNEp+7(sA2s?B=MHb?K$FLJf69O0I z#iOoM51JoT!g>wfcI3=OzS3<+jT}j59QF5bfD0n_KQJ%aaudLKhe%+w4O z2oK}6bHIqbrh0at_8yt0P~?)*m7Llw3lFd3@|8{(R{tu z(?cXySFdP?cz-PMeGAup&MR|ueXWrJl*Gu2E z8$#Y=99Im0vn3rdW((A4q0CeGRuUP~lpWDN(^ZUw-=SFB>0?b96x4)kXn5_;EkIo3 zgLR0i0u+t@hF@gz%c{S9PB<@Pz-Ke1JBGx_7p>@rB~``Dp|Jy-l4710WDGRTdpVPw zuwk zRan~q=YI6z^yKSyW2Zcn$NZv0I_&4R!#O03B@^X6KLNVclSD08=MN*{m)+)Av&IzV(HlgU#%6N>Fj|mNay|OXI*y?!3R&{^hbt|B6<%(@^ zVs&!*cG>(~Q{?#8SH0GKpi5tpNOi=ZpY*|_pEj9R^Rv^@C)YADt{Y;X9EBtAz1jf5 zSnPk&Onwv?q(wzF7v~$^6?u01SeHazLr#CnUV#)lV(6@C0iAYH7JbtbiRj>u@Rwu(91(bP4&1YpVZO z5nL2UnhDPDU!cKHZaT34celJZ#)f80l`(-VzoGDV$`!Y2(k$yp_d9GuR-wDY>mLs% z%wW3jkS`qnO(BDhg7e><7=}a>fYgSQC3Hr!K8&8U)|E|J1QoeQn4j+ukI)Q%1?!J= zYk_FGB(%GnPS7wl1(kw-Ddk+g^j*J(IaG5YooUy_U?(VO@s} zo?3v)O#j{Mp2_WT3;U8T?O#PlCmjtV%<|OF|A;96aba|iy}doCW2`E)!!p&OD-bT? zF^K}ib+Zag2h&j3ygL-vF|?Rd*L9JVRH6WO ziAjUDvd{3%o^|f;m@Ua*K-*EMu}fsnz?_#1e|@W{~8SWrVa1HexIAVEJ>)CVP}~ z=iC8JY3t#nNd8|bS7UTF|9B+seKcSVXdN>zc#W4D7u)Otwgm>g>+Qw!2=Zu80HwIu zY%g(=G3nddg$_+>9$dz3LZdljW~I6uu_BKcqMMkpva_1I3F0mzVZ!jQ=8n~qv&v6= z3nSyg#BC9EFd{r`%F`Fjj~83W{D7JDMD`MlAMRzB4%ZWm744_UggVNaxIh2{n{W=wKoRAHfl$%r)s9W4P5(<6 z92v83Yv)8*jf37Uj*82rQF5c@<2(H@yZbsUZ&olD8n8e7lST_UcfJC|nAcb)79(Me ziqS9pkNUlryxT?uwPy!`&-P?Q_bS%!``^Lc!TvHYd2kM6`!xp zLZEs_pxib^YJsDU03bLVOJ7Ohf=;y8*mA%V#GCD3}vlt+VTc%pWlQ#I^58ue zx~PeG&hU}4uz(tE8FHR}dNeu4OL@LU4y7RfeHXc7coARtY|P(LTkHOGiI<^zK0;Lp zEMHv^wWE-aDZz(?ThPEGO?Q8TlEI}EtGlpaC6c0@Ud7NWW1p)ny<36E@ z8n8lPam-${mM8O_xoF)#IKmOnmmC?*_%(!Y?M3bW(HnD;gnX3_2Fe$F+FIXOhU-5+c#$S(L9dXQhtc?du*V;Z^Ys*ILXYJ!O z_KUU0O1Wp1;;U}@r-B+W49|s}pW^NtcmIs{NVK8U~QioYT!d22@YZ( zCR>kR1G!y`yd-vB|51B*@PBPP(nMxT<2LtgNwi?+!sc=(@DmqvR{gRE+rm2`rVBqk zdYu!UcA&~@jwqmgmCjlHnBF|EpPpPaw-djcHKW(5L`FD9q2K~{&P%O}{4pm4L~*Y^ zLExCu0@%d?KTFhq@-HnWo;7X@Pklb_MY||}pKy4RUm(teOAivJK#8}-McA%nmwIiS zq`|v~(hKzH2I(#rbPjoqJfJAD0#j|0d`?Z~^jQ>szFxgG;rqzAGAJvV>-nd8s$JdR z=GTNgl7CVF+OqUVd^U@}sv~CZtYXsm01-4GM}BAY#JULO88YL5MmgcO88ZutNIQ#bZoL(3ujG_LXpVfS1)d_6P~$+#u*uhz z4gnK|l=`rv2A4T`m|{tLu+Zg_0AuXj6OA1d34OgTX}{F2H@ZSO8Ek&-9Shn?3Wsre3=<`=qROgPQdy zr{_!>%Tt^kiss4=g8Zd=dMbo`4lN^_LV-SUlGdIQR9HXY~*F0N+b-GB4dpX}*@wA6KxDd8% zMI>@~KDiM4!yK;S#aQC&Nnipdea$P?TR7G^(Qg~(o~M2ekApYN3TWk)R0X;Kffa83 zH8cPZ-+SF||MEni|H9ZryCB@u*q<9|Yer4XFL+Y8`?Yt{-})oS7hG0EnleL=3b_+2 z+E%Cx0B1-Sn;2WUGaN3@^=uGYVR`F@<(EY6NA?~@c;hA$j@XOH72uj)Y9KSG(8cog zvC;_PZ;(&UEx%!eyvm>HU3cqR@u`5per*aRfS9~=(S z2vCDeCFWeDR@mRMeM-r{O8FD>7kS~50m1DR$EIYeZ)tt&pd60<(imr^C9pCT$I}VV zlOAc5=vK^306FXF`3z@`2IPikdlRB6&ShQxEc2wyu^u{gGcXGVO%Dioyw!(~2C)>4 z7F+yELX!q}F$T<1BH=o1PB2B?n(REbH}@|S(%zt;=6HNjlV)*1(?qvCb5oC9_#?!) zHXb57E?<+}D}j>2gR+3ikBsimxNO+=s7$9IIm+EcBCHPXFsIeUNq)8(jz>F*c9Zf( zMXw_$ATkaJ>7?uf4c|2&uEMa;ov0u}e{(Ze%bbmT6 tXk6U;RffS8fBfOzkyQt6>pyg{SX@6dasvJASFcWk;ws+rF?a=IQ#Rk*E$#c4Qg8EPs5f40hUQo#5w+FkV0GK?YVqPF zG90RxoH>g^lXCgDrTW|-C7%G3)fStylAoT=V{kzbR3@OPO);fILmkdJ1aI*K6Uhb} z?UI}WFL=md0RtgcM$FSGCd(FY_r(9Zn~#;BBmEx?2n&X(2UhK+AWI~WPywP^2q3KTUif&D08z%%q)>n` z!kW7tsAAWfV1^Q+Swb4#E3S63yTpxe`xP+I{ZSc@3Er{J<&f_6?8n^W#nN^28D1QE zSa94NBKw`Td9KV$j z*LOnqyW$^P42rP@{beS%8{IL2<1e3WJ^Oz)gI&lM~cNvt@bqyU5aH?tItGc5`*74*~g~Qz!c|==ibL*szOfJ#?0 z8XP)T=%ii!@OEWYHfe@Tb^Z=(S0!WR#@6w)VHp3>)%M;k2QA5>#JhO|1F%|I9~e|> zv>B_decgq2Dp|9ZJYLiz97VSxPOz=KZEavPeD$;uH#e9;!_W>(+cfi5)e@aQMpZ#U z$>HQ~t=Ii!h4NETt#?LuQMY;`-dN``tRWteFt=@|P>t-9QXpkx(>-t`$OK@rffpT` zOAr=jx`j)w9hH6pTOXDl-LNj=8E+~Wu9pv!1&35mL!{odFY5)qtno|!!iZ;L3M|Py zyuv14F&164SyuB#f}07wny>L4aZz@HTjk4ISskw1=Susl*1vX~qM@)$8YKhINfp_W z4YhF036n2f{K8sC1Rw)-bD@pv1Q0Y~I5<4axIq2$l;_>v%5NhtkqjA;>szBxUhIPPXU6iQgX9g5;VCObgp4%1?`iMx@A0ad%RZ< zb*Ab{hV}6koGj(5ag1~I>`21Sk!<9+7;PE}+!Nwrep$KK9 z)#Tw(NrmMwlQ)7}5xG>G0xeM1k=TQR@&(|BM1tY1P(BLFQy2z0I|j3}Rsdrqc4;N- zu@x$VZG;EYrEY*rDHflL4^wxpYS6EpEQ9LTk` zCP%T-?0SO1gPt3SM$A_KSbTgtm7|uUw(p3gql;>4*%@@P{`SKdY>|u}EJ1%mEecK1 zO87xip4G%7gD#*DO!9i>A&cTQ{oS;t2?@112Ena>lo556J6UBP0<<5-{fV|H;nPog z^Ph9Z50uM9dAzzG==U{7bGHkVGtZn#7YTdK{a@!U5Sc^POnv4(DSI&tr^8~V@S6lb zWv!k9;7Hs!ey3`7}16ttJxeH`i=Bf@Je$CoIVK6Jf24;)bmp&?eefeMa z4N%1N-EA6bPh-W%WH+Wei(v3)KX;S}g!wk?q->1g z%&x6OA5ZH4s`5Iaw`E_UOH1z6c0GP!lh~#@ie01|E$IQGr5-Kg@qWh{{%jzw~Y>MzztIsJ?zjh|B;M;B~gk>me$ z!|}&0fag1k?W`fx7ma8a34M9aO12|5CpomHr(4N1E~cV=i}%axL;~VOAuLap<%GSs z81Lu^?<>tE?@Qz2!`)ahOeHIUfQxj|R64cAf5be;tq?ekq`xev=(1H#kIi7Q{O`H% z_can8=Eb6(=evMGDL-jeg|F3o2rh!I9Z-ZQUYpnH`y22o=PuSXnz>FC8kt$&RC_22ln2hNxM#3eI0qB?<5gnFjhP>0MIgr)w!gNty zgDahXY#kFy~O+fIflq;L&*5mA?)`-n?hWi~vZ?(whs-dRvw!v*<;K5j-7aigC!IR!XMhH(CrbWX>7bUw0zsu&kGqim$7R% z*>NzQ1B+I1{!^Q7Z*EVfQxUMvwB~Vn?k{m9I#%#4iv5$saQ~B)a$*Jz4%Gu4=VaGR zdiN91?Opml&EXQXlc*q5jq4iF4tU(lNPxx7F_dNu1RKbryaLTbTB=mHs;mCMI{AH3~XgC8oz3Y=low0o z40uhtsaq4@ktwdHtQ9F#i>372?a(eOcyy=Hr!@imyXAbZ=I8XiYK%=yVJYv$FO{uV z^G|GoTjtTe05GdzsN-IzKCV<<0VM2%Knx>| zBJ{uHsU7S|#KrqZ7#U(>1YZAMlx-yy8L=0TL$|%M8$RQ1&G4*d)5>w8HunE%#`vv`? z2L0lft(+2C&C7L76uQ0Zi=8wV41q_tqPK>m8qqdB@R;0^tIO$FIO$x%nH8?%cjEItg%Cpuhq*-oPj?mF$x;ur z{+>|*3yue+=XzaD<*&9R%l)URg+1CrD1ryG3h>cLc;~ZS!23=x)ExmI-fnE3hCxma%=8g$~lbO4)d}%lER{rz4o> zyDbYauEv90A06;W&_{`k7V)n#=tgj`W;pn&(Q0q7KPhtJVD_`}6Q!SsO`WYs)K;v9 zF>9CG@%b*gT4$3{d&cRUsuoe?rx@JtCAQNY)v=>!fE-AJu#)iS8$DN{GFafb1@LNG zh&8Ebu&_DRxJz0@O2ix0!jQoh&G^bc0v)|p*29-FWSrW+%y1>$;_zCZJ+@bhTv4ek zGR)HG-bb;ZR#XGRVFXM`JXToT6a_#6zztG`b8wNDN#hp&CWRF?%WA{~S2hyjm( ziiCNGcGd9csegofT_USpbP)_n`l_w*d1_K%2U{22I)Sh&3`y(aNBQbzu4Y~jT&7ncS*qt|c1b-$%;=N`q3Ci^GrH^}_$#BO*2CcBThw;a5B*RM) z_MCC?>|KNMq}018^VWD_3mo#*o|i99UCzzFnY)@5#cjaLi2t2DtAi?WDYlyHsX+q; zAD5DDt3#mx$;@_Jj20f|-x|Jru%qf6W_JP%!j`|=xUL|(u<7(IdVFUlFR(wd8I z&iTsqqW+Yt6;|3#E%`MVSN3U~k^CRPJf~d#T2t-RBK^{?ebxi6fvv#+x-$OhwF``? z%p~HYI|8kVQ5*h_wcf#Oeo`%SzR+Anph%t!N6kcN~3EdzVH__}AXezl1d z^eRCl#Bu~R{Xw4jajkW#z>NckYV#eYws4D{ow}8nJ9}K0UINmUMF^MFC7+vgcB`Pf zwqjHBcAiLU(y4DoCA65vukFg1uN!eP-$B(}yjQ${lhsl&_c^s2NR9}8weBz%{nfIw zR?LQDjXiA782g^t23Fz1EhDI~X@`89a`U2dNG?|T(Nm=mF)&p|8R*2nl&@0;V#dT! zs5MZWJP6;EbOXh}5T%Zh&RO{gvU<}HkKNbLNn3sod*^)6+dXEOztjuO-Al6>@Tcm4 zy1`+j&-LHVGTsGl%}@jipOWzKb_Z>hc{PSMWm4l}skg&=W3CYctP1s4zM6Mr z8ZLRzE8mA;EKXWba`k0{3@*~XfFUOC-Bv4{2Bg!v4ZeC)9tONrYriG(mb1&A_L!Z= z_*F^Zb4$_icP3^Vw@%@@_4nG;sjhI9Q(sN!*r=dUNwt^jbwgmHS}mF8k+2>EYP%2C zdJg&slgLFUaaiqVeGQreZxJ!14z{!2tFHTfZFmVPzgk*Ta(ew}EzBy;YAek8%RjO8SEQp?Ayon7B<@0`BZjq0XJOh2HE+6d zNRr|_`9Md~_YnyH85iDEM>xGq+BeH?C4%!}7ni~u6#r+mVzm)Px(+20x<7)KZDo5r zJ*yYe#qgjI)D1~2o5%%96L6a#lFP>_2)5m>?a^F6?+E|tkgAC>QiSsT2D^`&-==){ zqFl)c>1LN>!JON925g%B+IG`Q8sw6B&n}&SyC8xk%p(*vl4}s$zQ;QXD}|&`+U_oW zCM3aO(&#cBuRR0In{tc;cx+O6M~A)mgmdeGLpa}^&O8Oo%*Fzvc4-u`Tn3>EYQmmx zcTAAem{u84WK*t4R~`UV%22qeN2dbf2SV}j333Km(+?Zm>@U&4tZiY#5=g-Y>!}4%GvlKU?F9vJ{prKJ(0d1puIvAGf zmLL=B75I-cUh|IAS4LmySr^856isX9wch(qsiFU0h~bnN#Vnce(AmQYx%s zxdQ#&hrQ+x2u?`^^%+iLfv=sWwq9mZ$L|Rwt(c_|X!E)`#0=>OQAZ>0{V-Ul@;VSX z8CX%ryxZoDnWD0I+0MQIK4gm4z$j5%iu(<<7{F)y8PO2K51%T_qasdD)`YezN4Eo` zo-d}Gq{6D%Hld@Ay-;JD34SGG8v2kL>kt1p@Lzy_jEKS=!t!4Fmbd6?B9AgIiA&9`OGG8J^Bh>2?lR0Iiff%>cOih+=@E2JKZJgn2Y1?q zS%s6;M50!Jmvyq1-EX|h=T`kU3RJ<|wQ;G9%P70Fqbk<7gXB?<*MX6>NV@32!l~3I zRN(|_vtAvk_ru|{q1eRT;-r1LnQer}om`Jtw9&;Q90UFP@K#Q-^em}v0qtbt`|TL- z6qD#e@PdF1_9|-#+Z?WpWuLS|31X#=B?}^5jjIs;P!P#z^Q~j5U9Lrxr^(aic_U+N zBiPWb8}E&MT3Mybelb24IVM$JhI=4|hmZxzg(_KRNA%XV|NAI2lYzn@TQP}5yCekf z$REXct72iqeL)jK%5jQWifE6X9EOuTB>StX5JbB+TwVphBl`Bel@mbIkT5-oI|5{J zKlr!$70IjHc7%ICN;z3AAIg33WXEz|nA?HR^fZd+8-TS+b?vOxTZXb_^hZTyK14Tt zlZ-aZ#Zx}1kSPq`gKZwIXkO!_Zl!FlQ<$CCN&?BDEXa4!U;!SVp!>k%BG4X!d(t4o z*}n$WS7L>Db4_ZqI9@JFJ06Y9T?2zywpIIW39&g(?sfDEjiwQKmyWkfNSv-Jh|MV& zBe-4Uea_!zJAxl~c*vaMT>-F5k15brq31I>g4KFDqR#sNgEZDNmeMUGZI_c+V(unajy z36`h*n!-M*lDeIdi+f+xk6(w9$6Yc+7`Q2(br=<#+&d=&(i#z*mRB|j}{*Q z@OJmHs=sFIrI6MF{^k_Y#if<*Wye}U5*NgcEnC=wl4dFB6?Vi)T;oJekl{#;y?)}T z{4PASNVG2(h2vCpXv~4^Vni(K_wf`u9g*8I99Rc1ERPhP$!}sprE|2Pe?D12VI=aI zfMoeknn;`Uk)<6vV;&0p=tJXDf}9227v~20naH_gt2RU+_g>o;F0<~jZqzhuIHoreB@zwj_@F! zJ6=FHYQa_wxUluJtYEtj#a%5wY=`1CJB|3dY!J6%qD%rgU7;HWoQODlp@r5i2}jz3eZbl=-+Lw+@ zO9_6zi*N^ykUwt&q-$awTtETwlGqo@S=FG_y$@|xq*dp@$DcJqh8nc zxKLzYIWR6`=DO{2gHznAOICQabJLvHELIc>cVIjYhv-*P{M(N@CE%_?E^(TaH-jFf zWSAj@c1P(!g^U`dXKetFBDTBbt@$CQD`Z(3O;M;#s)j4A5WzOD2a!yZW(>Up8*@;IBI);BR5SCahzV<3D4W~b#z?4!*%feF}W zyaIcD%8F31zp4i6f{cGDb6W}Gt>S4Y1~j1W+T>$kxUQR06YY~hm%$!P7hX23uU-$a zpQnkRXMuUr+>BV89!#s8bS~6QcmH)wxi8-GqOc7E4#_seh@+X_y+)C+;F6p*m*G0N zJ+JCF!*e>iqmd|p?g9?(c#(@yY}0mEB?9h5V4rH|kaOnu_6b3{nL1d_3Us~M`b#f> zzYy+Xh^-1&o+5y^dN7Xdr?$UG%=)O(xsT`>b%8r zc3Q=Z)9(%PR6w6syv*IaO!vIBM)SnQG?M6^48a)%U#-_MJ0eJKkaz+9;Jj66+bIm= z0m=PkGoRc-6ZDp{0=X>pv}Ugj1B81n70d5Yt5NRMx=%4eyl6v7KL@v{v=6>TXPXY- zEtV3#Hc*sTR`d^4C*gHk_511IV2~fLmCbB4O`*SWg`loI>w01E zOpEp&e9eOmLtBQXIj-_xpV?ar{9<1wN7=QKbszk?|Ms80_C84-BeL~AkDM(kUhr+F zm5d1qfQuQ2c627mvPb*G`)MnW(9IAEgs^bC2zLy-+qxN~OHMDI7aC9}!3cx4RA`uM z&4rG(Q)iI*KA94$c50Zr;hsGF4XR6q92AePL-$p#-F83>QUiX|x8lG*a-_;(*`J$4 zu7SqI9*$?gBhE6JcBtI=xdC)AeK{-tK@`bO}Zr?~Rg3 zpTBSF--thm2Y*l93h4n{k8nl`Q7Jrb;EG2zx~Vd z=kL4lq17Tm&rjx0#MjQT)eXUA&*1Ctvz?coV;|LT#h2Izf~}vPpUoeVhl{t^=iI}e zA>TPqg1e0m`P*N+K7D><-=^Q@AGxnRb~8;slHX4E1Vj3UKl0xlpEuVtH$Oi=b^1!( zA>S(>J#YHoJ;&Y1{5+pOuQLw>??3(C2=5#(v5y4%zi_{Mf5rL9{pf!UyexbY{4?yw z(r@4={cZ0w{n_t1_vvTR*WAbdhxEJt`{tA2&2CM%UL>I`+HzDZgoaLHUXOvhIGD zaMe^bAg}}ArKoB^Z1h5yCaVEG-~RtKNWQ{f+|cDXJK{s#Os4G9;{X0278Ca?H%xPa z;tOzuM_4%2L#>2Ku8LKL7Mg0gE*X(cdyd}Pk9}gWfK|z;v_=DOd5OuczjsafDOaG8 z=R~FO0!FS#p4kL|vsU)SxOj&So#0rLP;4G#bCY!Z|2&XM8Fm$+)gOL>x(eiK=h+`& zM815yqk51jb+YSd;B$@X6!8U*7nXWE5LXW2`e7{ATJd>Se(zAWPScMw*skrJYF`$q zDhe#N3f$wzs*aV2iafA5ty1#ZX>M|U13Zu;Y+LsX6a&n~!8U5lZvY-8k+JnD)IdyKDMczQtl zqb+{dArqE_B#o^YIpS9FL@r;Z^23M;VuAJ|6m-lceAPKqKHUI3WWm+=N2`oS2R;Wx z)ptiDHEU0I{Cx?y%)uA#(f}9hkKKbNj_PccZFZ;M|Fr-zw*TDP+19;asH0}BmEDSF z*qj;PHXt5bF-3T;LJwRN*{kP*D<<^gL8F>wV8&%9IxS(YuH11hYcP}6kEJ`_0v8P! z>0{FV|B)nqjvpCTDx$#UKUOSAN9?qAk^8oCGOi7warXz<;0QqX&hHductKo#HU>2t zSsH)*{fo7_V52`N$w-*R8YZA$q&fVrx;E&iC#Z)6|IfU`$!H+SaOdxQZEJfj!OpE1 z0vl6`|A^O1%{?Q1IWqpnyl#?qjm)BS=K;zfGHowLW1RB;a0gsARP^$119)gF>i&D@ z>;gABHy04~x>PL_G}oCmep^`m`IGGB7O0jn>=e-lH)eNMa>2;R@uULy4@5{V2WbhQ zfO3LP)dDx`{g1$ZzR&-L3TG?AFg=LI#D?~@hp6%GM(`I|Xe}`FBkVv+HL_4hwef>K z^$U=44YOYF$U-H4Cf`;Ti6I^2h9HU(L&PjDmRKoEvWw`Qz*pn)Q%5Z{2LB!BuoeNW zTz0Ylp5h;e;RQzyS|9l9E3XQv7*AsuhTE?cpFCr$Zw<%Gq59xy$M-k)>Zkx z6wlUnJ7)3EYKvu>?+793EC?HC)_1ScYPs_L{>{N%kcRO}z=zvcZe8X*R-1Qw26l){ zS+!^WKZzQF)0_4`qwYYpyz{X#C7zwwzt17ds@UM=Z$P- zJ)(-I^KATMAyL#%`C%7+e?iU`th9;$dp00nQZ-qHaDvIO=nuCV11T5!20Df4;DU{; zm2Q3Sv1I&4>|Dk!gGh`oBXU$^Cs99WWB_04*4sip=FlDT=Vwv)q+V`{Dl#`udsALn zv;$Y!G+mrtWi^EXHdHAj>6B`HD1oz^G;p$gWfM7;!?I7A_q_;Dn>4wkxk(eLL+Ai5 z!k5ip?q766BRruIj$vh_w<9$Z0D5hs|BXb34M4*4S7DyjeBcjuHrh!?s3Z4>5b24~ zMn;NyalCxU`2`HN{3-BH*BTCm>{J&zuuHcMhWy7Njy=#bBWOGTeN{eClBDE?8a5Gp zC_LkAj%R0H1J$7#4M?BsR2>zBY(vzD`nA^TLJ_hwlR^CVcY=6;0)I=A{V~6D(Y!iG z+^kQ#i)^Om$0byEJ`}wP?zXf**|cvORw5E)5&hEx5+PxSg{F)Na*l`_g5cE(FwTaJ z(|5M_wB&;6EO>UABPdp~7dSMJRQj3t+Q@K@2j*T|DU_@1o>^rs2v^toraKQj)i>Cd zYc)!Vk=4Lg_CtR9v5tbaxovQ~nwiREq<*nmsNAz$y=y`miN@byNo4|G!iJ#j3yc3{^@#nck68Ukv}oz?u@;UaX;M^J8Q>SWdXJP%CCxzzh3(dgWWLTj(!p zrPi767EDw}X_~@&%vChbI3NTT&Y&Sav5_F@**7~n;84i|%Uh_rlLv7FJ+p4Orsgb_ z-J`$k6(QXdZ>Nb-tTMUvofd&$(1UL0hZ*GASziG{8T=-iqJ4CGy@vMpE@>zDsx zYgpQex?~xDU@TEmqR zZbrT90CH=5&zhe*LC2fI<{8o}*Iwg0@e(8}Z&E01>^8(#yc&y5sx^i+#JX=lG;uW- zW!u8s(_@WTiqRv1Rg5^@fLS|Glo7RXCBcX?Q9lLP@^0#~aNA$UqlVP8E(jJ3)Tppw zn?VyqT`u2*De2$ZO6w_;f&x}u|I|6kp29~0eQ{ZR*vgc%WaO#PPu-@e89Lb`E(G&4 z)f*`WM7}&#_6BRt4=W%2JeKR1ok1m3CmPpIX6M?c%%v=V7h1{Y5(oE%kp{%S-3~Ps z)nHso%4BcD@>i&F0R^4dnb5J-WUaT#X68`?C}abs{SR2>0yI(xW=}!9@Sm~ zPAP09x*4uiZ(y0{5YM|6kFBDUrk=?Ya|6^=${V^)|E*?%=JEX^d}*N)tab{9)3|oM zRU`|Q0rr3J5M0}%GbO;HnY>Z9*?kP$#=aNFn7E1f)dyrPz4uh7zU0hM*y;*UV ziZO&9^r}b8bQO-%u8ZcQGgqMT5UabYMrpNvY70{aN7F>3a{GjWT_cip zN#|^S(0pA>tGUDJE*;_Bx}iJ8BrM2CyjH{KDzr&!QP0%4?-!sengk{`{ib-ZeB{U1 z+^m2AqOOf&u8bOq-^Ja8iHrgttEvGO+eTi3SB{?i^RjVjf1sS6)=2D=6J|;wl z#XubvT6ter)A#=MY4r{0*{203FvY<3c8L1B<=uY=#W8Yf<>o3{r6Gqg>tEe(b+Mlm zoJhYsKxH~A#lO_a@jw;^_c>-bV0AFYbn|xs#^M#nHdMSPwhm~84LZ%N*9n^d+Q@NN zF=p;kd5gFII-$l3>a>WoiQ3>|qYE6ZW`bJfikx>3ZQ&u_{47ksgd$y24M7oyM~ZSG zZ6o9fQ#^p^J`pLmR(PnrOO9Jebct5UU3by$dAf(c$e6<q6Y_7bHz5Pf- zKB8&4VA~r(pt!UOTrkQ}cg^=Qzx>>_FQ-dr|8*@3>e0ZJPD~^cM}BeOes^BokJ*~1 zVP>okVQi;_XpO5h2kO;+?r}Nq5sUdGs9c_qz_u!wHIUAAGK?H1?Kh?lW_i@>hHp;h zmH>ov5PemF$gtP6;MX|8ycW%c0q+J#z4>B}Ev%GmR08irBU$|0Yh=+yVP+wM%V@%E z-fOSw$r$!eiv#Q3mX0j1n>xxb3x%J<;t*#( z&~|jn500^G!c^9tZ=ulfaZ%lvLuF%65PuP)C4<^UQ!ynyC)x)l8bdy!00`~YJaV*>`vI|mg6gC`5R9z2;%n3bffp^)cIH0%;mak zsrM%v?RjWZbPpms&6XF=2(bbM7st8Hsh~|apE}O#BhO{XEht4m{8jF5-~9A``qA&^ zBN9o}4v9hsB|Y%E-M^3V$XGm?$E@3q-b+5xKi8uOb8lLI(v(%Fx39%HsX>gj8xcm)K0723Sv5AR1`M8HzDWMqdc z#yqq;;ze&zG{--9nG?Ta=-kDmqj45in{ZH@$Mwkq5t^IlDG>ScvSKV+Z-hl+QK&Ph zglm$u_;eaG8!^&l(_rm;0H{OjhggFP)RKoCZnzQkW7fp$7%SOcN@OhV#>xQyFw0Q< zoELnajp=leEE{xAv{J3ptt0r2-%1*OG!JddyM-e!)Y_CKTYW`=V&(hwTUhuQF`|L6 z=s;Q>O&;{?FWrC=b{#u3+~nYprnNz;I-PJ-x)~0dg}#TRt^#|cGea`rUD9KZMrU9E z`{KWtW0(o_{I%~?Z3jZhmVMw%>&}1n`M)htxf$ijqZMs|5F{ke%!x2@~XePg6hXTf11eh$u~ zYrV)LXnjQS`a((zQQd@D>=uNsYCvY6YN4*N4hxobekBwim~pgn+5foWS#&6;#ORRt zS?v{vwm|6v$4mV;snVQqC zq4t@eg-hK?YDZt;8%1zl^N_6UAZ%wDU+Tk>{JQ>GJc|-Yj`a0q8%x%MXXS69NzyX$ zi#4}T*m9PL`gD-Ys*Mq5xK4P%!zOA=nS<-vT|&bXI#cFh*tYZ>3`PgS%Y=KooIinf zfMjv(^ZULJ2r`{QuH)$J^MBzSI+fF&;5WK9kHDm(hCHkm{e+mS$`u20fG|aYdx`R< z)8>gz88leP`yQ=`{9)o^#{FgxOau-*`qBEH^+SyYyhUnWuxh}0 zxn^{|B^=G?6Xs0#`Rm2l$bj}*->-Ji4*uG%LluYFm=TC>e9V$u8Nq(wvg+c6H4NHI zRqiJe;7GpvcX>+m=)nTrsn$)q`oWT8S4upiacybpj1vG1N#|uNZ4<`#lSeGc@E(dK z3xGr{(!jw2qM7oy!w;yIG1lLEkZS?YQnQb&lA-71CSb!`d9z4bi&^}q*Ynp9@c6?( z)%2;i@GzkYi(jfd;ro6HVK!`J4(nB6c!Ew^=`!OT+9F_?c=S8OzcdCD-%_^ zBAyu8ZDbb%i^(-?YO9zP0=0yxKm@X9Yg{sdk+s=leC$;XVnXya!baSqAr*OS2fz<& z%J|JYFu`1H`6JXD|4P6gu>nWu*rAprLK&N<}EygYftdAe>!utgOaoTE? z&V&D0A8V!YcP6D}8xXNBIyg>FkN}ooT#XS22PcE4&iH;d+L&iOj-GxiY|LZQb97%= zR!|gDXy-wz&`T77?JUVJR=*ZF*|0W{;3~>?!CRl{p zC+g4;KFVWq??-}#du`Kr?LkIwnOKE>8O8Eyq5c@D;c$HKOqDqz{RtFQV|dhfd2sv1 zP27cXzw4JAa~fMpZ8DC2idQ7y_IK{#TU^23JGSb1M5k!IWT5&^ns1KH&iSK9nc*K;BJ){t%I$B^Iv z-J_&uIk0#p4$AL#y#?JAoe>Wf1qK(02fFZEpu2wd#Qk2fcB|#JvG7ka!c6qO>4WT_ z$SqAM@ZbcOc-b9)cnF+6H6pkfNomcBqGdhP3ciK1;jv!ngL(SWoctXh^eZO{vZ86;Od8Zi{$jD0j?@JXbXybr zQqJ4t+vl!-k|RXLpo!Hd@-57jJ=;5Q$eM-j^{G$BMFMhJ-koOKDGw5L*cN}M4f6P= zy*q&r^?Js?=WTuL%?)QO8*}OUSvi)4jX6CrwEg1Ol~Y-Z)$I+y8b!4y(XyDKp=BZJ z(0LV3yfWP8#^B5IO?po(^e={iq{9rQw@EIG!1l-7Rl5%uSCgVbjRm3bdjPKphZUrE z6F38=5K%x61`GklzR>cT7m(f?2!rd8zWdYgwZ3}7SyoZ*tpxSZ_aFRk+mjO!^@495 zg!}p}q{kzfbfGPfrf_(@&2u7rRQ!D}@X_ZFGrTd=*34W;ZRfx{;$7VgHj$O}mpRZ& zQIB(mL%e8E%&YsI;{sO~UsX#KxRvia!i?yAj2cJO=kB4_FUuZwyiIWp3OXd`Y&CmK}6w2gm%kyH1&I%X$axN+QD9 z1i1#Pj$5GK19$2{qQ7TUR%|L66O+y2?Jk&WoKW7LqKglUJU9O|0+?KS%@6Zc8=dfL zZMXwdSYn`1ruj;sOU5+m39zy;y66mCloC$|G*W7r_rXP7CH$n0#}q^+phtHd)(Znd zNiF9{->^r_V3;S&;XPh{i#4j}wyO|ttY-!{rH>?<8l{*omTkW{zMJh&O$IHV;3IeA zuXIC3#8pJ0hsM_HGhPob)W&I4hnjXfqp1W`C9yDR*z{M?Pe~vID09C(Vl-;B2uSdu z{pHkw-L2H@a14vjv#}HS{VQwrs3M!07Ok)two1ZebNfh$>&`hR)8mLBewRbcHmIq1IJl zbZSWq0+385NdO4CFljN!6ZHs^xIr4ZiKYQ)xUh4~3BJu@J0M(tfIY?wU7|)K1Y)vf z40Q33)|;xFYEv0jCfdd;Y)G1shlxB2>%BC#oxE1ZQVi*z#qRw8xOvQpYYY zD8T4~_DmA}2n1kLW$NceJ3$f+)#PemI(&&z-H76i?H~S)4iE@VPzg}2*S7Mx-cmnl zM3H(`=nvweF`ho^H23+Nw@vahGP47$Kd#-3+E~VVKX>xBL^U@z^=-VCwP@Ezv4b7D zG?x;SXEL|n*RvecMk&~v!Cu5~I8nRiHR&hnDlVv6DinJ30>EO-H5&Y8QcR;BEw25n4dsLr(0NP2!?PJRIz|Zl68d*hH(( z$C~upa(hFT1eIlqdo6*qAsar6z@`;6DT=k*u~AG3N@=O6cmHT|yxaGy`j zl)6z|#u%ZVBYA3=J&=0{^#!Q}+^#;r-wbzp&HDNB;Zx=mE=TV{NRGQ9dH9E1B#O#M zA?l7AU-&EnSur>E-YUeZ_Ia7|GMY?>BP;N_zf?%DCx&3aF6fSqOx1r6W(FXmwzJQV zaP@h5fBp3%M;vMDfKaa$bWZ8yBS>!KMkQCqYu{t<#FPp@r%0u`PVqBXq!WM=S0q!r z69RR#-zOgfCl>ThjGk8kK5qh#nCK7k1u<4BAx5TYaOI%ey0 zTVPFZ;GK^GfV-#~q&WPxeuY@F21e)~VwCcBos@p?#T{r3*yMst@Cear*kQ+(T|q+a zCX4y0umLD&)~DAks|w4D6;!)4;Q@QcnijGmJi)E9_8V@{EDZ@EOFLswfssk@Uxo+Za=07s>i8X@-U71_D;Q-rU>w)*=E3?{UStaQt*u}}9$;XDR`%qOlE^su;zo*83!lD#g6-GS8^YT0K2<7Ep zP0$oJ(9m*pdeZB43P?~0 z8iHEW!(?F$adJ<6rE(nZTokO z@Hz|b&ju7SXWcKx7w|UxZ)9l_ru3`6IZt3dQ;xF`3$jq2#7l?=Bp5P`va&4 z!Kea+lz!=tb$Sp-nMo(xg8AhX-0Bc%YC$ypb{~tfxS1RUnRokNblp<(yeI&u!uV)( zHq2jqhkh*&aLu+BE%s7ntTDsgwUSBUA%8smOdsPsy&04XMcbGNA`3kOUA>pTMQ{AF z{@?c#D0i^Opr8aGYd6l3y``A&^J0)t;qpLqj9{UZwJ}I2Wp^$tl)i2(&D?D3gUu=T$xQLQ()s9VjnRBvK@Paq!Xf%tpy|eSCFMzni{E%SZ_mn{8+6NU%gH zvDz}v1aP&_2w=p&Z!k$H4r5z6{%^Dr_eK0ZkXEX_4xFRo)sFA8(zi=ZRVgyGmT3;n z$o#6Tc+@Nhc7Fuu2Hsm*VR`VSFuWb^a=0DwHf@Xgk9%LPAG&E3GXyM2T}YY;lZEE)y`^r-Sl9=gFYt`4?hj%|&idDa3+e_Ea4W zC=Gp6BK2?R<4q5(zAAbt5n--XL4j=1tgQb5KtR90jB9Rq^YFqWH>Y~JM;FfU5AI*d zgfZ%|-<8+k(x~&1Ca`2$vfY{Y3h=hvlosUXmWb~=w(GEP7z4=jrZpNIrbj>Dgk2IqI z0i9J?C7gDkNo2mt@J%;#YdEP@R`Ywyx65#tF_RYZNrd<)81^#9P9%Nh0S@J-^}$o7 zsmlBW`3R8+9kreyKDpfh!evZ)5e)#JEWNz3-q|sl#UX{u-B{WRDC6^N9+s}07fkHU z_rpzD)2UVYjj?M-MM_n_Co8yw!6x~#OP2OyU@y)Uk)UI0NNp! zI*qEm|5_!aK{ZPWx4k8hRpb>~PRw#2urq#HWsnZ$8~d&nugyBweT4$xZ2na#g$sN@ zVpi2FO4TtX`S6gk6okM>e%qID31nB7nVZ40z8V$R0N1OKEn?j($OND002UpUX^I}b zl+7Dw8s!%xo8%J(Q{&=;D23PY;~V(5NVTa?0rKO5}?X z^9N0VF281f21k_CFI&v+W3nRx{9a(d2u~{e#>nk7S1zfK{a3IY;AE5_@j`Z2h78Oh zT;3)A3SKA(6{XbpJ3)mb&hx;^O&{W(Ty11&D>NJK1Z$5>GDH|Uf%ERX$s-p6sGWH9 zW8*PWckU5*gt__}j#uMeQZ2P=mM=%Y)wuuRm^*^`OMUoNgimucDPBLD#uGO{CQLheVLaF(7y zmToPUjj7bea#(maX|oXVUxs4Y!fE#9e2 zXgruvi@_Ib`!o-B=)SY&*7)qPadFR{>C@xJ`qgFSWt$O$%M8ag$iUKwFc5{@-&790 z9?y@prqPrYW-A_Z8xM@w8?=@;Er!vwR0%n8mBAHhBmW0=WkpLO8vKE^u)q~Fyn<^T zHW!7c$p8ZE0S^dbgkp!OVL5ZN_UXHuYz^h+z zz%k%3aU~F>1^1NXZevuAy5x9(R;An*16-WDci+8~znyEVDa3-7nP-ae^tKaG<1`J% zu%baW$9`rg>R~$&!<)x7q8rG!i!GJ^-Y{q1#hGgifx`&oEB;jSpuP}4Aa#8-sUgeU z5DS^7g5{eXDx7YEDPxx>%s_*G98~b{Iq?#DzeFOdy652O!SvK;b8HI13d^k|%9?_? zw;oooYYUQ*5YBcGP$yAmCnFl$o;>_8h|THVu2IGFyaW4}@?i{nOR#l%^cP8M!_oO$ zGoeCdG25;aYr%x5j-q*Tp6O@y4MNlalywqSZxkR$Ck`TPkYXek$vE95KrX95Xq>z# zH{dokN@5`*yC-APnAtrlcyvRLC2xLIU|X@LE*&#Tn8f(S0X{+`LI-VUh!3uJ zKroq89)v?cC(AEyEO)j{X0b?Na`#p?f{Hl&+Xtnqrv=kHGkzc7U|p@TC*Oqjuz0e9 zj0(IBo*La3pto*cG5wIb@NKU0uyHyR2CnonrPsrWAOJRqWzM5&uYcBwX%J0P!fo$K zWEFV@R+FqGaZ10AJb-*?17S>qD8=9ji(Y=6v!b_)s^4L63wXtFJIek^F@Kbfx-#_Q{woK&48#TaF7HhXwWn zFj?zj#j%_a;VvFF;|=%tmRD#YO$H4Ol3{=M$T{g`+TPj5o0)Tp!&Upu$7{wg>0@;R zxR0O|z~Q&d?Fv%Y96=fsfhY{70YyL!T-W-7C;E}UR=}TE8`g2^zDWbIp+xSgFe%5ZLmm$j z^|Sa+$BLg%IOtOs1GH2zs!?uq!6Bm3K~l#y@WbM7J`(m8K}I)tdXYb@zux3>DwJ1= zOI)!uY2=y%veAUsnc^^AFDlgTwO1`$G?{%FU?R9mDfJuU=mCidB66H38G7@rrH)Mr z_S^CLZI-X)1%5Gpt02>Ex7Xf~OWbCc7dtqia?9hDrRjIg4h0(!SENi=gw1qloOjG90T;yFvvMSl!I=871UVa|}@i8gsDjfSw`v&hD$SD~b^ zg!6JaIVyk9K2h;Z#577icO|y#o&554x`w33Aoy+sxkUA5gR}s`Q@4*TtNjVQpCbRX4qhz-b_GI>-K+NV|#IrmhW&aj02P( z{)U`>)&-rOW1}k~bM4`9cK@`xP)f`0XCIK)h^()bI7tSwqlY_{J&o`)@u0^ZsgRi6KSc0}Ou; zHF8XU(1P`YE1c*sXVYq9%5AA9jw#?@U-Hk@;!9T0%U#n?Ddj}!{z2&~d{)3?9Tv-s z_hqSnrG$QA*Y&U(Xk|YSFAQYiX`V0V4K1lwm96=q+C+1YWb%ZS?d&hGGWh_OjA;A9 z!=_4}#Fd=aym9*=-=`;K4ZmP7*qj~Bgk7%er%_=ofzX7pIQ=%s=wzN#=kpiEz%P!s z;c48JM%fx?5W5U%P-_u?--s2NL|sf8d~; zQT?N3tv-fVmX})q0000GQZuzYW%qrAbkXw4h~(vuotibpJs!(~V1vB1h9O5KecY*U zoKGr2=v0y}e#9`4SVx9ktF6nYVAcRBOAr;(8NX&<2CaN???|y32WeEt&e8} zix&33t>cvfNRp^}cwiIg0P?Dw+0zb0xmqw{^EUfv7p}$Ah8nYyG6i^cApduCaZv0000sx59WH%?jfGlCwX2f146%_Lr<# z4cTv78cW|aOqJH-8GeNn!GKN+Y;=4^UcW=&BJ=v*c37p)Es4#CWHX4A3;@Fz3ZlY( zvN$WVbsRl8z_EvOF$+bygn$cJXVVy-QmhXYz^=W!Jb9X|Ipydlbdmr8Fag)`k!sh` z$Bj3kI2`WfuFX$VtG{b&YnMhw+9!P_8&NyyDB6kNNk-I8`bsvUchXU{6TXs-sGanb zZA9;+qiQF8B^v<<`5w^TD#36@ES)>G9SHnEHKi#w0x$d}XmnH2*D-ied@5JRypDgx zUo9M3{|9KYAI%hN^<|OnvRfEdg9td3N4$khdO+kSGetrHF@cd6{nWDsG>@^Hb4#B5 zE6}=8Y4*m7092?qNu2Y5P~fI`0F`Sk)LV?t7J;~+0001DU;*lIdoW`LPAHQ8(a}&w zZ-4zRp|W1f&mMHUb}Nkib;vk#`XUy3L!ZFN0`R?$&ubIGuX%W-JHPx-$Ib4Fvp99r zA+St)9a|l})&mJVPkkD5LaoVLzs+$sK!N!5euM9qTy=s2a~iv|bff1giC1iyxcPDP zM}+x?fj{rM(M*@Wxtp4q(TKtqb-ctO7`sKeq2`xjxmWK`1E|l}`Ci^>8*|2$57teN z6nOdcO|`9*SxrL#qqWU6vHBI8)3iDmOYrZS|H5r#vo3N=9Fj7OV@)5z)5fx^`yCW} zi$aIPo#n4KSD*Km(~BKMuMIn>ZIU|+Uww-fhC7i;u|l&Hh$~=O_4hzzo*D0NF@I7+ zaBFbRi6UJs_h!8!?Qw0f5Zx8|^GgIltA|LcsSk-#ha6d=9u; zUa02o%~fjt;@(NS-z{$X&RENi?T1bE@Z(fjk;kNI%p>26r3aNzEJ&B%82u*LV_NL~ zo<6;mh0N!DU(>Gd^c_h=I!wHU@`sDmNIwPL_!8DM%VC+-`tizt$ZCX)x+=r*G-x&> z&+Nv~9_)}#zgk%)ov>xz$WSDd8!HP~`2ff;ajHQm05IFWY{0T`(0?UOel-b9(59y| zOpBQl!@(;oKsK_GFyyhk2_CBuY?9&VmKQ%)1mc!P1Q^XoUDmZ%#I{@deK7G42Nnvz z2Kt9<+OUpq20=-4j$_q4t-VlAeuadZ%8nT8u;M3qj~VaM7ketU5clylscWttT}XB0 z-NTwK4^qiLYI4dTp@qOAlG8m__rd&hbB5iBfD$v+{HAbba=2@xf=iw+a!ygdnfXl; z?PYGA@e$}hA%y!HutT6YOyXzQR7J8+Hq*mRlyeY=Biz2SUVh>h%E>A35@*(>C3wvx zDqf(K!JB2PfOq=lay4}PkfGzGQFH90s@Ofj0Wm4J`RvrAygOs85yeq%vqJHi#)<(q zJpq<<+t`_@xH$T`a_cTO#Dyj`zdyFh3{DS>LsSa|STy$8dik$ZChEa=7TOEfco1J* zt5{aZ2aAf*SHisH92J3n0Wg4nZ%^hvuR;cG-0c5y*PmZ^cM|7ONRvNfD+a_C>`>h~ zw99@MKsy3+JT8cWvdCvN3#CCw|Jatyu1@w*J&L<6x?=X=ykpT>35M(6U;hbT#?M|v zIBk`kio^YYTSQ9qv5CG8#-pu|=)o0akNB1qtYR)J$9p|gn09km9&J0CV}pv2OCJ$* z3{4rp9@Z=4KPi2Q+C*RqlSb9$cP%6|3_5Dyd-kON`oHd^Xvubl3RjR_(@MJHK@b@S zgs5(!Y0jEcaa8nvPg@N1HSfv8w^?>eEcD&Q7SWhH#z6UF*XoD#jjcY`WS~-Q?rUYK z`y3xD5<4ibi+&xvc6VlTgq-9AqpvBdp_-2Sh?lB>zX{@?I5TMqO?5`P;f}7eJ9no7zgqY(oei* z%H?LCF9j9|U>6Qa(PdXaZc;HF%>Bo$j?wgSBUYi_WC_p;jy;Kc)sGpVxD+oWO(th3FXOR0kSs(%?L2o5gPh) z(LaK0&f2-oH<;fQUL;d%6>`B9FJjQCKN2+dTj^90Ez*2v3{Vzo>G8s5bip}@NGlQr zoQxK(akJx#xeJApl>8tPDo6JHF%mijK034yc#EVcxBIpm1u|{0# z0AY~*Z-rZp2rDQ~lm04k2TO5~7dTi6yU!!WhY*<4R)VQE!@;fgBpK}JX8sPIPMdH@q%a0RRS%2E#KLQn5L7XAyiXk zB8oSpbAeMd5FFhBDo>JqT;G8PMbWR)0Q*RXw8jrO0r2PdKe;()qdI4+`Dg&X8x{d5 zY2^ch(668(Rb{befE?JethxdvR$N?oVrrly={x`V?fstOOQd5qr=Hu*=JS)=l-aC| zJl=CEn5@W8116GICJBmCAQO$RJQ>~R-Y!u`o#cY&n5 zPD~gnDbP2y)3Ir^cf9;*j$~@O{8dX~Z>x>%)yQ`IjCalk@Z4#ofONlSd>XfOO&U$2 zuW71tS2-dT$BQOy)C)|cULxIKCwrdM$4!O1hJjxw6&+VW2JwU5LlI|D$`$qyPdz== zX$8m7)JtPjRiej$ylIc7aYTBs@M)wZT_}S z6}53^o}zu;CLh1r1N(+AeZk(fAwu+vi<#c`eA15NCRXb1IJxE}j^VlT{+mg|U@4nr z>mV6&sZoV+0_#&Sy|pd9&xIIZ13{}4^{m(pWz!;kJ?eO7v)1I!Mh1HNWv1ZBWcPb{ z``JPB+E?rh^GfgxTWUWHiGWOazbH2=WRYhqAunUf^Zog5v0T9svDM1s@# zqFd_`RLL|d^E�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 L963b*00000DvmBP literal 0 HcmV?d00001 diff --git a/apps/app/public/onboarding/modules.webp b/apps/app/public/onboarding/modules.webp new file mode 100644 index 0000000000000000000000000000000000000000..b4ceabae1514dae536a8599b0c27854bf9d774ca GIT binary patch literal 45818 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 literal 0 HcmV?d00001 diff --git a/apps/app/public/onboarding/pages.webp b/apps/app/public/onboarding/pages.webp new file mode 100644 index 0000000000000000000000000000000000000000..90084952b5d0eba29483a03461768e35db30883b GIT binary patch literal 54686 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 |x%{jC6uV-i1<8TE9RxmN3Q6g>O( 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 literal 0 HcmV?d00001 diff --git a/apps/app/public/onboarding/views.webp b/apps/app/public/onboarding/views.webp new file mode 100644 index 0000000000000000000000000000000000000000..d92a151142c6da9154d8186a4b15a5cee03acc7b GIT binary patch literal 40132 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 Date: Mon, 14 Aug 2023 14:27:02 +0530 Subject: [PATCH 33/33] fix: gantt chart block left drag flicker (#1854) * fix: left drag flicker * fix: opposite side manual scroll --- .../cycles/cycles-list-gantt-chart.tsx | 4 +- .../gantt-chart/helpers/draggable.tsx | 128 ++++++++++++------ .../gantt-chart/hooks/block-update.tsx | 4 +- .../modules/modules-list-gantt-chart.tsx | 10 +- 4 files changed, 98 insertions(+), 48 deletions(-) diff --git a/apps/app/components/cycles/cycles-list-gantt-chart.tsx b/apps/app/components/cycles/cycles-list-gantt-chart.tsx index ea66f0929..c5d60015c 100644 --- a/apps/app/components/cycles/cycles-list-gantt-chart.tsx +++ b/apps/app/components/cycles/cycles-list-gantt-chart.tsx @@ -65,9 +65,7 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; - cyclesService - .patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user) - .finally(() => mutateCycles()); + cyclesService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user); }; const blockFormat = (blocks: ICycle[]) => diff --git a/apps/app/components/gantt-chart/helpers/draggable.tsx b/apps/app/components/gantt-chart/helpers/draggable.tsx index 8a85a0dd3..320f4355f 100644 --- a/apps/app/components/gantt-chart/helpers/draggable.tsx +++ b/apps/app/components/gantt-chart/helpers/draggable.tsx @@ -31,7 +31,36 @@ export const ChartDraggable: React.FC = ({ const { currentViewData } = useChart(); - const handleDrag = (dragDirection: "left" | "right") => { + const checkScrollEnd = (e: MouseEvent): number => { + let delWidth = 0; + + const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; + const appSidebar = document.querySelector("#app-sidebar") as HTMLElement; + + const posFromLeft = e.clientX; + // manually scroll to left if reached the left end while dragging + if (posFromLeft - appSidebar.clientWidth <= 70) { + if (e.movementX > 0) return 0; + + delWidth = -5; + + scrollContainer.scrollBy(delWidth, 0); + } else delWidth = e.movementX; + + // manually scroll to right if reached the right end while dragging + const posFromRight = window.innerWidth - e.clientX; + if (posFromRight <= 70) { + if (e.movementX < 0) return 0; + + delWidth = 5; + + scrollContainer.scrollBy(delWidth, 0); + } else delWidth = e.movementX; + + return delWidth; + }; + + const handleLeftDrag = () => { if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position) return; @@ -44,54 +73,30 @@ export const ChartDraggable: React.FC = ({ resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); - let initialMarginLeft = block?.position?.marginLeft; + let initialMarginLeft = parseInt(parentDiv.style.marginLeft); const handleMouseMove = (e: MouseEvent) => { if (!window) return; let delWidth = 0; - const posFromLeft = e.clientX; - const posFromRight = window.innerWidth - e.clientX; + delWidth = checkScrollEnd(e); - const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; - const appSidebar = document.querySelector("#app-sidebar") as HTMLElement; - - // manually scroll to left if reached the left end while dragging - if (posFromLeft - appSidebar.clientWidth <= 70) { - if (e.movementX > 0) return; - - delWidth = dragDirection === "left" ? -5 : 5; - - scrollContainer.scrollBy(-1 * Math.abs(delWidth), 0); - } else delWidth = dragDirection === "left" ? -1 * e.movementX : e.movementX; - - // manually scroll to right if reached the right end while dragging - if (posFromRight <= 70) { - if (e.movementX < 0) return; - - delWidth = dragDirection === "left" ? -5 : 5; - - scrollContainer.scrollBy(Math.abs(delWidth), 0); - } else delWidth = dragDirection === "left" ? -1 * e.movementX : e.movementX; - - // calculate new width and update the initialMarginLeft using += - const newWidth = Math.round((initialWidth += delWidth) / columnWidth) * columnWidth; + // 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`; - if (block.position) block.position.width = newWidth; + parentDiv.style.marginLeft = `${newMarginLeft}px`; - // update the margin left of the block if dragging from the left end - if (dragDirection === "left") { - // calculate new marginLeft and update the initial marginLeft using -= - const newMarginLeft = - Math.round((initialMarginLeft -= delWidth) / columnWidth) * columnWidth; - - parentDiv.style.marginLeft = `${newMarginLeft}px`; - if (block.position) block.position.marginLeft = newMarginLeft; + if (block.position) { + block.position.width = newWidth; + block.position.marginLeft = newMarginLeft; } }; @@ -103,7 +108,52 @@ export const ChartDraggable: React.FC = ({ (resizableDiv.clientWidth - blockInitialWidth) / columnWidth ); - handleBlock(totalBlockShifts, dragDirection); + handleBlock(totalBlockShifts, "left"); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }; + + const handleRightDrag = () => { + if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position) + 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) => { + if (!window) return; + + 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); + }; + + 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); @@ -122,7 +172,7 @@ export const ChartDraggable: React.FC = ({ {enableLeftDrag && ( <>
handleDrag("left")} + onMouseDown={handleLeftDrag} onMouseEnter={() => setIsLeftResizing(true)} onMouseLeave={() => setIsLeftResizing(false)} className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[1] w-6 h-10 bg-brand-backdrop rounded-md cursor-col-resize" @@ -138,7 +188,7 @@ export const ChartDraggable: React.FC = ({ {enableRightDrag && ( <>
handleDrag("right")} + onMouseDown={handleRightDrag} onMouseEnter={() => setIsRightResizing(true)} onMouseLeave={() => setIsRightResizing(false)} className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[1] w-6 h-6 bg-brand-backdrop rounded-md cursor-col-resize" diff --git a/apps/app/components/gantt-chart/hooks/block-update.tsx b/apps/app/components/gantt-chart/hooks/block-update.tsx index d9d808b38..5d183305b 100644 --- a/apps/app/components/gantt-chart/hooks/block-update.tsx +++ b/apps/app/components/gantt-chart/hooks/block-update.tsx @@ -37,7 +37,5 @@ export const updateGanttIssue = ( if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; - issuesService - .patchIssue(workspaceSlug, issue.project, issue.id, newPayload, user) - .finally(() => mutate()); + issuesService.patchIssue(workspaceSlug, issue.project, issue.id, newPayload, user); }; diff --git a/apps/app/components/modules/modules-list-gantt-chart.tsx b/apps/app/components/modules/modules-list-gantt-chart.tsx index 2dd482d8b..64ceccd1a 100644 --- a/apps/app/components/modules/modules-list-gantt-chart.tsx +++ b/apps/app/components/modules/modules-list-gantt-chart.tsx @@ -69,9 +69,13 @@ export const ModulesListGanttChartView: FC = ({ modules, mutateModules }) if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; - modulesService - .patchModule(workspaceSlug.toString(), module.project, module.id, newPayload, user) - .finally(() => mutateModules()); + modulesService.patchModule( + workspaceSlug.toString(), + module.project, + module.id, + newPayload, + user + ); }; const blockFormat = (blocks: IModule[]) =>