mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: Issue pagination (#4109)
* dev: separate order by of issue queryset to separate utilty function * dev: pagination for spreadhseet and gantt * dev: group pagination * dev: paginate single entities * dev: refactor pagination * dev: paginating issue apis * dev: grouped pagination for empty groups * dev: ungrouped list * dev: fix paginator for single groups * dev: fix paginating true list * dev: state__group pagination * fix: imports * dev: fix grouping on taget date and project_id * dev: remove unused imports * dev: add ruff in dependencies * make store changes for pagination * fix some build errors due to type changes * dev: add total pages key * chore: paginator changes * implement pagination for spreadsheet, list, kanban and calendar * fix: order by grouped pagination * dev: sub group paginator * dev: grouped paginator * dev: sub grouping paginator * restructure gantt layout charts * dev: fix pagination count * dev: date filtering for issues * dev: group by counts * implement new logic for pagination layouts * fix: label id and assignee id interchange * dev: fix priority ordering * fix group by bugs * dev: grouping for priority * fix reeordering while update * dev: fix order by for pagination * fix: total results for sub group pagination * dev: add comments and fix ordering * fix orderby priority for spreadsheet * fix subGroupCount * Fix logic for load more in Kanban * fix issue quick add * dev: fix issue creation * dev: add sorting * fix order by for modules and cycles * fix non render of Issues * fix subGroupKey generation when subGroupId is null * dev: fix cycle and module issue * dev: fix sub grouping * fix: imports * fix minor build errors * fix major build errors * fix priority order by * grouped pagination cursor logic changes * fix calendar pagination * active cycle issues pagination * dev: fix lint errors * fix Kanban subgroup dnd * fix empty subgroup kanbans * fix updation from an empty field with groupBy * fix issue count of groups * fix issue sorting on first page fetch * dev: remove pagination from list endpoint add ordering for sub grouping and handle error for empty issues * refactor module and cycle issues * fix quick add refactor * refactor gantt roots * fix empty states * fix filter params * fix group by module * minor UX changes * fix sub grouping in Kanban * remove unnecessary sorting logic in backend (Nikhil's changes) * dev: add error handling when using without on results * calendar layout loader improvement * list per page count logic change * spreadsheet loader improvement * Added loader for issues load more pagination * fix quick add in gantt * dev: add profile issue pagination * fix all issue and profile issues logic * remove empty state from calendar layout * use useEffect instead of swr to fetch issues to have quick switching between views cycles etc * dev: add aggregation for multi fields * fix priority sorting for workspace issues * fix move from draft for draft issues * fix pagination loader for spreadsheet * fetch project, module and cycle stats on update, create and delete of issues * increase horizontal margin * change load more pagination to on scroll pagination for active cycle issues * fix linting error * dev: fix ordering when order by m2m * dev: fix null paginations * dev: commenting * 0add comments to the issue stores methods * fix order by for array properties * fix: priority ordering * perform optimistic updates while adding or removing cycles or modules * fix build errors * dev: add default values when iterating through sub group * Move code from EE to CE repo * chore: folder structure updates * Move sortabla and radio input to packages/ui * chore: updated empty and loading screens * chore: delete an estimate point * chore: estimate point response change * chore: updated create estimate and handled the build error * chore: migration fixes * chore: updated create estimate * [WEB-1322] dev: conflict free pages collaboration (#4463) * chore: pages realtime * chore: empty binary response * chore: added a ypy package * feat: pages collaboration * chore: update fetching logic * chore: degrade ypy version * chore: replace useEffect fetch logic with useSWR * chore: move all the update logic to the page store * refactor: remove react-hook-form * chore: save description_html as well * chore: migrate old data logic * fix: added description_binary as field name * fix: code cleanup * refactor: create separate hook to handle page description * fix: build errors * chore: combine updates instead of using the whole document * chore: removed ypy package * chore: added conflict resolving logic to the client side * chore: add a save changes button * chore: add read-only validation * chore: remove saving state information * chore: added permission class * chore: removed the migration file * chore: corrected the model field * chore: rename pageStore to page * chore: update collaboration provider * chore: add try catch to handle error --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * chore: create estimate workflow update * chore: editing and deleting the existing estimate updates * chore: updating the new estinates in update modal * chore: ui changed * chore: response changes of get and post * chore: new field added in estimates * chore: individual endpoint for estimate points * chore: typo changes * chore: create estimate point * chore: integrated new endpoints * chore: update key value pair * chore: update sorting in the estimates * Add custom option in the estimate templates * chore: handled current project active estimate * chore: handle estimate update worklfow * chore: AIO docker images for preview deployments (#4605) * fix: adding single docker base file * action added * fix action * dockerfile.base modified * action fix * dockerfile * fix: base aio dockerfile * fix: dockerfile.base * fix: dockerfile base * fix: modified folder structure * fix: action * fix: dockerfile * fix: dockerfile.base * fix: supervisor file name changed * fix: base dockerfile updated * fix dockerfile base * fix: base dockerfile * fix: docker files * fix: base dockerfile * update base image * modified docker aio base * aio base modified to debian-12-slim * fixes * finalize the dockerfiles with volume exposure * modified the aio build and dockerfile * fix: codacy suggestions implemented * fix: codacy fix * update aio build action --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * chore: handled estimates switch * chore: handled estimate edit * chore: handled close button in estimate edit * chore: updated ceate estimare workflow * chore: updated switch estimate * fix minor bugs in base issues store * single column scroll pagination * UI changes for load more button * chore: UI and typos * chore: resolved build error * [WEB-1184] feat: issue bulk operations (#4530) * chore: bulk operations * chore: archive bulk issues * chore: bulk ops keys changed * chore: bulk delete and archive confirmation modals * style: list layout spacing * chore: create hoc for multi-select groups * chore: update multiple select components * chore: archive, target and start date error messsage * chore: edge case handling * chore: bulk ops in spreadsheet layout * chore: update UI * chore: scroll element into view * fix: shift + arrow navigation * chore: implement bulk ops in the gantt layout * fix: ui bugs * chore: move selection logic to store * fix: group selection * refactor: multiple select store * style: dropdowns UI * fix: bulk assignee and label update mutation * chore: removed migrations * refactor: entities grouping logic * fix performance issue is selection of bulk ops * fix: shift keyboard navigation * fix: group click action * chore: start and target date validation * chore: remove optimistic updates, check archivability in frontend * chore: code optimisation * chore: add store comments * refactor: component fragmentation * style: issue active state --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: rahulramesha <rahulramesham@gmail.com> * fix a performance issue when there are too many groups * chore: updated delete dropdown and handled the repeated values while creating and updating the estimate point * [WEB-1424] chore: page and view logo implementation, and emoji/icon picker improvement (#4583) * chore: added logo_props * chore: logo props in cycles, views and modules * chore: emoji icon picker types updated * chore: info icon added to plane ui package * chore: icon color adjust helper function added * style: icon picker ui improvement and default color options updated * chore: update page logo action added in store * chore: emoji code to unicode helper function added * chore: common logo renderer component added * chore: app header project logo updated * chore: project logo updated across platform * chore: page logo picker added * chore: control link component improvement * chore: list item improvement * chore: emoji picker component updated * chore: space app and package logo prop type updated * chore: migration * chore: logo added to project view * chore: page logo picker added in create modal and breadcrumbs * chore: view logo picker added in create modal and updated breadcrumbs * fix: build error * chore: AIO docker images for preview deployments (#4605) * fix: adding single docker base file * action added * fix action * dockerfile.base modified * action fix * dockerfile * fix: base aio dockerfile * fix: dockerfile.base * fix: dockerfile base * fix: modified folder structure * fix: action * fix: dockerfile * fix: dockerfile.base * fix: supervisor file name changed * fix: base dockerfile updated * fix dockerfile base * fix: base dockerfile * fix: docker files * fix: base dockerfile * update base image * modified docker aio base * aio base modified to debian-12-slim * fixes * finalize the dockerfiles with volume exposure * modified the aio build and dockerfile * fix: codacy suggestions implemented * fix: codacy fix * update aio build action --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * fix: merge conflict * chore: lucide react added to planu ui package * chore: new emoji picker component added with lucid icon and code refactor * chore: logo component updated * chore: emoji picker updated for pages and views --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * chore: handled inline errors in the estimate switch * fix module and cycle drag and drop * Fix issue count bug for accumulated actions * chore: handled active and availability vadilation * chore: handled create and update components in projecr estimates * chore: added migration * Add category specific values for custom template * chore: estimate dropdown handled in issues * chore: estimate alerts * fix bulk updates * chore: updated alerts * add optional chaining * Extract the list row actions * change color of load more to match new Issues * list group collapsible * fix: updated and handled the estimate points * fix: upgrader ee banner * Fix issues with sortable * Fix sortable spacing issue in create estimate modal * fix: updated the issue create sorting * chore: removed radio button from ui and updated in the estimates * chore: resolved import error in packaged ui * chore: handled props in create modal * chore: removed ee files * chore: changed default analytics * fix: pagination ordering for grouped and subgrouped * chore: removed the migration file * chore: estimate point value in graph * chore: estimate point key change * chore: squashed migration (#4634) * chore: squashed migration * chore: removed instance migraion * chore: key changes * chore: issue activity back migration * dev: replaced estimate key with estimate id and replaced estimate type from number to string in issue * chore: estimate point value field * chore: estimate point activity * chore: removed the unused function * chore: resolved merge conflicts * chore: deploy board keys changed * chore: yarn lock file change * chore: resolved frontend build --------- Co-authored-by: guru_sainath <gurusainath007@gmail.com> * [WEB-1516] refactor: space app routing and layouts (#4705) * dev: change layout * chore: replace workspace slug and project id with anchor * chore: migration fixes * chore: update filtering logic * chore: endpoint changes * chore: update endpoint * chore: changed url pratterns * chore: use client side for layout and page * chore: issue vote changes * chore: project deploy board response change * refactor: publish project store and components * fix: update layout options after fetching settings * chore: remove unnecessary types * style: peek overview * refactor: components folder structure * fix: redirect from old path * chore: make the whole issue block clickable * chore: removed the migration file * chore: add server side redirection for old routes * chore: is enabled key change * chore: update types * chore: removed the migration file --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * Merge develop into revamp-estimates-ce * chore: removed migration file and updated the estimate system order and removed ee banner * chore: initial radio select in create estimate * chore: space key changes * Fix sortable component as the sort order was broken. * fix: formatting and linting errors * fix Alignment for load more * add logic to approuter * fix approuter changes and fix build * chore: removed the linting issue --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com> Co-authored-by: guru_sainath <gurusainath007@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: pushya22 <130810100+pushya22@users.noreply.github.com>
This commit is contained in:
parent
7ac07b7b73
commit
666d35afb9
@ -2,10 +2,6 @@
|
||||
from .base import BaseSerializer
|
||||
|
||||
from plane.db.models import Estimate, EstimatePoint
|
||||
from plane.app.serializers import (
|
||||
WorkspaceLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
)
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
@ -19,6 +19,8 @@ from plane.app.views import (
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
IssueViewSet,
|
||||
LabelViewSet,
|
||||
BulkIssueOperationsEndpoint,
|
||||
BulkArchiveIssuesEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@ -81,6 +83,11 @@ urlpatterns = [
|
||||
BulkDeleteIssuesEndpoint.as_view(),
|
||||
name="project-issues-bulk",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-archive-issues/",
|
||||
BulkArchiveIssuesEndpoint.as_view(),
|
||||
name="bulk-archive-issues",
|
||||
),
|
||||
##
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
|
||||
@ -298,4 +305,9 @@ urlpatterns = [
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-operation-issues/",
|
||||
BulkIssueOperationsEndpoint.as_view(),
|
||||
name="bulk-operations-issues",
|
||||
),
|
||||
]
|
||||
|
@ -113,9 +113,7 @@ from .issue.activity import (
|
||||
IssueActivityEndpoint,
|
||||
)
|
||||
|
||||
from .issue.archive import (
|
||||
IssueArchiveViewSet,
|
||||
)
|
||||
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
|
||||
|
||||
from .issue.attachment import (
|
||||
IssueAttachmentEndpoint,
|
||||
@ -154,6 +152,8 @@ from .issue.subscriber import (
|
||||
)
|
||||
|
||||
|
||||
from .issue.bulk_operations import BulkIssueOperationsEndpoint
|
||||
|
||||
from .module.base import (
|
||||
ModuleViewSet,
|
||||
ModuleLinkViewSet,
|
||||
|
@ -1,4 +1,6 @@
|
||||
# Python imports
|
||||
import traceback
|
||||
|
||||
import zoneinfo
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
@ -76,7 +78,11 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
response = super().handle_exception(exc)
|
||||
return response
|
||||
except Exception as e:
|
||||
print(e) if settings.DEBUG else print("Server Error")
|
||||
(
|
||||
print(e, traceback.format_exc())
|
||||
if settings.DEBUG
|
||||
else print("Server Error")
|
||||
)
|
||||
if isinstance(e, IntegrityError):
|
||||
return Response(
|
||||
{"error": "The payload is not valid"},
|
||||
|
@ -2,43 +2,50 @@
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.db.models import (
|
||||
Func,
|
||||
F,
|
||||
Q,
|
||||
OuterRef,
|
||||
Value,
|
||||
UUIDField,
|
||||
)
|
||||
from django.core import serializers
|
||||
from django.db.models import (
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
IssueSerializer,
|
||||
CycleIssueSerializer,
|
||||
)
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
CycleIssue,
|
||||
Issue,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
@ -86,14 +93,9 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id, cycle_id):
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
order_by_param = request.GET.get("order_by", "created_at")
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
queryset = (
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
@ -105,7 +107,6 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
"issue_module__module",
|
||||
"issue_cycle__cycle",
|
||||
)
|
||||
.order_by(order_by)
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
@ -130,73 +131,112 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.order_by(order_by)
|
||||
)
|
||||
if self.fields:
|
||||
issues = IssueSerializer(
|
||||
queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
else:
|
||||
issues = queryset.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
issue_queryset = issue_queryset.filter(**filters)
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
# group and sub group pagination
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
# Group Paginate
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, cycle_id):
|
||||
issues = request.data.get("issues", [])
|
||||
|
@ -1,52 +1,53 @@
|
||||
# Django imports
|
||||
from django.db.models import (
|
||||
Q,
|
||||
Case,
|
||||
When,
|
||||
Value,
|
||||
CharField,
|
||||
Count,
|
||||
F,
|
||||
Exists,
|
||||
OuterRef,
|
||||
Subquery,
|
||||
JSONField,
|
||||
Func,
|
||||
Prefetch,
|
||||
IntegerField,
|
||||
)
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import UUIDField
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
Count,
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
IntegerField,
|
||||
JSONField,
|
||||
OuterRef,
|
||||
Prefetch,
|
||||
Q,
|
||||
Subquery,
|
||||
UUIDField,
|
||||
Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from plane.app.serializers import (
|
||||
DashboardSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueSerializer,
|
||||
WidgetSerializer,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Dashboard,
|
||||
DashboardWidget,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Project,
|
||||
ProjectMember,
|
||||
User,
|
||||
Widget,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
ProjectMember,
|
||||
Widget,
|
||||
DashboardWidget,
|
||||
Dashboard,
|
||||
Project,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
IssueRelation,
|
||||
User,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueActivitySerializer,
|
||||
IssueSerializer,
|
||||
DashboardSerializer,
|
||||
WidgetSerializer,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
def dashboard_overview_stats(self, request, slug):
|
||||
@ -569,6 +570,7 @@ def dashboard_recent_collaborators(self, request, slug):
|
||||
)
|
||||
|
||||
return self.paginate(
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=project_members_with_activities,
|
||||
controller=lambda qs: self.get_results_controller(qs, slug),
|
||||
|
@ -1,14 +1,14 @@
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
from plane.app.serializers import ExporterHistorySerializer
|
||||
from plane.bgtasks.export_task import issue_export_task
|
||||
from plane.db.models import ExporterHistory, Project, Workspace
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
from plane.bgtasks.export_task import issue_export_task
|
||||
from plane.db.models import Project, ExporterHistory, Workspace
|
||||
|
||||
from plane.app.serializers import ExporterHistorySerializer
|
||||
|
||||
|
||||
class ExportIssuesEndpoint(BaseAPIView):
|
||||
@ -72,6 +72,7 @@ class ExportIssuesEndpoint(BaseAPIView):
|
||||
"cursor", False
|
||||
):
|
||||
return self.paginate(
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=exporter_history,
|
||||
on_results=lambda exporter_history: ExporterHistorySerializer(
|
||||
|
@ -2,52 +2,54 @@
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import (
|
||||
Prefetch,
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Q,
|
||||
Case,
|
||||
Value,
|
||||
CharField,
|
||||
When,
|
||||
Exists,
|
||||
Max,
|
||||
UUIDField,
|
||||
)
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import (
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
Prefetch,
|
||||
Exists,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
IssueSerializer,
|
||||
IssueFlatSerializer,
|
||||
IssueDetailSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueFlatSerializer,
|
||||
IssueSerializer,
|
||||
IssueDetailSerializer
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueSubscriber,
|
||||
IssueReaction,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet, BaseAPIView
|
||||
|
||||
|
||||
class IssueArchiveViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
@ -92,33 +94,6 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@ -126,125 +101,116 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||
|
||||
# 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 = self.get_queryset().filter(**filters)
|
||||
|
||||
# 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)
|
||||
|
||||
issue_queryset = (
|
||||
issue_queryset
|
||||
if show_sub_issues == "true"
|
||||
else issue_queryset.filter(parent__isnull=True)
|
||||
)
|
||||
if self.expand or self.fields:
|
||||
issues = IssueSerializer(
|
||||
issue_queryset,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
).data
|
||||
else:
|
||||
issues = issue_queryset.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
# group and sub group pagination
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
# Group Paginate
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
@ -351,3 +317,58 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue.save()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
if not len(issue_ids):
|
||||
return Response(
|
||||
{"error": "Issue IDs are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||
).select_related("state")
|
||||
bulk_archive_issues = []
|
||||
for issue in issues:
|
||||
if issue.state.group not in ["completed", "cancelled"]:
|
||||
return Response(
|
||||
{
|
||||
"error_code": 4091,
|
||||
"error_message": "INVALID_ARCHIVE_STATE_GROUP"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"archived_at": str(timezone.now().date()),
|
||||
"automation": False,
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = timezone.now().date()
|
||||
bulk_archive_issues.append(issue)
|
||||
Issue.objects.bulk_update(bulk_archive_issues, ["archived_at"])
|
||||
|
||||
return Response(
|
||||
{"archived_at": str(timezone.now().date())},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
@ -1,34 +1,30 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
Max,
|
||||
OuterRef,
|
||||
Prefetch,
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from rest_framework import status
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
@ -49,11 +45,21 @@ from plane.db.models import (
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
|
||||
|
||||
class IssueListEndpoint(BaseAPIView):
|
||||
@ -105,110 +111,28 @@ class IssueListEndpoint(BaseAPIView):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
|
||||
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 = queryset.filter(**filters)
|
||||
# Issue queryset
|
||||
issue_queryset, _ = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# 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")
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# 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)
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if self.fields or self.expand:
|
||||
issues = IssueSerializer(
|
||||
@ -304,33 +228,6 @@ class IssueViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@ -340,116 +237,104 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
# 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")
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# 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(),
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
).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)
|
||||
|
||||
# Only use serializer when expand or fields else return by values
|
||||
if self.expand or self.fields:
|
||||
issues = IssueSerializer(
|
||||
issue_queryset,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data
|
||||
else:
|
||||
issues = issue_queryset.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
@ -481,8 +366,13 @@ class IssueViewSet(BaseViewSet):
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.filter(pk=serializer.data["id"])
|
||||
issue_queryset_grouper(
|
||||
queryset=self.get_queryset().filter(
|
||||
pk=serializer.data["id"]
|
||||
),
|
||||
group_by=None,
|
||||
sub_group_by=None,
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
@ -523,6 +413,33 @@ class IssueViewSet(BaseViewSet):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.filter(pk=pk)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
|
288
apiserver/plane/app/views/issue/bulk_operations.py
Normal file
288
apiserver/plane/app/views/issue/bulk_operations.py
Normal file
@ -0,0 +1,288 @@
|
||||
# Python imports
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
Issue,
|
||||
IssueLabel,
|
||||
IssueAssignee,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
if not len(issue_ids):
|
||||
return Response(
|
||||
{"error": "Issue IDs are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get all the issues
|
||||
issues = (
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||
)
|
||||
.select_related("state")
|
||||
.prefetch_related("labels", "assignees")
|
||||
)
|
||||
# Current epoch
|
||||
epoch = int(timezone.now().timestamp())
|
||||
|
||||
# Project details
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
workspace_id = project.workspace_id
|
||||
|
||||
# Initialize arrays
|
||||
bulk_update_issues = []
|
||||
bulk_issue_activities = []
|
||||
bulk_update_issue_labels = []
|
||||
bulk_update_issue_assignees = []
|
||||
|
||||
properties = request.data.get("properties", {})
|
||||
|
||||
if properties.get("start_date", False) and properties.get("target_date", False):
|
||||
if (
|
||||
datetime.strptime(properties.get("start_date"), "%Y-%m-%d").date()
|
||||
> datetime.strptime(properties.get("target_date"), "%Y-%m-%d").date()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error_code": 4100,
|
||||
"error_message": "INVALID_ISSUE_DATES",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
for issue in issues:
|
||||
|
||||
# Priority
|
||||
if properties.get("priority", False):
|
||||
bulk_issue_activities.append(
|
||||
{
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": json.dumps(
|
||||
{"priority": properties.get("priority")}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{"priority": (issue.priority)}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
issue.priority = properties.get("priority")
|
||||
|
||||
# State
|
||||
if properties.get("state_id", False):
|
||||
bulk_issue_activities.append(
|
||||
{
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": json.dumps(
|
||||
{"state": properties.get("state")}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{"state": str(issue.state_id)}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
issue.state_id = properties.get("state_id")
|
||||
|
||||
# Start date
|
||||
if properties.get("start_date", False):
|
||||
if (
|
||||
issue.target_date
|
||||
and not properties.get("target_date", False)
|
||||
and issue.target_date
|
||||
<= datetime.strptime(
|
||||
properties.get("start_date"), "%Y-%m-%d"
|
||||
).date()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error_code": 4101,
|
||||
"error_message": "INVALID_ISSUE_START_DATE",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
bulk_issue_activities.append(
|
||||
{
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": json.dumps(
|
||||
{"start_date": properties.get("start_date")}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{"start_date": str(issue.start_date)}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
issue.start_date = properties.get("start_date")
|
||||
|
||||
# Target date
|
||||
if properties.get("target_date", False):
|
||||
if (
|
||||
issue.start_date
|
||||
and not properties.get("start_date", False)
|
||||
and issue.start_date
|
||||
>= datetime.strptime(
|
||||
properties.get("target_date"), "%Y-%m-%d"
|
||||
).date()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error_code": 4102,
|
||||
"error_message": "INVALID_ISSUE_TARGET_DATE",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
bulk_issue_activities.append(
|
||||
{
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": json.dumps(
|
||||
{"target_date": properties.get("target_date")}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{"target_date": str(issue.target_date)}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
issue.target_date = properties.get("target_date")
|
||||
|
||||
bulk_update_issues.append(issue)
|
||||
|
||||
# Labels
|
||||
if properties.get("label_ids", []):
|
||||
for label_id in properties.get("label_ids", []):
|
||||
bulk_update_issue_labels.append(
|
||||
IssueLabel(
|
||||
issue=issue,
|
||||
label_id=label_id,
|
||||
created_by=request.user,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
)
|
||||
bulk_issue_activities.append(
|
||||
{
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": json.dumps(
|
||||
{"label_ids": properties.get("label_ids", [])}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{
|
||||
"label_ids": [
|
||||
str(label.id)
|
||||
for label in issue.labels.all()
|
||||
]
|
||||
}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
|
||||
# Assignees
|
||||
if properties.get("assignee_ids", []):
|
||||
for assignee_id in properties.get(
|
||||
"assignee_ids", issue.assignees
|
||||
):
|
||||
bulk_update_issue_assignees.append(
|
||||
IssueAssignee(
|
||||
issue=issue,
|
||||
assignee_id=assignee_id,
|
||||
created_by=request.user,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
)
|
||||
bulk_issue_activities.append(
|
||||
{
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": json.dumps(
|
||||
{
|
||||
"assignee_ids": properties.get(
|
||||
"assignee_ids", []
|
||||
)
|
||||
}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{
|
||||
"assignee_ids": [
|
||||
str(assignee.id)
|
||||
for assignee in issue.assignees.all()
|
||||
]
|
||||
}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
|
||||
# Bulk update all the objects
|
||||
Issue.objects.bulk_update(
|
||||
bulk_update_issues,
|
||||
[
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"state",
|
||||
],
|
||||
batch_size=100,
|
||||
)
|
||||
|
||||
# Create new labels
|
||||
IssueLabel.objects.bulk_create(
|
||||
bulk_update_issue_labels,
|
||||
ignore_conflicts=True,
|
||||
batch_size=100,
|
||||
)
|
||||
|
||||
# Create new assignees
|
||||
IssueAssignee.objects.bulk_create(
|
||||
bulk_update_issue_assignees,
|
||||
ignore_conflicts=True,
|
||||
batch_size=100,
|
||||
)
|
||||
# update the issue activity
|
||||
[
|
||||
issue_activity.delay(**activity)
|
||||
for activity in bulk_issue_activities
|
||||
]
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
@ -6,18 +6,14 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
Max,
|
||||
OuterRef,
|
||||
Prefetch,
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
@ -28,6 +24,7 @@ from django.views.decorators.gzip import gzip_page
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
@ -44,10 +41,17 @@ from plane.db.models import (
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
# Module imports
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
from .. import BaseViewSet
|
||||
|
||||
|
||||
@ -88,153 +92,116 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, 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 = self.get_queryset().filter(**filters)
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# 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")
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# 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(),
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
# group and sub group pagination
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
# Group Paginate
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
).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)
|
||||
|
||||
# Only use serializer when expand else return by values
|
||||
if self.expand or self.fields:
|
||||
issues = IssueSerializer(
|
||||
issue_queryset,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data
|
||||
else:
|
||||
issues = issue_queryset.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
@ -265,12 +232,45 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
issue = (
|
||||
self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
)
|
||||
return Response(
|
||||
IssueSerializer(issue).data, status=status.HTTP_201_CREATED
|
||||
issue_queryset_grouper(
|
||||
queryset=self.get_queryset().filter(
|
||||
pk=serializer.data["id"]
|
||||
),
|
||||
group_by=None,
|
||||
sub_group_by=None,
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
@ -309,6 +309,33 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.filter(pk=pk)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
|
@ -1,37 +1,50 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
from django.db.models import (
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
)
|
||||
|
||||
# Django Imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import F, OuterRef, Func, Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import Value, UUIDField
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
ModuleIssueSerializer,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
ModuleIssueSerializer,
|
||||
IssueSerializer,
|
||||
)
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
ModuleIssue,
|
||||
Project,
|
||||
Issue,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
class ModuleIssueViewSet(BaseViewSet):
|
||||
serializer_class = ModuleIssueSerializer
|
||||
@ -80,82 +93,115 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id, module_id):
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
if self.fields or self.expand:
|
||||
issues = IssueSerializer(
|
||||
issue_queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
else:
|
||||
issues = issue_queryset.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
order_by_param = request.GET.get("order_by", "created_at")
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
# group and sub group pagination
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
# Group Paginate
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
|
||||
# create multiple issues inside a module
|
||||
def create_module_issues(self, request, slug, project_id, module_id):
|
||||
|
@ -1,26 +1,27 @@
|
||||
# Django imports
|
||||
from django.db.models import Q, OuterRef, Exists
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
# Module imports
|
||||
from ..base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import (
|
||||
Notification,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
Issue,
|
||||
WorkspaceMember,
|
||||
UserNotificationPreference,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
NotificationSerializer,
|
||||
UserNotificationPreferenceSerializer,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
Notification,
|
||||
UserNotificationPreference,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
# Module imports
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
|
||||
|
||||
class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
@ -131,6 +132,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
"cursor", False
|
||||
):
|
||||
return self.paginate(
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=(notifications),
|
||||
on_results=lambda notifications: NotificationSerializer(
|
||||
|
@ -1,26 +1,25 @@
|
||||
# Python imports
|
||||
import boto3
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import (
|
||||
Prefetch,
|
||||
Q,
|
||||
Exists,
|
||||
OuterRef,
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Prefetch,
|
||||
Q,
|
||||
Subquery,
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
@ -35,20 +34,19 @@ from plane.app.permissions import (
|
||||
ProjectBasePermission,
|
||||
ProjectMemberPermission,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
Workspace,
|
||||
State,
|
||||
UserFavorite,
|
||||
ProjectIdentifier,
|
||||
Module,
|
||||
Cycle,
|
||||
Inbox,
|
||||
DeployBoard,
|
||||
IssueProperty,
|
||||
Issue,
|
||||
Module,
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
State,
|
||||
Workspace,
|
||||
)
|
||||
from plane.utils.cache import cache_response
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
@ -168,6 +166,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
"cursor", False
|
||||
):
|
||||
return self.paginate(
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=(projects),
|
||||
on_results=lambda projects: ProjectListSerializer(
|
||||
|
@ -250,6 +250,7 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
).select_related("actor", "workspace", "issue", "project")
|
||||
|
||||
return self.paginate(
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=queryset,
|
||||
on_results=lambda issue_activities: IssueActivitySerializer(
|
||||
|
@ -1,47 +1,56 @@
|
||||
# Django imports
|
||||
from django.db.models import (
|
||||
Q,
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Case,
|
||||
Value,
|
||||
CharField,
|
||||
When,
|
||||
Exists,
|
||||
Max,
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import UUIDField
|
||||
from django.db.models import (
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from rest_framework import status
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
WorkspaceEntityPermission,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueViewSerializer,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueView,
|
||||
Workspace,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
IssueViewSerializer,
|
||||
IssueSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
IssueView,
|
||||
Issue,
|
||||
UserFavorite,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
|
||||
class GlobalViewViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewSerializer
|
||||
@ -143,17 +152,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug):
|
||||
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 = (
|
||||
@ -162,103 +160,107 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
)
|
||||
|
||||
# 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")
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# 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)
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
if self.fields:
|
||||
issues = IssueSerializer(
|
||||
issue_queryset, many=True, fields=self.fields
|
||||
).data
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
# group and sub group pagination
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=None,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=None,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
# Group Paginate
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=None,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
issues = issue_queryset.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueViewViewSet(BaseViewSet):
|
||||
|
@ -1,61 +1,66 @@
|
||||
# Python imports
|
||||
from datetime import date
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import (
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Q,
|
||||
Count,
|
||||
Case,
|
||||
Value,
|
||||
CharField,
|
||||
When,
|
||||
Max,
|
||||
Count,
|
||||
F,
|
||||
Func,
|
||||
IntegerField,
|
||||
UUIDField,
|
||||
OuterRef,
|
||||
Q,
|
||||
Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import ExtractWeek, Cast
|
||||
from django.db.models.fields import DateField
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.functions import Cast, ExtractWeek
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party modules
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.serializers import (
|
||||
WorkSpaceSerializer,
|
||||
ProjectMemberSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueSerializer,
|
||||
WorkspaceUserPropertiesSerializer,
|
||||
)
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Workspace,
|
||||
ProjectMember,
|
||||
IssueActivity,
|
||||
Issue,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
WorkspaceUserProperties,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
WorkspaceViewerPermission,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from plane.app.serializers import (
|
||||
IssueActivitySerializer,
|
||||
ProjectMemberSerializer,
|
||||
WorkSpaceSerializer,
|
||||
WorkspaceUserPropertiesSerializer,
|
||||
)
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
CycleIssue,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
User,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
WorkspaceUserProperties,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
|
||||
|
||||
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
||||
@ -99,22 +104,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, slug, user_id):
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
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",
|
||||
]
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
issue_queryset = (
|
||||
@ -152,100 +143,103 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.order_by("created_at")
|
||||
).distinct()
|
||||
|
||||
# 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")
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# 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(),
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
).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 = IssueSerializer(
|
||||
issue_queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||
@ -397,6 +391,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
queryset = queryset.filter(project__in=projects)
|
||||
|
||||
return self.paginate(
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=queryset,
|
||||
on_results=lambda issue_activities: IssueActivitySerializer(
|
||||
|
@ -5,6 +5,7 @@ import logging
|
||||
from celery import shared_task
|
||||
|
||||
# Django imports
|
||||
# Third party imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
@ -1,46 +1,29 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Exists, F, Func, OuterRef, Q, Prefetch
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import (
|
||||
Prefetch,
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Q,
|
||||
Case,
|
||||
Value,
|
||||
CharField,
|
||||
When,
|
||||
Exists,
|
||||
Max,
|
||||
IntegerField,
|
||||
)
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
IssueCommentSerializer,
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.serializers import (
|
||||
CommentReactionSerializer,
|
||||
IssueCommentSerializer,
|
||||
IssuePublicSerializer,
|
||||
IssueReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueComment,
|
||||
Label,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
State,
|
||||
ProjectMember,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
@ -49,8 +32,20 @@ from plane.db.models import (
|
||||
ProjectPublicMember,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
|
||||
|
||||
class IssueCommentPublicViewSet(BaseViewSet):
|
||||
@ -535,17 +530,10 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
anchor=anchor, entity_name="project"
|
||||
)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
project_id = project_deploy_board.entity_identifier
|
||||
slug = project_deploy_board.workspace.slug
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
@ -576,7 +564,6 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
)
|
||||
.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()
|
||||
@ -591,113 +578,118 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
).distinct()
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# 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")
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# 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(),
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
# group and sub group pagination
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
# Group Paginate
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
).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 = IssuePublicSerializer(issue_queryset, many=True).data
|
||||
|
||||
state_group_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
states = (
|
||||
State.objects.filter(
|
||||
~Q(name="Triage"),
|
||||
workspace_id=project_deploy_board.workspace_id,
|
||||
project_id=project_deploy_board.project_id,
|
||||
)
|
||||
.annotate(
|
||||
custom_order=Case(
|
||||
*[
|
||||
When(group=value, then=Value(index))
|
||||
for index, value in enumerate(state_group_order)
|
||||
],
|
||||
default=Value(len(state_group_order)),
|
||||
output_field=IntegerField(),
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
.values("name", "group", "color", "id")
|
||||
.order_by("custom_order", "sequence")
|
||||
)
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace_id=project_deploy_board.workspace_id,
|
||||
project_id=project_deploy_board.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,
|
||||
)
|
||||
|
@ -1,5 +1,9 @@
|
||||
# Python imports
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from sentry_sdk import capture_exception
|
||||
@ -11,6 +15,10 @@ def log_exception(e):
|
||||
logger = logging.getLogger("plane")
|
||||
logger.error(e)
|
||||
|
||||
# Log traceback if running in Debug
|
||||
if settings.DEBUG:
|
||||
logger.error(traceback.format_exc(e))
|
||||
|
||||
# Capture in sentry if configured
|
||||
capture_exception(e)
|
||||
return
|
||||
|
@ -1,240 +1,191 @@
|
||||
def resolve_keys(group_keys, value):
|
||||
"""resolve keys to a key which will be used for
|
||||
grouping
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import Q, UUIDField, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
Args:
|
||||
group_keys (string): key which will be used for grouping
|
||||
value (obj): data value
|
||||
|
||||
Returns:
|
||||
string: the key which will be used for
|
||||
"""
|
||||
keys = group_keys.split(".")
|
||||
for key in keys:
|
||||
value = value.get(key, None)
|
||||
return value
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Issue,
|
||||
Label,
|
||||
Module,
|
||||
Project,
|
||||
ProjectMember,
|
||||
State,
|
||||
WorkspaceMember,
|
||||
)
|
||||
|
||||
|
||||
def group_results(results_data, group_by, sub_group_by=False):
|
||||
"""group results data into certain group_by
|
||||
def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||
|
||||
Args:
|
||||
results_data (obj): complete results data
|
||||
group_by (key): string
|
||||
FIELD_MAPPER = {
|
||||
"label_ids": "labels__id",
|
||||
"assignee_ids": "assignees__id",
|
||||
"module_ids": "issue_module__module_id",
|
||||
}
|
||||
|
||||
Returns:
|
||||
obj: grouped results
|
||||
"""
|
||||
if sub_group_by:
|
||||
main_responsive_dict = dict()
|
||||
annotations_map = {
|
||||
"assignee_ids": ("assignees__id", ~Q(assignees__id__isnull=True)),
|
||||
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
|
||||
"module_ids": (
|
||||
"issue_module__module_id",
|
||||
~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
}
|
||||
default_annotations = {
|
||||
key: Coalesce(
|
||||
ArrayAgg(
|
||||
field,
|
||||
distinct=True,
|
||||
filter=condition,
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
for key, (field, condition) in annotations_map.items()
|
||||
if FIELD_MAPPER.get(key) != group_by
|
||||
or FIELD_MAPPER.get(key) != sub_group_by
|
||||
}
|
||||
|
||||
if sub_group_by == "priority":
|
||||
main_responsive_dict = {
|
||||
"urgent": {},
|
||||
"high": {},
|
||||
"medium": {},
|
||||
"low": {},
|
||||
"none": {},
|
||||
}
|
||||
return queryset.annotate(**default_annotations)
|
||||
|
||||
for value in results_data:
|
||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(main_group_attribute, list) and not isinstance(
|
||||
group_attribute, list
|
||||
):
|
||||
if len(main_group_attribute):
|
||||
for attrib in main_group_attribute:
|
||||
if str(attrib) not in main_responsive_dict:
|
||||
main_responsive_dict[str(attrib)] = {}
|
||||
if (
|
||||
str(group_attribute)
|
||||
in main_responsive_dict[str(attrib)]
|
||||
):
|
||||
main_responsive_dict[str(attrib)][
|
||||
str(group_attribute)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(attrib)][
|
||||
str(group_attribute)
|
||||
] = []
|
||||
main_responsive_dict[str(attrib)][
|
||||
str(group_attribute)
|
||||
].append(value)
|
||||
else:
|
||||
if str(None) not in main_responsive_dict:
|
||||
main_responsive_dict[str(None)] = {}
|
||||
|
||||
if str(group_attribute) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][
|
||||
str(group_attribute)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][
|
||||
str(group_attribute)
|
||||
] = []
|
||||
main_responsive_dict[str(None)][
|
||||
str(group_attribute)
|
||||
].append(value)
|
||||
def issue_on_results(issues, group_by, sub_group_by):
|
||||
|
||||
elif isinstance(group_attribute, list) and not isinstance(
|
||||
main_group_attribute, list
|
||||
):
|
||||
if str(main_group_attribute) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_group_attribute)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if (
|
||||
str(attrib)
|
||||
in main_responsive_dict[str(main_group_attribute)]
|
||||
):
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(attrib)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(attrib)
|
||||
] = []
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(attrib)
|
||||
].append(value)
|
||||
else:
|
||||
if (
|
||||
str(None)
|
||||
in main_responsive_dict[str(main_group_attribute)]
|
||||
):
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(None)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(None)
|
||||
] = []
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(None)
|
||||
].append(value)
|
||||
FIELD_MAPPER = {
|
||||
"labels__id": "label_ids",
|
||||
"assignees__id": "assignee_ids",
|
||||
"issue_module__module_id": "module_ids",
|
||||
}
|
||||
|
||||
elif isinstance(group_attribute, list) and isinstance(
|
||||
main_group_attribute, list
|
||||
):
|
||||
if len(main_group_attribute):
|
||||
for main_attrib in main_group_attribute:
|
||||
if str(main_attrib) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_attrib)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if (
|
||||
str(attrib)
|
||||
in main_responsive_dict[str(main_attrib)]
|
||||
):
|
||||
main_responsive_dict[str(main_attrib)][
|
||||
str(attrib)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_attrib)][
|
||||
str(attrib)
|
||||
] = []
|
||||
main_responsive_dict[str(main_attrib)][
|
||||
str(attrib)
|
||||
].append(value)
|
||||
else:
|
||||
if (
|
||||
str(None)
|
||||
in main_responsive_dict[str(main_attrib)]
|
||||
):
|
||||
main_responsive_dict[str(main_attrib)][
|
||||
str(None)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_attrib)][
|
||||
str(None)
|
||||
] = []
|
||||
main_responsive_dict[str(main_attrib)][
|
||||
str(None)
|
||||
].append(value)
|
||||
else:
|
||||
if str(None) not in main_responsive_dict:
|
||||
main_responsive_dict[str(None)] = {}
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][
|
||||
str(attrib)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(None)][
|
||||
str(attrib)
|
||||
] = []
|
||||
main_responsive_dict[str(None)][
|
||||
str(attrib)
|
||||
].append(value)
|
||||
else:
|
||||
if str(None) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(None)].append(
|
||||
value
|
||||
)
|
||||
else:
|
||||
main_responsive_dict[str(None)][str(None)] = []
|
||||
main_responsive_dict[str(None)][str(None)].append(
|
||||
value
|
||||
)
|
||||
else:
|
||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
original_list = ["assignee_ids", "label_ids", "module_ids"]
|
||||
|
||||
if str(main_group_attribute) not in main_responsive_dict:
|
||||
main_responsive_dict[str(main_group_attribute)] = {}
|
||||
required_fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"state__group",
|
||||
]
|
||||
|
||||
if (
|
||||
str(group_attribute)
|
||||
in main_responsive_dict[str(main_group_attribute)]
|
||||
):
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(group_attribute)
|
||||
].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(group_attribute)
|
||||
] = []
|
||||
main_responsive_dict[str(main_group_attribute)][
|
||||
str(group_attribute)
|
||||
].append(value)
|
||||
if group_by in FIELD_MAPPER:
|
||||
original_list.remove(FIELD_MAPPER[group_by])
|
||||
original_list.append(group_by)
|
||||
|
||||
return main_responsive_dict
|
||||
if sub_group_by in FIELD_MAPPER:
|
||||
original_list.remove(FIELD_MAPPER[sub_group_by])
|
||||
original_list.append(sub_group_by)
|
||||
|
||||
else:
|
||||
response_dict = {}
|
||||
required_fields.extend(original_list)
|
||||
return issues.values(*required_fields)
|
||||
|
||||
if group_by == "priority":
|
||||
response_dict = {
|
||||
"urgent": [],
|
||||
"high": [],
|
||||
"medium": [],
|
||||
"low": [],
|
||||
"none": [],
|
||||
}
|
||||
|
||||
for value in results_data:
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(group_attribute, list):
|
||||
if len(group_attribute):
|
||||
for attrib in group_attribute:
|
||||
if str(attrib) in response_dict:
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
response_dict[str(attrib)] = []
|
||||
response_dict[str(attrib)].append(value)
|
||||
else:
|
||||
if str(None) in response_dict:
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
response_dict[str(None)] = []
|
||||
response_dict[str(None)].append(value)
|
||||
else:
|
||||
if str(group_attribute) in response_dict:
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
else:
|
||||
response_dict[str(group_attribute)] = []
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
|
||||
return response_dict
|
||||
def issue_group_values(field, slug, project_id=None, filters=dict):
|
||||
if field == "state_id":
|
||||
queryset = State.objects.filter(
|
||||
~Q(name="Triage"),
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
if field == "labels__id":
|
||||
queryset = Label.objects.filter(workspace__slug=slug).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "assignees__id":
|
||||
if project_id:
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).values_list("member_id", flat=True)
|
||||
else:
|
||||
return list(
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, is_active=True
|
||||
).values_list("member_id", flat=True)
|
||||
)
|
||||
if field == "issue_module__module_id":
|
||||
queryset = Module.objects.filter(
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "cycle_id":
|
||||
queryset = Cycle.objects.filter(
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "project_id":
|
||||
queryset = Project.objects.filter(workspace__slug=slug).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
return list(queryset)
|
||||
if field == "priority":
|
||||
return [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"urgent",
|
||||
"none",
|
||||
]
|
||||
if field == "state__group":
|
||||
return [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
if field == "target_date":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("target_date", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
if field == "start_date":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("start_date", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
return []
|
||||
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
# The date from pattern
|
||||
@ -63,24 +64,27 @@ def date_filter(filter, date_term, queries):
|
||||
"""
|
||||
for query in queries:
|
||||
date_query = query.split(";")
|
||||
if len(date_query) >= 2:
|
||||
match = pattern.match(date_query[0])
|
||||
if match:
|
||||
if len(date_query) == 3:
|
||||
digit, term = date_query[0].split("_")
|
||||
string_date_filter(
|
||||
filter=filter,
|
||||
duration=int(digit),
|
||||
subsequent=date_query[1],
|
||||
term=term,
|
||||
date_filter=date_term,
|
||||
offset=date_query[2],
|
||||
)
|
||||
else:
|
||||
if "after" in date_query:
|
||||
filter[f"{date_term}__gte"] = date_query[0]
|
||||
if date_query:
|
||||
if len(date_query) >= 2:
|
||||
match = pattern.match(date_query[0])
|
||||
if match:
|
||||
if len(date_query) == 3:
|
||||
digit, term = date_query[0].split("_")
|
||||
string_date_filter(
|
||||
filter=filter,
|
||||
duration=int(digit),
|
||||
subsequent=date_query[1],
|
||||
term=term,
|
||||
date_filter=date_term,
|
||||
offset=date_query[2],
|
||||
)
|
||||
else:
|
||||
filter[f"{date_term}__lte"] = date_query[0]
|
||||
if "after" in date_query:
|
||||
filter[f"{date_term}__gte"] = date_query[0]
|
||||
else:
|
||||
filter[f"{date_term}__lte"] = date_query[0]
|
||||
else:
|
||||
filter[f"{date_term}__contains"] = date_query[0]
|
||||
|
||||
|
||||
def filter_state(params, filter, method, prefix=""):
|
||||
|
84
apiserver/plane/utils/order_queryset.py
Normal file
84
apiserver/plane/utils/order_queryset.py
Normal file
@ -0,0 +1,84 @@
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
Min,
|
||||
Value,
|
||||
When,
|
||||
)
|
||||
|
||||
# Custom ordering for priority and state
|
||||
PRIORITY_ORDER = ["urgent", "high", "medium", "low", "none"]
|
||||
STATE_ORDER = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
|
||||
def order_issue_queryset(issue_queryset, order_by_param="-created_at"):
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
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")
|
||||
order_by_param = (
|
||||
"-priority_order"
|
||||
if order_by_param.startswith("-")
|
||||
else "priority_order"
|
||||
)
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__group",
|
||||
"-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")
|
||||
order_by_param = (
|
||||
"-state_order" if order_by_param.startswith("-") else "state_order"
|
||||
)
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"assignees__first_name",
|
||||
"issue_module__module__name",
|
||||
"-labels__name",
|
||||
"-assignees__first_name",
|
||||
"-issue_module__module__name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
min_values=Min(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-min_values" if order_by_param.startswith("-") else "min_values"
|
||||
)
|
||||
order_by_param = (
|
||||
"-min_values" if order_by_param.startswith("-") else "min_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
order_by_param = order_by_param
|
||||
return issue_queryset, order_by_param
|
@ -1,33 +1,49 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import ParseError
|
||||
from collections.abc import Sequence
|
||||
# Python imports
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Count, F, Window
|
||||
from django.db.models.functions import RowNumber
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
|
||||
|
||||
class Cursor:
|
||||
# The cursor value
|
||||
def __init__(self, value, offset=0, is_prev=False, has_results=None):
|
||||
self.value = value
|
||||
self.offset = int(offset)
|
||||
self.is_prev = bool(is_prev)
|
||||
self.has_results = has_results
|
||||
|
||||
# Return the cursor value in string format
|
||||
def __str__(self):
|
||||
return f"{self.value}:{self.offset}:{int(self.is_prev)}"
|
||||
|
||||
# Return the cursor value
|
||||
def __eq__(self, other):
|
||||
return all(
|
||||
getattr(self, attr) == getattr(other, attr)
|
||||
for attr in ("value", "offset", "is_prev", "has_results")
|
||||
)
|
||||
|
||||
# Return the representation of the cursor
|
||||
def __repr__(self):
|
||||
return f"{type(self).__name__,}: value={self.value} offset={self.offset}, is_prev={int(self.is_prev)}"
|
||||
|
||||
# Return if the cursor is true
|
||||
def __bool__(self):
|
||||
return bool(self.has_results)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value):
|
||||
"""Return the cursor value from string format"""
|
||||
try:
|
||||
bits = value.split(":")
|
||||
if len(bits) != 3:
|
||||
@ -50,15 +66,19 @@ class CursorResult(Sequence):
|
||||
self.max_hits = max_hits
|
||||
|
||||
def __len__(self):
|
||||
# Return the length of the results
|
||||
return len(self.results)
|
||||
|
||||
def __iter__(self):
|
||||
# Return the iterator of the results
|
||||
return iter(self.results)
|
||||
|
||||
def __getitem__(self, key):
|
||||
# Return the results based on the key
|
||||
return self.results[key]
|
||||
|
||||
def __repr__(self):
|
||||
# Return the representation of the results
|
||||
return f"<{type(self).__name__}: results={len(self.results)}>"
|
||||
|
||||
|
||||
@ -85,11 +105,14 @@ class OffsetPaginator:
|
||||
max_offset=None,
|
||||
on_results=None,
|
||||
):
|
||||
# Key tuple and remove `-` if descending order by
|
||||
self.key = (
|
||||
order_by
|
||||
if order_by is None or isinstance(order_by, (list, tuple, set))
|
||||
else (order_by,)
|
||||
else (order_by[1::] if order_by.startswith("-") else order_by,)
|
||||
)
|
||||
# Set desc to true when `-` exists in the order by
|
||||
self.desc = True if order_by.startswith("-") else False
|
||||
self.queryset = queryset
|
||||
self.max_limit = max_limit
|
||||
self.max_offset = max_offset
|
||||
@ -101,11 +124,101 @@ class OffsetPaginator:
|
||||
if cursor is None:
|
||||
cursor = Cursor(0, 0, 0)
|
||||
|
||||
# Get the min from limit and max limit
|
||||
limit = min(limit, self.max_limit)
|
||||
|
||||
# queryset
|
||||
queryset = self.queryset
|
||||
if self.key:
|
||||
queryset = queryset.order_by(*self.key)
|
||||
queryset = queryset.order_by(
|
||||
(
|
||||
F(*self.key).desc(nulls_last=True)
|
||||
if self.desc
|
||||
else F(*self.key).asc(nulls_last=True)
|
||||
),
|
||||
"-created_at",
|
||||
)
|
||||
# The current page
|
||||
page = cursor.offset
|
||||
# The offset
|
||||
offset = cursor.offset * cursor.value
|
||||
stop = offset + (cursor.value or limit) + 1
|
||||
|
||||
if self.max_offset is not None and offset >= self.max_offset:
|
||||
raise BadPaginationError("Pagination offset too large")
|
||||
if offset < 0:
|
||||
raise BadPaginationError("Pagination offset cannot be negative")
|
||||
|
||||
results = queryset[offset:stop]
|
||||
|
||||
if cursor.value != limit:
|
||||
results = results[-(limit + 1) :]
|
||||
|
||||
# Adjust cursors based on the results for pagination
|
||||
next_cursor = Cursor(limit, page + 1, False, results.count() > limit)
|
||||
# If the page is greater than 0, then set the previous cursor
|
||||
prev_cursor = Cursor(limit, page - 1, True, page > 0)
|
||||
|
||||
# Process the results
|
||||
results = results[:limit]
|
||||
|
||||
# Process the results
|
||||
if self.on_results:
|
||||
results = self.on_results(results)
|
||||
|
||||
# Count the queryset
|
||||
count = queryset.count()
|
||||
|
||||
# Optionally, calculate the total count and max_hits if needed
|
||||
max_hits = math.ceil(count / limit)
|
||||
|
||||
# Return the cursor results
|
||||
return CursorResult(
|
||||
results=results,
|
||||
next=next_cursor,
|
||||
prev=prev_cursor,
|
||||
hits=count,
|
||||
max_hits=max_hits,
|
||||
)
|
||||
|
||||
def process_results(self, results):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class GroupedOffsetPaginator(OffsetPaginator):
|
||||
|
||||
# Field mappers
|
||||
FIELD_MAPPER = {
|
||||
"labels__id": "label_ids",
|
||||
"assignees__id": "assignee_ids",
|
||||
"modules__id": "module_ids",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queryset,
|
||||
group_by_field_name,
|
||||
group_by_fields,
|
||||
count_filter,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# Initiate the parent class for all the parameters
|
||||
super().__init__(queryset, *args, **kwargs)
|
||||
self.group_by_field_name = group_by_field_name
|
||||
self.group_by_fields = group_by_fields
|
||||
self.count_filter = count_filter
|
||||
|
||||
def get_result(self, limit=50, cursor=None):
|
||||
# offset is page #
|
||||
# value is page limit
|
||||
if cursor is None:
|
||||
cursor = Cursor(0, 0, 0)
|
||||
|
||||
limit = min(limit, self.max_limit)
|
||||
|
||||
# Adjust the initial offset and stop based on the cursor and limit
|
||||
queryset = self.queryset
|
||||
|
||||
page = cursor.offset
|
||||
offset = cursor.offset * cursor.value
|
||||
@ -116,20 +229,73 @@ class OffsetPaginator:
|
||||
if offset < 0:
|
||||
raise BadPaginationError("Pagination offset cannot be negative")
|
||||
|
||||
results = list(queryset[offset:stop])
|
||||
if cursor.value != limit:
|
||||
results = results[-(limit + 1) :]
|
||||
# Compute the results
|
||||
results = {}
|
||||
# Create window for all the groups
|
||||
queryset = queryset.annotate(
|
||||
row_number=Window(
|
||||
expression=RowNumber(),
|
||||
partition_by=[F(self.group_by_field_name)],
|
||||
order_by=(
|
||||
(
|
||||
F(*self.key).desc(
|
||||
nulls_last=True
|
||||
) # order by desc if desc is set
|
||||
if self.desc
|
||||
else F(*self.key).asc(
|
||||
nulls_last=True
|
||||
) # Order by asc if set
|
||||
),
|
||||
F("created_at").desc(),
|
||||
),
|
||||
)
|
||||
)
|
||||
# Filter the results by row number
|
||||
results = queryset.filter(
|
||||
row_number__gt=offset, row_number__lt=stop
|
||||
).order_by(
|
||||
(
|
||||
F(*self.key).desc(nulls_last=True)
|
||||
if self.desc
|
||||
else F(*self.key).asc(nulls_last=True)
|
||||
),
|
||||
F("created_at").desc(),
|
||||
)
|
||||
|
||||
next_cursor = Cursor(limit, page + 1, False, len(results) > limit)
|
||||
prev_cursor = Cursor(limit, page - 1, True, page > 0)
|
||||
|
||||
results = list(results[:limit])
|
||||
if self.on_results:
|
||||
results = self.on_results(results)
|
||||
# Adjust cursors based on the grouped results for pagination
|
||||
next_cursor = Cursor(
|
||||
limit,
|
||||
page + 1,
|
||||
False,
|
||||
queryset.filter(row_number__gte=stop).exists(),
|
||||
)
|
||||
prev_cursor = Cursor(
|
||||
limit,
|
||||
page - 1,
|
||||
True,
|
||||
page > 0,
|
||||
)
|
||||
|
||||
# Count the queryset
|
||||
count = queryset.count()
|
||||
max_hits = math.ceil(count / limit)
|
||||
|
||||
# Optionally, calculate the total count and max_hits if needed
|
||||
# This might require adjustments based on specific use cases
|
||||
if results:
|
||||
max_hits = math.ceil(
|
||||
queryset.values(self.group_by_field_name)
|
||||
.annotate(
|
||||
count=Count(
|
||||
"id",
|
||||
filter=self.count_filter,
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
.order_by("-count")[0]["count"]
|
||||
/ limit
|
||||
)
|
||||
else:
|
||||
max_hits = 0
|
||||
return CursorResult(
|
||||
results=results,
|
||||
next=next_cursor,
|
||||
@ -138,6 +304,393 @@ class OffsetPaginator:
|
||||
max_hits=max_hits,
|
||||
)
|
||||
|
||||
def __get_total_queryset(self):
|
||||
# Get total queryset
|
||||
return (
|
||||
self.queryset.values(self.group_by_field_name)
|
||||
.annotate(
|
||||
count=Count(
|
||||
"id",
|
||||
filter=self.count_filter,
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
.order_by()
|
||||
)
|
||||
|
||||
def __get_total_dict(self):
|
||||
# Convert the total into dictionary of keys as group name and value as the total
|
||||
total_group_dict = {}
|
||||
for group in self.__get_total_queryset():
|
||||
total_group_dict[str(group.get(self.group_by_field_name))] = (
|
||||
total_group_dict.get(
|
||||
str(group.get(self.group_by_field_name)), 0
|
||||
)
|
||||
+ (1 if group.get("count") == 0 else group.get("count"))
|
||||
)
|
||||
|
||||
return total_group_dict
|
||||
|
||||
def __get_field_dict(self):
|
||||
# Create a field dictionary
|
||||
total_group_dict = self.__get_total_dict()
|
||||
return {
|
||||
str(field): {
|
||||
"results": [],
|
||||
"total_results": total_group_dict.get(str(field), 0),
|
||||
}
|
||||
for field in self.group_by_fields
|
||||
}
|
||||
|
||||
def __result_already_added(self, result, group):
|
||||
# Check if the result is already added then add it
|
||||
for existing_issue in group:
|
||||
if existing_issue["id"] == result["id"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __query_multi_grouper(self, results):
|
||||
# Grouping for m2m values
|
||||
total_group_dict = self.__get_total_dict()
|
||||
|
||||
# Preparing a dict to keep track of group IDs associated with each label ID
|
||||
result_group_mapping = defaultdict(set)
|
||||
# Preparing a dict to group result by group ID
|
||||
grouped_by_field_name = defaultdict(list)
|
||||
|
||||
# Iterate over results to fill the above dictionaries
|
||||
for result in results:
|
||||
result_id = result["id"]
|
||||
group_id = result[self.group_by_field_name]
|
||||
result_group_mapping[str(result_id)].add(str(group_id))
|
||||
|
||||
# Adding group_ids key to each issue and grouping by group_name
|
||||
for result in results:
|
||||
result_id = result["id"]
|
||||
group_ids = list(result_group_mapping[str(result_id)])
|
||||
result[self.FIELD_MAPPER.get(self.group_by_field_name)] = (
|
||||
[] if "None" in group_ids else group_ids
|
||||
)
|
||||
# If a result belongs to multiple groups, add it to each group
|
||||
for group_id in group_ids:
|
||||
if not self.__result_already_added(
|
||||
result, grouped_by_field_name[group_id]
|
||||
):
|
||||
grouped_by_field_name[group_id].append(result)
|
||||
|
||||
# Convert grouped_by_field_name back to a list for each group
|
||||
processed_results = {
|
||||
str(group_id): {
|
||||
"results": issues,
|
||||
"total_results": total_group_dict.get(str(group_id)),
|
||||
}
|
||||
for group_id, issues in grouped_by_field_name.items()
|
||||
}
|
||||
|
||||
return processed_results
|
||||
|
||||
def __query_grouper(self, results):
|
||||
# Grouping for single values
|
||||
processed_results = self.__get_field_dict()
|
||||
for result in results:
|
||||
(
|
||||
print(result["created_at"].date(), result["priority"])
|
||||
if str(result[self.group_by_field_name])
|
||||
== "c88dfd3b-e97e-4948-851b-a5fe1e36ffd0"
|
||||
else None
|
||||
)
|
||||
group_value = str(result.get(self.group_by_field_name))
|
||||
if group_value in processed_results:
|
||||
processed_results[str(group_value)]["results"].append(result)
|
||||
|
||||
return processed_results
|
||||
|
||||
def process_results(self, results):
|
||||
# Process results
|
||||
if results:
|
||||
if self.group_by_field_name in self.FIELD_MAPPER:
|
||||
processed_results = self.__query_multi_grouper(results=results)
|
||||
else:
|
||||
processed_results = self.__query_grouper(results=results)
|
||||
else:
|
||||
processed_results = {}
|
||||
return processed_results
|
||||
|
||||
|
||||
class SubGroupedOffsetPaginator(OffsetPaginator):
|
||||
FIELD_MAPPER = {
|
||||
"labels__id": "label_ids",
|
||||
"assignees__id": "assignee_ids",
|
||||
"modules__id": "module_ids",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queryset,
|
||||
group_by_field_name,
|
||||
sub_group_by_field_name,
|
||||
group_by_fields,
|
||||
sub_group_by_fields,
|
||||
count_filter,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(queryset, *args, **kwargs)
|
||||
self.group_by_field_name = group_by_field_name
|
||||
self.group_by_fields = group_by_fields
|
||||
self.sub_group_by_field_name = sub_group_by_field_name
|
||||
self.sub_group_by_fields = sub_group_by_fields
|
||||
self.count_filter = count_filter
|
||||
|
||||
def get_result(self, limit=30, cursor=None):
|
||||
# offset is page #
|
||||
# value is page limit
|
||||
if cursor is None:
|
||||
cursor = Cursor(0, 0, 0)
|
||||
|
||||
limit = min(limit, self.max_limit)
|
||||
|
||||
# Adjust the initial offset and stop based on the cursor and limit
|
||||
queryset = self.queryset
|
||||
|
||||
page = cursor.offset
|
||||
offset = cursor.offset * cursor.value
|
||||
stop = offset + (cursor.value or limit) + 1
|
||||
|
||||
if self.max_offset is not None and offset >= self.max_offset:
|
||||
raise BadPaginationError("Pagination offset too large")
|
||||
if offset < 0:
|
||||
raise BadPaginationError("Pagination offset cannot be negative")
|
||||
|
||||
# Compute the results
|
||||
results = {}
|
||||
|
||||
# Create windows for group and sub group field name
|
||||
queryset = queryset.annotate(
|
||||
row_number=Window(
|
||||
expression=RowNumber(),
|
||||
partition_by=[
|
||||
F(self.group_by_field_name),
|
||||
F(self.sub_group_by_field_name),
|
||||
],
|
||||
order_by=(
|
||||
(
|
||||
F(*self.key).desc(nulls_last=True)
|
||||
if self.desc
|
||||
else F(*self.key).asc(nulls_last=True)
|
||||
),
|
||||
"-created_at",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Filter the results
|
||||
results = queryset.filter(
|
||||
row_number__gt=offset, row_number__lt=stop
|
||||
).order_by(
|
||||
(
|
||||
F(*self.key).desc(nulls_last=True)
|
||||
if self.desc
|
||||
else F(*self.key).asc(nulls_last=True)
|
||||
),
|
||||
F("created_at").desc(),
|
||||
)
|
||||
|
||||
# Adjust cursors based on the grouped results for pagination
|
||||
next_cursor = Cursor(
|
||||
limit,
|
||||
page + 1,
|
||||
False,
|
||||
queryset.filter(row_number__gte=stop).exists(),
|
||||
)
|
||||
prev_cursor = Cursor(
|
||||
limit,
|
||||
page - 1,
|
||||
True,
|
||||
page > 0,
|
||||
)
|
||||
|
||||
# Count the queryset
|
||||
count = queryset.count()
|
||||
|
||||
# Optionally, calculate the total count and max_hits if needed
|
||||
# This might require adjustments based on specific use cases
|
||||
if results:
|
||||
max_hits = math.ceil(
|
||||
queryset.values(self.group_by_field_name)
|
||||
.annotate(
|
||||
count=Count(
|
||||
"id",
|
||||
filter=self.count_filter,
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
.order_by("-count")[0]["count"]
|
||||
/ limit
|
||||
)
|
||||
else:
|
||||
max_hits = 0
|
||||
return CursorResult(
|
||||
results=results,
|
||||
next=next_cursor,
|
||||
prev=prev_cursor,
|
||||
hits=count,
|
||||
max_hits=max_hits,
|
||||
)
|
||||
|
||||
def __get_group_total_queryset(self):
|
||||
# Get group totals
|
||||
return (
|
||||
self.queryset.order_by(self.group_by_field_name)
|
||||
.values(self.group_by_field_name)
|
||||
.annotate(
|
||||
count=Count(
|
||||
"id",
|
||||
filter=self.count_filter,
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def __get_subgroup_total_queryset(self):
|
||||
# Get subgroup totals
|
||||
return (
|
||||
self.queryset.values(
|
||||
self.group_by_field_name, self.sub_group_by_field_name
|
||||
)
|
||||
.annotate(
|
||||
count=Count("id", filter=self.count_filter, distinct=True)
|
||||
)
|
||||
.order_by()
|
||||
.values(
|
||||
self.group_by_field_name, self.sub_group_by_field_name, "count"
|
||||
)
|
||||
)
|
||||
|
||||
def __get_total_dict(self):
|
||||
# Use the above to convert to dictionary of 2D objects
|
||||
total_group_dict = {}
|
||||
total_sub_group_dict = {}
|
||||
for group in self.__get_group_total_queryset():
|
||||
total_group_dict[str(group.get(self.group_by_field_name))] = (
|
||||
total_group_dict.get(
|
||||
str(group.get(self.group_by_field_name)), 0
|
||||
)
|
||||
+ (1 if group.get("count") == 0 else group.get("count"))
|
||||
)
|
||||
|
||||
# Sub group total values
|
||||
for item in self.__get_subgroup_total_queryset():
|
||||
group = str(item[self.group_by_field_name])
|
||||
subgroup = str(item[self.sub_group_by_field_name])
|
||||
count = item["count"]
|
||||
|
||||
if group not in total_sub_group_dict:
|
||||
total_sub_group_dict[str(group)] = {}
|
||||
|
||||
if subgroup not in total_sub_group_dict[group]:
|
||||
total_sub_group_dict[str(group)][str(subgroup)] = {}
|
||||
|
||||
total_sub_group_dict[group][subgroup] = count
|
||||
|
||||
return total_group_dict, total_sub_group_dict
|
||||
|
||||
def __get_field_dict(self):
|
||||
total_group_dict, total_sub_group_dict = self.__get_total_dict()
|
||||
|
||||
return {
|
||||
str(group): {
|
||||
"results": {
|
||||
str(sub_group): {
|
||||
"results": [],
|
||||
"total_results": total_sub_group_dict.get(
|
||||
str(group)
|
||||
).get(str(sub_group), 0),
|
||||
}
|
||||
for sub_group in total_sub_group_dict.get(str(group), [])
|
||||
},
|
||||
"total_results": total_group_dict.get(str(group), 0),
|
||||
}
|
||||
for group in self.group_by_fields
|
||||
}
|
||||
|
||||
def __query_multi_grouper(self, results):
|
||||
# Multi grouper
|
||||
processed_results = self.__get_field_dict()
|
||||
# Preparing a dict to keep track of group IDs associated with each label ID
|
||||
result_group_mapping = defaultdict(set)
|
||||
result_sub_group_mapping = defaultdict(set)
|
||||
|
||||
# Iterate over results to fill the above dictionaries
|
||||
if self.group_by_field_name in self.FIELD_MAPPER:
|
||||
for result in results:
|
||||
result_id = result["id"]
|
||||
group_id = result[self.group_by_field_name]
|
||||
result_group_mapping[str(result_id)].add(str(group_id))
|
||||
|
||||
# Use the same calculation for the sub group
|
||||
if self.sub_group_by_field_name in self.FIELD_MAPPER:
|
||||
for result in results:
|
||||
result_id = result["id"]
|
||||
sub_group_id = result[self.sub_group_by_field_name]
|
||||
result_sub_group_mapping[str(result_id)].add(str(sub_group_id))
|
||||
|
||||
# Iterate over results
|
||||
for result in results:
|
||||
# Get the group value
|
||||
group_value = str(result.get(self.group_by_field_name))
|
||||
# Get the sub group value
|
||||
sub_group_value = str(result.get(self.sub_group_by_field_name))
|
||||
if (
|
||||
group_value in processed_results
|
||||
and sub_group_value
|
||||
in processed_results[str(group_value)]["results"]
|
||||
):
|
||||
if self.group_by_field_name in self.FIELD_MAPPER:
|
||||
# for multi grouper
|
||||
group_ids = list(result_group_mapping[str(result_id)])
|
||||
result[self.FIELD_MAPPER.get(self.group_by_field_name)] = (
|
||||
[] if "None" in group_ids else group_ids
|
||||
)
|
||||
if self.sub_group_by_field_name in self.FIELD_MAPPER:
|
||||
sub_group_ids = list(result_group_mapping[str(result_id)])
|
||||
# for multi groups
|
||||
result[self.FIELD_MAPPER.get(self.group_by_field_name)] = (
|
||||
[] if "None" in sub_group_ids else sub_group_ids
|
||||
)
|
||||
|
||||
processed_results[str(group_value)]["results"][
|
||||
str(sub_group_value)
|
||||
]["results"].append(result)
|
||||
|
||||
return processed_results
|
||||
|
||||
def __query_grouper(self, results):
|
||||
# Single grouper
|
||||
processed_results = self.__get_field_dict()
|
||||
for result in results:
|
||||
group_value = str(result.get(self.group_by_field_name))
|
||||
sub_group_value = str(result.get(self.sub_group_by_field_name))
|
||||
processed_results[group_value]["results"][sub_group_value][
|
||||
"results"
|
||||
].append(result)
|
||||
|
||||
return processed_results
|
||||
|
||||
def process_results(self, results):
|
||||
if results:
|
||||
if (
|
||||
self.group_by_field_name in self.FIELD_MAPPER
|
||||
or self.sub_group_by_field_name in self.FIELD_MAPPER
|
||||
):
|
||||
processed_results = self.__query_multi_grouper(results=results)
|
||||
else:
|
||||
processed_results = self.__query_grouper(results=results)
|
||||
else:
|
||||
processed_results = {}
|
||||
return processed_results
|
||||
|
||||
|
||||
class BasePaginator:
|
||||
"""BasePaginator class can be inherited by any View to return a paginated view"""
|
||||
@ -171,6 +724,11 @@ class BasePaginator:
|
||||
cursor_cls=Cursor,
|
||||
extra_stats=None,
|
||||
controller=None,
|
||||
group_by_field_name=None,
|
||||
group_by_fields=None,
|
||||
sub_group_by_field_name=None,
|
||||
sub_group_by_fields=None,
|
||||
count_filter=None,
|
||||
**paginator_kwargs,
|
||||
):
|
||||
"""Paginate the request"""
|
||||
@ -178,15 +736,27 @@ class BasePaginator:
|
||||
|
||||
# Convert the cursor value to integer and float from string
|
||||
input_cursor = None
|
||||
if request.GET.get(self.cursor_name):
|
||||
try:
|
||||
input_cursor = cursor_cls.from_string(
|
||||
request.GET.get(self.cursor_name)
|
||||
)
|
||||
except ValueError:
|
||||
raise ParseError(detail="Invalid cursor parameter.")
|
||||
try:
|
||||
input_cursor = cursor_cls.from_string(
|
||||
request.GET.get(self.cursor_name, f"{per_page}:0:0"),
|
||||
)
|
||||
except ValueError:
|
||||
raise ParseError(detail="Invalid cursor parameter.")
|
||||
|
||||
if not paginator:
|
||||
if group_by_field_name:
|
||||
paginator_kwargs["group_by_field_name"] = group_by_field_name
|
||||
paginator_kwargs["group_by_fields"] = group_by_fields
|
||||
paginator_kwargs["count_filter"] = count_filter
|
||||
|
||||
if sub_group_by_field_name:
|
||||
paginator_kwargs["sub_group_by_field_name"] = (
|
||||
sub_group_by_field_name
|
||||
)
|
||||
paginator_kwargs["sub_group_by_fields"] = (
|
||||
sub_group_by_fields
|
||||
)
|
||||
|
||||
paginator = paginator_cls(**paginator_kwargs)
|
||||
|
||||
try:
|
||||
@ -196,12 +766,14 @@ class BasePaginator:
|
||||
except BadPaginationError:
|
||||
raise ParseError(detail="Error in parsing")
|
||||
|
||||
# Serialize result according to the on_result function
|
||||
if on_results:
|
||||
results = on_results(cursor_result.results)
|
||||
else:
|
||||
results = cursor_result.results
|
||||
|
||||
if group_by_field_name:
|
||||
results = paginator.process_results(results=results)
|
||||
|
||||
# Add Manipulation functions to the response
|
||||
if controller is not None:
|
||||
results = controller(results)
|
||||
@ -211,6 +783,9 @@ class BasePaginator:
|
||||
# Return the response
|
||||
response = Response(
|
||||
{
|
||||
"grouped_by": group_by_field_name,
|
||||
"sub_grouped_by": sub_group_by_field_name,
|
||||
"total_count": (cursor_result.hits),
|
||||
"next_cursor": str(cursor_result.next),
|
||||
"prev_cursor": str(cursor_result.prev),
|
||||
"next_page_results": cursor_result.next.has_results,
|
||||
|
@ -60,4 +60,5 @@ zxcvbn==4.4.28
|
||||
# timezone
|
||||
pytz==2024.1
|
||||
# jwt
|
||||
PyJWT==2.8.0
|
||||
PyJWT==2.8.0
|
||||
|
||||
|
27
packages/types/src/issues/base.d.ts
vendored
27
packages/types/src/issues/base.d.ts
vendored
@ -1,3 +1,6 @@
|
||||
import { StateGroup } from "components/states";
|
||||
import { TIssuePriorities } from "../issues";
|
||||
|
||||
// issues
|
||||
export * from "./issue";
|
||||
export * from "./issue_reaction";
|
||||
@ -7,16 +10,30 @@ export * from "./issue_relation";
|
||||
export * from "./issue_sub_issues";
|
||||
export * from "./activity/base";
|
||||
|
||||
export type TLoader = "init-loader" | "mutation" | undefined;
|
||||
export type TLoader = "init-loader" | "mutation" | "pagination" | undefined;
|
||||
|
||||
export type TGroupedIssues = {
|
||||
[group_id: string]: string[];
|
||||
};
|
||||
|
||||
export type TSubGroupedIssues = {
|
||||
[sub_grouped_id: string]: {
|
||||
[group_id: string]: string[];
|
||||
};
|
||||
[sub_grouped_id: string]: TGroupedIssues;
|
||||
};
|
||||
|
||||
export type TUnGroupedIssues = string[];
|
||||
export type TIssues = TGroupedIssues | TSubGroupedIssues;
|
||||
|
||||
export type TPaginationData = {
|
||||
nextCursor: string;
|
||||
prevCursor: string;
|
||||
nextPageResults: boolean;
|
||||
};
|
||||
|
||||
export type TIssuePaginationData = {
|
||||
[group_id: string]: TPaginationData;
|
||||
};
|
||||
|
||||
export type TGroupedIssueCount = {
|
||||
[group_id: string]: number;
|
||||
};
|
||||
|
||||
export type TUnGroupedIssues = string[];
|
59
packages/types/src/issues/issue.d.ts
vendored
59
packages/types/src/issues/issue.d.ts
vendored
@ -4,15 +4,15 @@ import { TIssueLink } from "./issue_link";
|
||||
import { TIssueReaction } from "./issue_reaction";
|
||||
|
||||
// new issue structure types
|
||||
export type TIssue = {
|
||||
|
||||
export type TBaseIssue = {
|
||||
id: string;
|
||||
sequence_id: number;
|
||||
name: string;
|
||||
description_html: string;
|
||||
sort_order: number;
|
||||
|
||||
state_id: string;
|
||||
priority: TIssuePriorities;
|
||||
state_id: string | null;
|
||||
priority: TIssuePriorities | null;
|
||||
label_ids: string[];
|
||||
assignee_ids: string[];
|
||||
estimate_point: string | null;
|
||||
@ -21,7 +21,7 @@ export type TIssue = {
|
||||
attachment_count: number;
|
||||
link_count: number;
|
||||
|
||||
project_id: string;
|
||||
project_id: string | null;
|
||||
parent_id: string | null;
|
||||
cycle_id: string | null;
|
||||
module_ids: string[] | null;
|
||||
@ -37,9 +37,14 @@ export type TIssue = {
|
||||
updated_by: string;
|
||||
|
||||
is_draft: boolean;
|
||||
};
|
||||
|
||||
export type TIssue = TBaseIssue & {
|
||||
description_html?: string;
|
||||
is_subscribed?: boolean;
|
||||
|
||||
parent?: partial<TIssue>;
|
||||
|
||||
issue_reactions?: TIssueReaction[];
|
||||
issue_attachment?: TIssueAttachment[];
|
||||
issue_link?: TIssueLink[];
|
||||
@ -51,3 +56,47 @@ export type TIssue = {
|
||||
export type TIssueMap = {
|
||||
[issue_id: string]: TIssue;
|
||||
};
|
||||
|
||||
type TIssueResponseResults =
|
||||
| TBaseIssue[]
|
||||
| {
|
||||
[key: string]: {
|
||||
results:
|
||||
| TBaseIssue[]
|
||||
| {
|
||||
[key: string]: {
|
||||
results: TBaseIssue[];
|
||||
total_results: number;
|
||||
};
|
||||
};
|
||||
total_results: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TIssuesResponse = {
|
||||
grouped_by: string;
|
||||
next_cursor: string;
|
||||
prev_cursor: string;
|
||||
next_page_results: boolean;
|
||||
prev_page_results: boolean;
|
||||
total_count: number;
|
||||
count: number;
|
||||
total_pages: number;
|
||||
extra_stats: null;
|
||||
results: TIssueResponseResults;
|
||||
}
|
||||
|
||||
export type TBulkIssueProperties = Pick<
|
||||
TIssue,
|
||||
| "state_id"
|
||||
| "priority"
|
||||
| "label_ids"
|
||||
| "assignee_ids"
|
||||
| "start_date"
|
||||
| "target_date"
|
||||
>;
|
||||
|
||||
export type TBulkOperationsPayload = {
|
||||
issue_ids: string[];
|
||||
properties: Partial<TBulkIssueProperties>;
|
||||
};
|
||||
|
2
packages/types/src/users.d.ts
vendored
2
packages/types/src/users.d.ts
vendored
@ -186,6 +186,8 @@ export interface IUserEmailNotificationSettings {
|
||||
issue_completed: boolean;
|
||||
}
|
||||
|
||||
export type TProfileViews = "assigned" | "created" | "subscribed";
|
||||
|
||||
// export interface ICurrentUser {
|
||||
// id: readonly string;
|
||||
// avatar: string;
|
||||
|
28
packages/types/src/view-props.d.ts
vendored
28
packages/types/src/view-props.d.ts
vendored
@ -1,3 +1,5 @@
|
||||
import { EIssueLayoutTypes } from "constants/issue";
|
||||
|
||||
export type TIssueLayouts =
|
||||
| "list"
|
||||
| "kanban"
|
||||
@ -13,9 +15,9 @@ export type TIssueGroupByOptions =
|
||||
| "state_detail.group"
|
||||
| "project"
|
||||
| "assignees"
|
||||
| "mentions"
|
||||
| "cycle"
|
||||
| "module"
|
||||
| "target_date"
|
||||
| null;
|
||||
|
||||
export type TIssueOrderByOptions =
|
||||
@ -32,10 +34,10 @@ export type TIssueOrderByOptions =
|
||||
| "-assignees__first_name"
|
||||
| "labels__name"
|
||||
| "-labels__name"
|
||||
| "modules__name"
|
||||
| "-modules__name"
|
||||
| "cycle__name"
|
||||
| "-cycle__name"
|
||||
| "issue_module__module__name"
|
||||
| "-issue_module__module__name"
|
||||
| "issue_cycle__cycle__name"
|
||||
| "-issue_cycle__cycle__name"
|
||||
| "target_date"
|
||||
| "-target_date"
|
||||
| "estimate_point"
|
||||
@ -72,7 +74,9 @@ export type TIssueParams =
|
||||
| "order_by"
|
||||
| "type"
|
||||
| "sub_issue"
|
||||
| "show_empty_groups";
|
||||
| "show_empty_groups"
|
||||
| "cursor"
|
||||
| "per_page";
|
||||
|
||||
export type TCalendarLayouts = "month" | "week";
|
||||
|
||||
@ -82,9 +86,9 @@ export interface IIssueFilterOptions {
|
||||
created_by?: string[] | null;
|
||||
labels?: string[] | null;
|
||||
priority?: string[] | null;
|
||||
project?: string[] | null;
|
||||
cycle?: string[] | null;
|
||||
module?: string[] | null;
|
||||
project?: string[] | null;
|
||||
start_date?: string[] | null;
|
||||
state?: string[] | null;
|
||||
state_group?: string[] | null;
|
||||
@ -99,7 +103,7 @@ export interface IIssueDisplayFilterOptions {
|
||||
};
|
||||
group_by?: TIssueGroupByOptions;
|
||||
sub_group_by?: TIssueGroupByOptions;
|
||||
layout?: TIssueLayouts;
|
||||
layout?: EIssueLayoutTypes;
|
||||
order_by?: TIssueOrderByOptions;
|
||||
show_empty_groups?: boolean;
|
||||
sub_issue?: boolean;
|
||||
@ -191,3 +195,11 @@ export interface IWorkspaceGlobalViewProps {
|
||||
display_filters: IWorkspaceIssueDisplayFilterOptions | undefined;
|
||||
display_properties: IIssueDisplayProperties;
|
||||
}
|
||||
|
||||
export interface IssuePaginationOptions {
|
||||
canGroup: boolean;
|
||||
perPageCount: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
groupedBy?: TIssueGroupByOptions;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
|
||||
interface IPriorityIcon {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
priority: TIssuePriorities;
|
||||
priority: TIssuePriorities | undefined | null;
|
||||
size?: number;
|
||||
withContainer?: boolean;
|
||||
}
|
||||
@ -31,7 +31,7 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
|
||||
low: SignalLow,
|
||||
none: Ban,
|
||||
};
|
||||
const Icon = icons[priority];
|
||||
const Icon = icons[priority ?? "none"];
|
||||
|
||||
if (!Icon) return null;
|
||||
|
||||
@ -41,7 +41,7 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center border rounded p-0.5 flex-shrink-0",
|
||||
priorityClasses[priority],
|
||||
priorityClasses[priority ?? "none"],
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
|
@ -12,7 +12,7 @@ type Story = StoryObj<typeof Sortable>;
|
||||
|
||||
const data = [
|
||||
{ id: "1", name: "John Doe" },
|
||||
{ id: "2", name: "Jane Doe 2" },
|
||||
{ id: "2", name: "Satish" },
|
||||
{ id: "3", name: "Alice" },
|
||||
{ id: "4", name: "Bob" },
|
||||
{ id: "5", name: "Charlie" },
|
||||
|
24
space/types/project.d.ts
vendored
24
space/types/project.d.ts
vendored
@ -1,11 +1,5 @@
|
||||
import { TLogoProps } from "@plane/types";
|
||||
|
||||
export type TWorkspaceDetails = {
|
||||
name: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TViewDetails = {
|
||||
list: boolean;
|
||||
gantt: boolean;
|
||||
@ -22,21 +16,3 @@ export type TProjectDetails = {
|
||||
logo_props: TLogoProps;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type TProjectSettings = {
|
||||
id: string;
|
||||
anchor: string;
|
||||
comments: boolean;
|
||||
reactions: boolean;
|
||||
votes: boolean;
|
||||
inbox: unknown;
|
||||
workspace: string;
|
||||
workspace_detail: TWorkspaceDetails;
|
||||
project: string;
|
||||
project_details: TProjectDetails;
|
||||
views: TViewDetails;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
24
space/types/publish.d.ts
vendored
Normal file
24
space/types/publish.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
import { IWorkspaceLite } from "@plane/types";
|
||||
import { TProjectDetails, TViewDetails } from "@/types/project";
|
||||
|
||||
export type TPublishEntityType = "project";
|
||||
|
||||
export type TPublishSettings = {
|
||||
anchor: string | undefined;
|
||||
is_comments_enabled: boolean;
|
||||
created_at: string | undefined;
|
||||
created_by: string | undefined;
|
||||
entity_identifier: string | undefined;
|
||||
entity_name: TPublishEntityType | undefined;
|
||||
id: string | undefined;
|
||||
inbox: unknown;
|
||||
project: string | undefined;
|
||||
project_details: TProjectDetails | undefined;
|
||||
is_reactions_enabled: boolean;
|
||||
updated_at: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
view_props: TViewDetails | undefined;
|
||||
is_votes_enabled: boolean;
|
||||
workspace: string | undefined;
|
||||
workspace_detail: IWorkspaceLite | undefined;
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper";
|
||||
import UserProfileHeader from "../header";
|
||||
import ProfileIssuesMobileHeader from "../mobile-header";
|
||||
|
||||
const ProfileHeader = () => {
|
||||
const { profileViewId } = useParams();
|
||||
|
||||
return (
|
||||
<AppHeaderWrapper
|
||||
header={<UserProfileHeader type={profileViewId?.toString()} />}
|
||||
mobileHeader={<ProfileIssuesMobileHeader />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileHeader;
|
@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// components
|
||||
import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper";
|
||||
import UserProfileHeader from "../header";
|
||||
import ProfileIssuesMobileHeader from "../mobile-header";
|
||||
|
||||
const ProfileAssignedHeader = () => (
|
||||
<AppHeaderWrapper header={<UserProfileHeader type="Assigned" />} mobileHeader={<ProfileIssuesMobileHeader />} />
|
||||
);
|
||||
|
||||
export default ProfileAssignedHeader;
|
@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// components
|
||||
import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper";
|
||||
import UserProfileHeader from "../header";
|
||||
import ProfileIssuesMobileHeader from "../mobile-header";
|
||||
|
||||
const ProfileCreatedHeader = () => (
|
||||
<AppHeaderWrapper header={<UserProfileHeader type="Created" />} mobileHeader={<ProfileIssuesMobileHeader />} />
|
||||
);
|
||||
|
||||
export default ProfileCreatedHeader;
|
@ -12,7 +12,13 @@ import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||
ISSUE_LAYOUTS,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
@ -42,7 +48,7 @@ const ProfileIssuesMobileHeader = observer(() => {
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
{ layout: layout },
|
||||
{ layout: layout as EIssueLayoutTypes | undefined },
|
||||
userId.toString()
|
||||
);
|
||||
},
|
||||
|
@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// components
|
||||
import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper";
|
||||
import UserProfileHeader from "../header";
|
||||
import ProfileIssuesMobileHeader from "../mobile-header";
|
||||
|
||||
const ProfileSubscribedHeader = () => (
|
||||
<AppHeaderWrapper header={<UserProfileHeader type="Subscribed" />} mobileHeader={<ProfileIssuesMobileHeader />} />
|
||||
);
|
||||
|
||||
export default ProfileSubscribedHeader;
|
@ -7,7 +7,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||
// icons
|
||||
import { ArrowRight, PanelRight } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
@ -15,7 +15,7 @@ import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@ -70,7 +70,7 @@ const CycleIssuesHeader: React.FC = observer(() => {
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
issues: { issuesCount },
|
||||
issues: { getGroupIssueCount },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { currentProjectCycleIds, getCycleById } = useCycle();
|
||||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
@ -96,7 +96,7 @@ const CycleIssuesHeader: React.FC = observer(() => {
|
||||
};
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId);
|
||||
},
|
||||
@ -147,6 +147,7 @@ const CycleIssuesHeader: React.FC = observer(() => {
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -231,7 +232,13 @@ const CycleIssuesHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 md:flex ">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -5,14 +5,14 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
@ -37,7 +37,7 @@ const CycleIssuesMobileHeader = () => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
|
@ -4,14 +4,14 @@ import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType, EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
@ -56,7 +56,7 @@ const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
);
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
@ -131,7 +131,7 @@ const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban"]}
|
||||
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -6,7 +6,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||
// icons
|
||||
import { Briefcase, Circle, ExternalLink } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
@ -14,7 +14,7 @@ import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType, EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { SPACE_BASE_URL } from "@/helpers/common.helper";
|
||||
@ -44,7 +44,7 @@ const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
} = useMember();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
issues: { issuesCount },
|
||||
issues: { getGroupIssueCount },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
@ -79,7 +79,7 @@ const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
);
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
@ -108,6 +108,7 @@ const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -176,7 +177,12 @@ const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="items-center gap-2 hidden md:flex">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -6,14 +6,14 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
@ -44,7 +44,7 @@ const ProjectIssuesMobileHeader = observer(() => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||
// icons
|
||||
import { ArrowRight, PanelRight } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
@ -15,7 +15,7 @@ import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EIssuesStoreType, EIssueFilterType, EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@ -71,7 +71,7 @@ const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters },
|
||||
issues: { issuesCount },
|
||||
issues: { getGroupIssueCount },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
const { updateFilters } = useIssuesActions(EIssuesStoreType.MODULE);
|
||||
const { projectModuleIds, getModuleById } = useModule();
|
||||
@ -97,11 +97,11 @@ const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!projectId) return;
|
||||
updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
[projectId, moduleId, updateFilters]
|
||||
[projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
@ -122,7 +122,7 @@ const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
updateFilters(projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[projectId, moduleId, issueFilters, updateFilters]
|
||||
[projectId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
@ -130,7 +130,7 @@ const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
if (!projectId) return;
|
||||
updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
|
||||
},
|
||||
[projectId, moduleId, updateFilters]
|
||||
[projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
@ -138,7 +138,7 @@ const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
if (!projectId) return;
|
||||
updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
},
|
||||
[projectId, moduleId, updateFilters]
|
||||
[projectId, updateFilters]
|
||||
);
|
||||
|
||||
// derived values
|
||||
@ -147,6 +147,7 @@ const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -231,7 +232,13 @@ const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="hidden gap-2 md:flex">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -6,14 +6,14 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
@ -46,7 +46,7 @@ const ModuleIssuesMobileHeader = observer(() => {
|
||||
} = useMember();
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
|
||||
},
|
||||
|
@ -4,14 +4,14 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EIssuesStoreType, EIssueFilterType, EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
@ -52,7 +52,7 @@ const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
@ -202,7 +202,13 @@ const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProfileIssuesPage } from "@/components/profile/profile-issues";
|
||||
|
||||
const ProfilePageHeader = {
|
||||
assigned: "Profile - Assigned",
|
||||
created: "Profile - Created",
|
||||
subscribed: "Profile - Subscribed",
|
||||
};
|
||||
|
||||
const ProfileIssuesTypePage = () => {
|
||||
const { profileViewId } = useParams() as { profileViewId: "assigned" | "subscribed" | "created" | undefined };
|
||||
|
||||
if (!profileViewId) return null;
|
||||
|
||||
const header = ProfilePageHeader[profileViewId];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={header} />
|
||||
<ProfileIssuesPage type={profileViewId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileIssuesTypePage;
|
@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProfileIssuesPage } from "@/components/profile/profile-issues";
|
||||
|
||||
const ProfileAssignedIssuesPage = () => (
|
||||
<>
|
||||
<PageHead title="Profile - Assigned" />
|
||||
<ProfileIssuesPage type="assigned" />
|
||||
</>
|
||||
);
|
||||
|
||||
export default ProfileAssignedIssuesPage;
|
@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProfileIssuesPage } from "@/components/profile/profile-issues";
|
||||
|
||||
const ProfileCreatedIssuesPage = () => (
|
||||
<>
|
||||
<PageHead title="Profile - Created" />
|
||||
<ProfileIssuesPage type="created" />
|
||||
</>
|
||||
);
|
||||
|
||||
export default observer(ProfileCreatedIssuesPage);
|
@ -17,7 +17,7 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
|
||||
const { isAuthorized, showProfileIssuesFilter } = props;
|
||||
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
const pathname = usePathname();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// store
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProfileIssuesPage } from "@/components/profile/profile-issues";
|
||||
|
||||
const ProfileSubscribedIssuesPage = () => (
|
||||
<>
|
||||
<PageHead title="Profile - Subscribed" />
|
||||
<ProfileIssuesPage type="subscribed" />
|
||||
</>
|
||||
);
|
||||
|
||||
export default observer(ProfileSubscribedIssuesPage);
|
@ -47,7 +47,7 @@ const ArchivedIssueDetailsPage = observer(() => {
|
||||
|
||||
// derived values
|
||||
const issue = archivedIssueId ? getIssueById(archivedIssueId.toString()) : undefined;
|
||||
const project = issue ? getProjectById(issue?.project_id) : undefined;
|
||||
const project = issue ? getProjectById(issue?.project_id ?? "") : undefined;
|
||||
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
|
||||
// auth
|
||||
const canRestoreIssue = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { AllIssueLayoutRoot } from "@/components/issues";
|
||||
@ -13,7 +12,8 @@ import { useGlobalView, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const GlobalViewIssuesPage = observer(() => {
|
||||
// router
|
||||
const { globalViewId } = useParams();
|
||||
//const { globalViewId } = useParams();
|
||||
const globalViewId = "assigned";
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { getViewDetailsById } = useGlobalView();
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
|
||||
export const BulkDeleteIssuesModalItem: React.FC<any> = observer((props) => {
|
||||
const { issue, delete_issue_ids, identifier } = props;
|
||||
const { getStateById } = useProjectState();
|
||||
interface Props {
|
||||
issue: ISearchIssueResponse;
|
||||
canDeleteIssueIds: boolean;
|
||||
identifier: string | undefined;
|
||||
}
|
||||
|
||||
const color = getStateById(issue.state_id)?.color;
|
||||
export const BulkDeleteIssuesModalItem: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, canDeleteIssueIds, identifier } = props;
|
||||
|
||||
const color = issue.state__color;
|
||||
|
||||
return (
|
||||
<Combobox.Option
|
||||
@ -21,7 +26,7 @@ export const BulkDeleteIssuesModalItem: React.FC<any> = observer((props) => {
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" checked={delete_issue_ids} readOnly />
|
||||
<input type="checkbox" checked={canDeleteIssueIds} readOnly />
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
|
@ -1,28 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
import { Search } from "lucide-react";
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { IUser, TIssue } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
//plane
|
||||
import { ISearchIssueResponse, IUser } from "@plane/types";
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
//components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
//constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { PROJECT_ISSUES_LIST } from "@/constants/fetch-keys";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
//hooks
|
||||
import { useIssues, useProject } from "@/hooks/store";
|
||||
import { IssueService } from "@/services/issue";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project";
|
||||
// ui
|
||||
// icons
|
||||
// types
|
||||
// store hooks
|
||||
// components
|
||||
import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item";
|
||||
// constants
|
||||
|
||||
type FormInput = {
|
||||
delete_issue_ids: string[];
|
||||
@ -34,7 +34,7 @@ type Props = {
|
||||
user: IUser | undefined;
|
||||
};
|
||||
|
||||
const issueService = new IssueService();
|
||||
const projectService = new ProjectService();
|
||||
|
||||
export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
@ -47,13 +47,23 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
// fetching project issues.
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId && isOpen ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||
workspaceSlug && projectId && isOpen
|
||||
? () => issueService.getIssues(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const debouncedSearchTerm: string = useDebounce(query, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
|
||||
setIsSearching(true);
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug.toString(), projectId.toString(), {
|
||||
search: debouncedSearchTerm,
|
||||
workspace_search: false,
|
||||
})
|
||||
.then((res: ISearchIssueResponse[]) => setIssues(res))
|
||||
.finally(() => setIsSearching(false));
|
||||
}, [debouncedSearchTerm, isOpen, projectId, workspaceSlug]);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@ -107,14 +117,33 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
||||
|
||||
const projectDetails = getProjectById(projectId as string);
|
||||
|
||||
const filteredIssues: TIssue[] =
|
||||
query === ""
|
||||
? Object.values(issues ?? {})
|
||||
: Object.values(issues ?? {})?.filter(
|
||||
(issue) =>
|
||||
issue.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
`${projectDetails?.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase())
|
||||
) ?? [];
|
||||
const issueList =
|
||||
issues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select issues to delete</h2>
|
||||
)}
|
||||
<ul className="text-sm text-custom-text-200">
|
||||
{issues.map((issue) => (
|
||||
<BulkDeleteIssuesModalItem
|
||||
issue={issue}
|
||||
identifier={projectDetails?.identifier}
|
||||
canDeleteIssueIds={watch("delete_issue_ids").includes(issue.id)}
|
||||
key={issue.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<EmptyState
|
||||
type={
|
||||
query === "" ? EmptyStateType.ISSUE_RELATION_EMPTY_STATE : EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
|
||||
}
|
||||
layout="screen-simple"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||
@ -160,40 +189,20 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">
|
||||
Select issues to delete
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-custom-text-200">
|
||||
{filteredIssues.map((issue) => (
|
||||
<BulkDeleteIssuesModalItem
|
||||
issue={issue}
|
||||
identifier={projectDetails?.identifier}
|
||||
delete_issue_ids={watch("delete_issue_ids").includes(issue.id)}
|
||||
key={issue.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
{isSearching ? (
|
||||
<Loader className="space-y-3 p-3">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<EmptyState
|
||||
type={
|
||||
query === ""
|
||||
? EmptyStateType.ISSUE_RELATION_EMPTY_STATE
|
||||
: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
|
||||
}
|
||||
layout="screen-simple"
|
||||
/>
|
||||
</div>
|
||||
<>{issueList}</>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
|
||||
{filteredIssues.length > 0 && (
|
||||
{issues.length > 0 && (
|
||||
<div className="flex items-center justify-end gap-2 p-3">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
|
@ -9,9 +9,7 @@ type Props = {
|
||||
children: ReactNode;
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
classNames?: string;
|
||||
alwaysRender?: boolean;
|
||||
placeholderChildren?: ReactNode;
|
||||
pauseHeightUpdateWhileRendering?: boolean;
|
||||
};
|
||||
|
||||
const RenderIfVisible: React.FC<Props> = (props) => {
|
||||
@ -23,15 +21,13 @@ const RenderIfVisible: React.FC<Props> = (props) => {
|
||||
as = "div",
|
||||
children,
|
||||
classNames = "",
|
||||
alwaysRender = false, //render the children even if it is not visible in root
|
||||
placeholderChildren = null, //placeholder children
|
||||
pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained
|
||||
} = props;
|
||||
const [shouldVisible, setShouldVisible] = useState<boolean>(alwaysRender);
|
||||
const [shouldVisible, setShouldVisible] = useState<boolean>();
|
||||
const placeholderHeight = useRef<string>(defaultHeight);
|
||||
const intersectionRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const isVisible = alwaysRender || shouldVisible;
|
||||
const isVisible = shouldVisible;
|
||||
|
||||
// Set visibility with intersection observer
|
||||
useEffect(() => {
|
||||
@ -68,11 +64,10 @@ const RenderIfVisible: React.FC<Props> = (props) => {
|
||||
if (intersectionRef.current && isVisible) {
|
||||
placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
|
||||
}
|
||||
}, [isVisible, intersectionRef, alwaysRender, pauseHeightUpdateWhileRendering]);
|
||||
}, [isVisible, intersectionRef]);
|
||||
|
||||
const child = isVisible ? <>{children}</> : placeholderChildren;
|
||||
const style =
|
||||
isVisible && !pauseHeightUpdateWhileRendering ? {} : { height: placeholderHeight.current, width: "100%" };
|
||||
const style = isVisible ? {} : { height: placeholderHeight.current, width: "100%" };
|
||||
const className = isVisible ? classNames : cn(classNames, "bg-custom-background-80");
|
||||
|
||||
return React.createElement(as, { ref: intersectionRef, style, className }, child);
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
import { FC, Fragment, useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
@ -8,7 +8,7 @@ import { CalendarCheck } from "lucide-react";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// types
|
||||
import { ICycle, TIssue } from "@plane/types";
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
|
||||
// components
|
||||
@ -23,7 +23,8 @@ import { EIssuesStoreType } from "@/constants/issue";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssues, useProject } from "@/hooks/store";
|
||||
import { useIssueDetail, useIssues, useProject } from "@/hooks/store";
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
||||
export type ActiveCycleStatsProps = {
|
||||
@ -37,6 +38,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||
|
||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
|
||||
|
||||
const issuesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [issuesLoaderElement, setIssueLoaderElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const currentValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "Priority-Issues":
|
||||
@ -50,17 +54,29 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||
}
|
||||
};
|
||||
const {
|
||||
issues: { fetchActiveCycleIssues },
|
||||
issues: { getActiveCycleById, fetchActiveCycleIssues, fetchNextActiveCycleIssues },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const { currentProjectDetails } = useProject();
|
||||
|
||||
const { data: activeCycleIssues } = useSWR(
|
||||
useSWR(
|
||||
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null,
|
||||
workspaceSlug && projectId && cycle.id ? () => fetchActiveCycleIssues(workspaceSlug, projectId, cycle.id) : null
|
||||
workspaceSlug && projectId && cycle.id
|
||||
? () => fetchActiveCycleIssues(workspaceSlug, projectId, 30, cycle.id)
|
||||
: null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const cycleIssues = activeCycleIssues ?? [];
|
||||
const cycleIssueDetails = getActiveCycleById(cycle.id);
|
||||
|
||||
const loadMoreIssues = useCallback(() => {
|
||||
fetchNextActiveCycleIssues(workspaceSlug, projectId, cycle.id);
|
||||
}, [workspaceSlug, projectId, cycle.id, issuesLoaderElement, cycleIssueDetails?.nextPageResults]);
|
||||
|
||||
useIntersectionObserver(issuesContainerRef, issuesLoaderElement, loadMoreIssues, `0% 0% 100% 0%`);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden bg-custom-background-100 col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
|
||||
@ -134,53 +150,75 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||
as="div"
|
||||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
||||
>
|
||||
<div className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm">
|
||||
{cycleIssues ? (
|
||||
cycleIssues.length > 0 ? (
|
||||
cycleIssues.map((issue: TIssue) => (
|
||||
<Link
|
||||
key={issue.id}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
className="group flex cursor-pointer items-center justify-between gap-2 rounded-md hover:bg-custom-background-90 p-1"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate">
|
||||
<PriorityIcon priority={issue.priority} withContainer size={12} />
|
||||
<div
|
||||
ref={issuesContainerRef}
|
||||
className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm"
|
||||
>
|
||||
{cycleIssueDetails && cycleIssueDetails.issueIds ? (
|
||||
cycleIssueDetails.issueCount > 0 ? (
|
||||
<>
|
||||
{cycleIssueDetails.issueIds.map((issueId: string) => {
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
|
||||
if (!issue) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={issue.id}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
className="group flex cursor-pointer items-center justify-between gap-2 rounded-md hover:bg-custom-background-90 p-1"
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{currentProjectDetails?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<StateDropdown
|
||||
value={issue.state_id ?? undefined}
|
||||
onChange={() => {}}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled
|
||||
buttonVariant="background-with-text"
|
||||
buttonContainerClassName="cursor-pointer max-w-24"
|
||||
showTooltip
|
||||
/>
|
||||
{issue.target_date && (
|
||||
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}>
|
||||
<div className="h-full flex truncate items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 group-hover:bg-custom-background-100 cursor-pointer">
|
||||
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="text-xs truncate">
|
||||
{renderFormattedDateWithoutYear(issue.target_date)}
|
||||
<div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate">
|
||||
<PriorityIcon priority={issue.priority} withContainer size={12} />
|
||||
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{currentProjectDetails?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
</Tooltip>
|
||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<StateDropdown
|
||||
value={issue.state_id}
|
||||
onChange={() => {}}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled
|
||||
buttonVariant="background-with-text"
|
||||
buttonContainerClassName="cursor-pointer max-w-24"
|
||||
showTooltip
|
||||
/>
|
||||
{issue.target_date && (
|
||||
<Tooltip
|
||||
tooltipHeading="Target Date"
|
||||
tooltipContent={renderFormattedDate(issue.target_date)}
|
||||
>
|
||||
<div className="h-full flex truncate items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 group-hover:bg-custom-background-100 cursor-pointer">
|
||||
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="text-xs truncate">
|
||||
{renderFormattedDateWithoutYear(issue.target_date)}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{(cycleIssueDetails.nextPageResults === undefined || cycleIssueDetails.nextPageResults) && (
|
||||
<div
|
||||
ref={setIssueLoaderElement}
|
||||
className={
|
||||
"h-11 relative flex items-center gap-3 bg-custom-background-80 p-3 text-sm cursor-pointer animate-pulse"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ICycle } from "@plane/types";
|
||||
// hooks
|
||||
import { CycleGanttBlock } from "@/components/cycles";
|
||||
import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "@/components/gantt-chart";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { useCycle } from "@/hooks/store";
|
||||
// components
|
||||
// types
|
||||
// constants
|
||||
import { CycleGanttBlock } from "@/components/cycles";
|
||||
import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar, ChartDataType } from "@/components/gantt-chart";
|
||||
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
||||
// helpers
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@ -23,6 +23,28 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||
// store hooks
|
||||
const { getCycleById, updateCycleDetails } = useCycle();
|
||||
|
||||
const getBlockById = useCallback(
|
||||
(id: string, currentViewData?: ChartDataType | undefined) => {
|
||||
const cycle = getCycleById(id);
|
||||
const block = {
|
||||
data: cycle,
|
||||
id: cycle?.id ?? "",
|
||||
sort_order: cycle?.sort_order ?? 0,
|
||||
start_date: getDate(cycle?.start_date),
|
||||
target_date: getDate(cycle?.end_date),
|
||||
};
|
||||
|
||||
if (currentViewData) {
|
||||
return {
|
||||
...block,
|
||||
position: getMonthChartItemPositionWidthInMonth(currentViewData, block),
|
||||
};
|
||||
}
|
||||
return block;
|
||||
},
|
||||
[getCycleById]
|
||||
);
|
||||
|
||||
const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !cycle) return;
|
||||
|
||||
@ -32,28 +54,13 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||
await updateCycleDetails(workspaceSlug.toString(), cycle.project_id, cycle.id, payload);
|
||||
};
|
||||
|
||||
const blockFormat = (blocks: (ICycle | null)[]) => {
|
||||
if (!blocks) return [];
|
||||
|
||||
const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date);
|
||||
|
||||
const structuredBlocks = filteredBlocks.map((block) => ({
|
||||
data: block,
|
||||
id: block?.id ?? "",
|
||||
sort_order: block?.sort_order ?? 0,
|
||||
start_date: getDate(block?.start_date),
|
||||
target_date: getDate(block?.end_date),
|
||||
}));
|
||||
|
||||
return structuredBlocks;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<GanttChartRoot
|
||||
title="Cycles"
|
||||
loaderTitle="Cycles"
|
||||
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
|
||||
blockIds={cycleIds}
|
||||
getBlockById={getBlockById}
|
||||
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
|
||||
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
|
||||
blockToRender={(data: ICycle) => <CycleGanttBlock cycleId={data.id} />}
|
||||
|
@ -27,7 +27,7 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
|
||||
// derived values
|
||||
const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined;
|
||||
|
||||
if (!issueDetails) return null;
|
||||
if (!issueDetails || !issueDetails.project_id) return null;
|
||||
|
||||
const projectDetails = getProjectById(issueDetails.project_id);
|
||||
|
||||
@ -75,7 +75,7 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
|
||||
// derived values
|
||||
const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined;
|
||||
|
||||
if (!issueDetails) return null;
|
||||
if (!issueDetails || !issueDetails.project_id) return null;
|
||||
|
||||
const projectDetails = getProjectById(issueDetails.project_id);
|
||||
const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? [];
|
||||
@ -122,7 +122,7 @@ export const AssignedCompletedIssueListItem: React.FC<IssueListItemProps> = obse
|
||||
// derived values
|
||||
const issueDetails = getIssueById(issueId);
|
||||
|
||||
if (!issueDetails) return null;
|
||||
if (!issueDetails || !issueDetails.project_id) return null;
|
||||
|
||||
const projectDetails = getProjectById(issueDetails.project_id);
|
||||
|
||||
@ -154,7 +154,7 @@ export const CreatedUpcomingIssueListItem: React.FC<IssueListItemProps> = observ
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return null;
|
||||
if (!issue || !issue.project_id) return null;
|
||||
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
const targetDate = getDate(issue.target_date);
|
||||
@ -205,7 +205,7 @@ export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observe
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return null;
|
||||
if (!issue || !issue.project_id) return null;
|
||||
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
|
||||
@ -257,7 +257,7 @@ export const CreatedCompletedIssueListItem: React.FC<IssueListItemProps> = obser
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return null;
|
||||
if (!issue || !issue.project_id) return null;
|
||||
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
|
||||
|
@ -37,7 +37,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
||||
const { setPeekIssue } = useIssueDetail();
|
||||
|
||||
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||
issue.project_id && setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||
|
||||
const filterParams = getRedirectionFilters(tab);
|
||||
|
||||
|
@ -23,7 +23,7 @@ type Props = TDropdownProps & {
|
||||
dropdownArrowClassName?: string;
|
||||
onChange: (val: string | null) => void;
|
||||
onClose?: () => void;
|
||||
projectId: string;
|
||||
projectId: string | undefined;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
@ -127,7 +127,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
{isOpen && (
|
||||
{isOpen && projectId && (
|
||||
<CycleOptions isOpen={isOpen} projectId={projectId} placement={placement} referenceElement={referenceElement} />
|
||||
)}
|
||||
</Combobox>
|
||||
|
@ -25,8 +25,8 @@ type Props = TDropdownProps & {
|
||||
dropdownArrowClassName?: string;
|
||||
onChange: (val: string | undefined) => void;
|
||||
onClose?: () => void;
|
||||
projectId: string;
|
||||
value: string | undefined;
|
||||
projectId: string | undefined;
|
||||
value: string | undefined | null;
|
||||
};
|
||||
|
||||
type DropdownOptions =
|
||||
@ -120,7 +120,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
const selectedEstimate = value && estimatePointById ? estimatePointById(value) : undefined;
|
||||
|
||||
const onOpen = async () => {
|
||||
if (!currentActiveEstimateId && workspaceSlug) await getProjectEstimates(workspaceSlug, projectId);
|
||||
if (!currentActiveEstimateId && workspaceSlug && projectId) await getProjectEstimates(workspaceSlug, projectId);
|
||||
};
|
||||
|
||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||
|
@ -24,7 +24,7 @@ type Props = TDropdownProps & {
|
||||
button?: ReactNode;
|
||||
dropdownArrow?: boolean;
|
||||
dropdownArrowClassName?: string;
|
||||
projectId: string;
|
||||
projectId: string | undefined;
|
||||
showCount?: boolean;
|
||||
onClose?: () => void;
|
||||
} & (
|
||||
@ -272,7 +272,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
{isOpen && (
|
||||
{isOpen && projectId && (
|
||||
<ModuleOptions
|
||||
isOpen={isOpen}
|
||||
projectId={projectId}
|
||||
|
@ -28,7 +28,7 @@ type Props = TDropdownProps & {
|
||||
highlightUrgent?: boolean;
|
||||
onChange: (val: TIssuePriorities) => void;
|
||||
onClose?: () => void;
|
||||
value: TIssuePriorities | undefined;
|
||||
value: TIssuePriorities | undefined | null;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@ -304,7 +304,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
placement,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
value,
|
||||
value = "none",
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@ -363,8 +363,8 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
|
||||
? BorderButton
|
||||
: BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant)
|
||||
? BackgroundButton
|
||||
: TransparentButton;
|
||||
? BackgroundButton
|
||||
: TransparentButton;
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -408,7 +408,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
<ButtonToRender
|
||||
priority={value}
|
||||
priority={value ?? undefined}
|
||||
className={cn(buttonClassName, {
|
||||
"text-custom-text-200": resolvedTheme?.includes("dark") || resolvedTheme === "custom",
|
||||
})}
|
||||
|
@ -25,9 +25,9 @@ type Props = TDropdownProps & {
|
||||
dropdownArrowClassName?: string;
|
||||
onChange: (val: string) => void;
|
||||
onClose?: () => void;
|
||||
projectId: string;
|
||||
projectId: string | undefined;
|
||||
showDefaultState?: boolean;
|
||||
value: string | undefined;
|
||||
value: string | undefined | null;
|
||||
};
|
||||
|
||||
export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
@ -96,7 +96,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
const selectedState = stateValue ? getStateById(stateValue) : undefined;
|
||||
|
||||
const onOpen = async () => {
|
||||
if (!statesList && workspaceSlug) {
|
||||
if (!statesList && workspaceSlug && projectId) {
|
||||
setStateLoader(true);
|
||||
await fetchProjectStates(workspaceSlug, projectId);
|
||||
setStateLoader(false);
|
||||
|
@ -10,11 +10,12 @@ import { BLOCK_HEIGHT } from "../constants";
|
||||
// components
|
||||
import { ChartAddBlock, ChartDraggable } from "../helpers";
|
||||
import { useGanttChart } from "../hooks";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
|
||||
|
||||
type Props = {
|
||||
block: IGanttBlock;
|
||||
blockId: string;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
showAllBlocks: boolean;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
enableBlockLeftResize: boolean;
|
||||
@ -27,7 +28,9 @@ type Props = {
|
||||
|
||||
export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
block,
|
||||
blockId,
|
||||
getBlockById,
|
||||
showAllBlocks,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
enableBlockLeftResize,
|
||||
@ -38,9 +41,14 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
selectionHelpers,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
const { currentViewData, updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||
const { getIsIssuePeeked } = useIssueDetail();
|
||||
|
||||
const block = getBlockById(blockId, currentViewData);
|
||||
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
|
||||
const handleChartBlockPosition = (
|
||||
@ -73,13 +81,14 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
if (!block.data) return null;
|
||||
|
||||
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
|
||||
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
|
||||
const isBlockHoveredOn = isBlockActive(block.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`block-${block.id}`}
|
||||
className="relative min-w-full w-max"
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
@ -93,7 +102,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
"bg-custom-primary-100/10": isBlockSelected && isBlockHoveredOn,
|
||||
"border border-r-0 border-custom-border-400": isBlockFocused,
|
||||
})}
|
||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||
onMouseEnter={() => updateActiveBlockId(blockId)}
|
||||
onMouseLeave={() => updateActiveBlockId(null)}
|
||||
>
|
||||
{isBlockVisibleOnChart ? (
|
||||
|
@ -4,13 +4,14 @@ import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
// constants
|
||||
import { HEADER_HEIGHT } from "../constants";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
|
||||
// components
|
||||
import { GanttChartBlock } from "./block";
|
||||
|
||||
export type GanttChartBlocksProps = {
|
||||
itemsContainerWidth: number;
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockIds: string[];
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
enableBlockLeftResize: boolean;
|
||||
@ -25,9 +26,10 @@ export type GanttChartBlocksProps = {
|
||||
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||
const {
|
||||
itemsContainerWidth,
|
||||
blocks,
|
||||
blockIds,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
getBlockById,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
@ -45,25 +47,22 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
}}
|
||||
>
|
||||
{blocks?.map((block) => {
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
|
||||
|
||||
return (
|
||||
<GanttChartBlock
|
||||
key={block.id}
|
||||
block={block}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableAddBlock={enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{blockIds?.map((blockId) => (
|
||||
<GanttChartBlock
|
||||
key={blockId}
|
||||
blockId={blockId}
|
||||
getBlockById={getBlockById}
|
||||
showAllBlocks={showAllBlocks}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableAddBlock={enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,11 +6,11 @@ import { VIEWS_LIST } from "@/components/gantt-chart/data";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// types
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
import { IGanttBlock, TGanttViews } from "../types";
|
||||
import { TGanttViews } from "../types";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockIds: string[];
|
||||
fullScreenMode: boolean;
|
||||
handleChartView: (view: TGanttViews) => void;
|
||||
handleToday: () => void;
|
||||
@ -19,14 +19,16 @@ type Props = {
|
||||
};
|
||||
|
||||
export const GanttChartHeader: React.FC<Props> = observer((props) => {
|
||||
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode } = props;
|
||||
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode } = props;
|
||||
// chart hook
|
||||
const { currentView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto text-sm font-medium">{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}</div>
|
||||
<div className="ml-auto text-sm font-medium">
|
||||
{blockIds ? `${blockIds.length} ${loaderTitle}` : "Loading..."}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
@ -6,6 +6,7 @@ import { observer } from "mobx-react";
|
||||
import { MultipleSelectGroup } from "@/components/core";
|
||||
import {
|
||||
BiWeekChartView,
|
||||
ChartDataType,
|
||||
DayChartView,
|
||||
GanttChartBlocksList,
|
||||
GanttChartSidebar,
|
||||
@ -27,17 +28,19 @@ import { GANTT_SELECT_GROUP } from "../constants";
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
|
||||
type Props = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockIds: string[];
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
bottomSpacing: boolean;
|
||||
chartBlocks: IGanttBlock[] | null;
|
||||
enableBlockLeftResize: boolean;
|
||||
enableBlockMove: boolean;
|
||||
enableBlockRightResize: boolean;
|
||||
enableReorder: boolean;
|
||||
enableAddBlock: boolean;
|
||||
enableSelection: boolean;
|
||||
enableAddBlock: boolean;
|
||||
itemsContainerWidth: number;
|
||||
showAllBlocks: boolean;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
@ -48,11 +51,12 @@ type Props = {
|
||||
|
||||
export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blocks,
|
||||
blockIds,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
blockToRender,
|
||||
blockUpdateHandler,
|
||||
bottomSpacing,
|
||||
chartBlocks,
|
||||
enableBlockLeftResize,
|
||||
enableBlockMove,
|
||||
enableBlockRightResize,
|
||||
@ -63,6 +67,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
showAllBlocks,
|
||||
sidebarToRender,
|
||||
title,
|
||||
canLoadMoreBlocks,
|
||||
updateCurrentViewRenderPayload,
|
||||
quickAdd,
|
||||
} = props;
|
||||
@ -116,7 +121,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
<MultipleSelectGroup
|
||||
containerRef={ganttContainerRef}
|
||||
entities={{
|
||||
[GANTT_SELECT_GROUP]: chartBlocks?.map((block) => block.id) ?? [],
|
||||
[GANTT_SELECT_GROUP]: blockIds ?? [],
|
||||
}}
|
||||
disabled
|
||||
>
|
||||
@ -135,33 +140,38 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<GanttChartSidebar
|
||||
blocks={blocks}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
quickAdd={quickAdd}
|
||||
selectionHelpers={helpers}
|
||||
/>
|
||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||
<ActiveChartView />
|
||||
{currentViewData && (
|
||||
<GanttChartBlocksList
|
||||
itemsContainerWidth={itemsContainerWidth}
|
||||
blocks={chartBlocks}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableAddBlock={enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
showAllBlocks={showAllBlocks}
|
||||
selectionHelpers={helpers}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
sidebarToRender={sidebarToRender}
|
||||
title={title}
|
||||
quickAdd={quickAdd}
|
||||
selectionHelpers={helpers}
|
||||
/>
|
||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||
<ActiveChartView />
|
||||
{currentViewData && (
|
||||
<GanttChartBlocksList
|
||||
itemsContainerWidth={itemsContainerWidth}
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableAddBlock={enableAddBlock}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
showAllBlocks={showAllBlocks}
|
||||
selectionHelpers={helpers}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
|
@ -13,17 +13,13 @@ import { currentViewDataWithView } from "../data";
|
||||
// constants
|
||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
||||
import {
|
||||
generateMonthChart,
|
||||
getNumberOfDaysBetweenTwoDatesInMonth,
|
||||
getMonthChartItemPositionWidthInMonth,
|
||||
} from "../views";
|
||||
import { generateMonthChart, getNumberOfDaysBetweenTwoDatesInMonth } from "../views";
|
||||
|
||||
type ChartViewRootProps = {
|
||||
border: boolean;
|
||||
title: string;
|
||||
loaderTitle: string;
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockIds: string[];
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
@ -35,6 +31,9 @@ type ChartViewRootProps = {
|
||||
enableSelection: boolean;
|
||||
bottomSpacing: boolean;
|
||||
showAllBlocks: boolean;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
loadMoreBlocks?: () => void;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
};
|
||||
|
||||
@ -42,11 +41,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
const {
|
||||
border,
|
||||
title,
|
||||
blocks = null,
|
||||
blockIds,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
loaderTitle,
|
||||
blockUpdateHandler,
|
||||
sidebarToRender,
|
||||
blockToRender,
|
||||
canLoadMoreBlocks,
|
||||
enableBlockLeftResize,
|
||||
enableBlockRightResize,
|
||||
enableBlockMove,
|
||||
@ -60,25 +62,10 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
// states
|
||||
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
|
||||
const [fullScreenMode, setFullScreenMode] = useState(false);
|
||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
||||
// hooks
|
||||
const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } =
|
||||
useGanttChart();
|
||||
|
||||
// rendering the block structure
|
||||
const renderBlockStructure = (view: ChartDataType, blocks: IGanttBlock[] | null) =>
|
||||
blocks
|
||||
? blocks.map((block: IGanttBlock) => ({
|
||||
...block,
|
||||
position: getMonthChartItemPositionWidthInMonth(view, block),
|
||||
}))
|
||||
: [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentViewData || !blocks) return;
|
||||
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
|
||||
}, [currentViewData, blocks]);
|
||||
|
||||
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
|
||||
const selectedCurrentView: TGanttViews = view;
|
||||
const selectedCurrentViewData: ChartDataType | undefined =
|
||||
@ -168,7 +155,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
})}
|
||||
>
|
||||
<GanttChartHeader
|
||||
blocks={blocks}
|
||||
blockIds={blockIds}
|
||||
fullScreenMode={fullScreenMode}
|
||||
toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)}
|
||||
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||
@ -176,17 +163,19 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
loaderTitle={loaderTitle}
|
||||
/>
|
||||
<GanttChartMainContent
|
||||
blocks={blocks}
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
blockToRender={blockToRender}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
bottomSpacing={bottomSpacing}
|
||||
chartBlocks={chartBlocks}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableReorder={enableReorder}
|
||||
enableAddBlock={enableAddBlock}
|
||||
enableSelection={enableSelection}
|
||||
enableAddBlock={enableAddBlock}
|
||||
itemsContainerWidth={itemsContainerWidth}
|
||||
showAllBlocks={showAllBlocks}
|
||||
sidebarToRender={sidebarToRender}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
import { ChartDataType, ChartViewRoot, IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
// context
|
||||
import { GanttStoreProvider } from "@/components/gantt-chart/contexts";
|
||||
|
||||
@ -8,11 +8,14 @@ type GanttChartRootProps = {
|
||||
border?: boolean;
|
||||
title: string;
|
||||
loaderTitle: string;
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockIds: string[];
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blockToRender: (data: any) => React.ReactNode;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
enableBlockLeftResize?: boolean;
|
||||
enableBlockRightResize?: boolean;
|
||||
enableBlockMove?: boolean;
|
||||
@ -27,11 +30,14 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
const {
|
||||
border = true,
|
||||
title,
|
||||
blocks,
|
||||
blockIds,
|
||||
loaderTitle = "blocks",
|
||||
blockUpdateHandler,
|
||||
sidebarToRender,
|
||||
blockToRender,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
canLoadMoreBlocks,
|
||||
enableBlockLeftResize = false,
|
||||
enableBlockRightResize = false,
|
||||
enableBlockMove = false,
|
||||
@ -48,7 +54,10 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||
<ChartViewRoot
|
||||
border={border}
|
||||
title={title}
|
||||
blocks={blocks}
|
||||
blockIds={blockIds}
|
||||
getBlockById={getBlockById}
|
||||
loadMoreBlocks={loadMoreBlocks}
|
||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||
loaderTitle={loaderTitle}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
sidebarToRender={sidebarToRender}
|
||||
|
@ -4,7 +4,7 @@ import { MutableRefObject } from "react";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart/types";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { CyclesSidebarBlock } from "./block";
|
||||
@ -13,29 +13,33 @@ import { CyclesSidebarBlock } from "./block";
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
blockIds: string[];
|
||||
enableReorder: boolean;
|
||||
};
|
||||
|
||||
export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
||||
const { blockUpdateHandler, blocks, enableReorder } = props;
|
||||
const { blockUpdateHandler, blockIds, getBlockById, enableReorder } = props;
|
||||
|
||||
const handleOnDrop = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blockIds, getBlockById, blockUpdateHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => (
|
||||
{blockIds ? (
|
||||
blockIds.map((blockId, index) => {
|
||||
const block = getBlockById(blockId);
|
||||
if (!block.start_date || !block.target_date) return null;
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blocks.length - 1}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
@ -48,7 +52,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
))
|
||||
)})
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
|
@ -33,6 +33,8 @@ export const IssuesSidebarBlock = observer((props: Props) => {
|
||||
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
|
||||
if (!block.data) return null;
|
||||
|
||||
const isIssueSelected = selectionHelpers?.getIsEntitySelected(block.id);
|
||||
const isIssueFocused = selectionHelpers?.getIsEntityActive(block.id);
|
||||
const isBlockHoveredOn = isBlockActive(block.id);
|
||||
|
@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { MutableRefObject } from "react";
|
||||
import { RefObject, MutableRefObject, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
||||
// hooks
|
||||
//hooks
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
@ -14,54 +16,81 @@ import { IssuesSidebarBlock } from "./block";
|
||||
|
||||
type Props = {
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
getBlockById: (id: string) => IGanttBlock;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
blockIds: string[];
|
||||
enableReorder: boolean;
|
||||
enableSelection: boolean;
|
||||
showAllBlocks?: boolean;
|
||||
selectionHelpers?: TSelectionHelper;
|
||||
};
|
||||
|
||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
const { blockUpdateHandler, blocks, enableReorder, enableSelection, showAllBlocks = false, selectionHelpers } = props;
|
||||
export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blockUpdateHandler,
|
||||
blockIds,
|
||||
getBlockById,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
loadMoreBlocks,
|
||||
canLoadMoreBlocks,
|
||||
ganttContainerRef,
|
||||
showAllBlocks = false,
|
||||
selectionHelpers
|
||||
} = props;
|
||||
|
||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useIntersectionObserver(ganttContainerRef, intersectionElement, loadMoreBlocks, "50% 0% 50% 0%");
|
||||
|
||||
const handleOnDrop = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blockIds, getBlockById, blockUpdateHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
|
||||
{blockIds ? (
|
||||
<>
|
||||
{blockIds.map((blockId, index) => {
|
||||
const block = getBlockById(blockId);
|
||||
const isBlockVisibleOnSidebar = block?.start_date && block?.target_date;
|
||||
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || (!showAllBlocks && !isBlockVisibleOnSidebar)) return;
|
||||
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blocks.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||
<IssuesSidebarBlock
|
||||
block={block}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
isDragging={isDragging}
|
||||
dragHandleRef={dragHandleRef}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
);
|
||||
})
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{(isDragging: boolean, dragHandleRef: MutableRefObject<HTMLButtonElement | null>) => (
|
||||
<IssuesSidebarBlock
|
||||
block={block}
|
||||
enableReorder={enableReorder}
|
||||
enableSelection={enableSelection}
|
||||
isDragging={isDragging}
|
||||
dragHandleRef={dragHandleRef}
|
||||
selectionHelpers={selectionHelpers}
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
);
|
||||
})}
|
||||
{canLoadMoreBlocks && (
|
||||
<div ref={setIntersectionElement} className="p-2">
|
||||
<div className="flex h-10 md:h-8 w-full items-center justify-between gap-1.5 rounded md:px-1 px-4 py-1.5 bg-custom-background-80 animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
@ -72,4 +101,4 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ import { MutableRefObject } from "react";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
|
||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||
import { handleOrderChange } from "../utils";
|
||||
import { ModulesSidebarBlock } from "./block";
|
||||
@ -13,29 +13,32 @@ import { ModulesSidebarBlock } from "./block";
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
blockIds: string[];
|
||||
enableReorder: boolean;
|
||||
};
|
||||
|
||||
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
||||
const { blockUpdateHandler, blocks, enableReorder } = props;
|
||||
const { blockUpdateHandler, blockIds, getBlockById, enableReorder } = props;
|
||||
|
||||
const handleOnDrop = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean
|
||||
) => {
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blocks, blockUpdateHandler);
|
||||
handleOrderChange(draggingBlockId, droppedBlockId, dropAtEndOfList, blockIds, getBlockById, blockUpdateHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => (
|
||||
{blockIds ? (
|
||||
blockIds.map((blockId, index) => {
|
||||
const block = getBlockById(blockId);
|
||||
return (
|
||||
<GanttDnDHOC
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
isLastChild={index === blocks.length - 1}
|
||||
isLastChild={index === blockIds.length - 1}
|
||||
isDragEnabled={enableReorder}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
@ -48,7 +51,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
||||
/>
|
||||
)}
|
||||
</GanttDnDHOC>
|
||||
))
|
||||
)})
|
||||
) : (
|
||||
<Loader className="space-y-3 pr-2">
|
||||
<Loader.Item height="34px" />
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { RefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { MultipleSelectGroupAction } from "@/components/core";
|
||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
@ -10,23 +11,31 @@ import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { GANTT_SELECT_GROUP, HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
||||
|
||||
type Props = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
blockIds: string[];
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
canLoadMoreBlocks?: boolean;
|
||||
loadMoreBlocks?: () => void;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
enableReorder: boolean;
|
||||
enableSelection: boolean;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
};
|
||||
|
||||
export const GanttChartSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
blocks,
|
||||
blockIds,
|
||||
blockUpdateHandler,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
sidebarToRender,
|
||||
getBlockById,
|
||||
loadMoreBlocks,
|
||||
canLoadMoreBlocks,
|
||||
ganttContainerRef,
|
||||
title,
|
||||
quickAdd,
|
||||
selectionHelpers,
|
||||
@ -74,7 +83,19 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div className="min-h-full h-max bg-custom-background-100 overflow-hidden">
|
||||
{sidebarToRender?.({ title, blockUpdateHandler, blocks, enableReorder, enableSelection, selectionHelpers })}
|
||||
{sidebarToRender &&
|
||||
sidebarToRender({
|
||||
title,
|
||||
blockUpdateHandler,
|
||||
blockIds,
|
||||
getBlockById,
|
||||
enableReorder,
|
||||
enableSelection,
|
||||
canLoadMoreBlocks,
|
||||
ganttContainerRef,
|
||||
loadMoreBlocks,
|
||||
selectionHelpers
|
||||
})}
|
||||
</div>
|
||||
{quickAdd ? quickAdd : null}
|
||||
</div>
|
||||
|
@ -1,38 +1,38 @@
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
|
||||
|
||||
export const handleOrderChange = (
|
||||
draggingBlockId: string | undefined,
|
||||
droppedBlockId: string | undefined,
|
||||
dropAtEndOfList: boolean,
|
||||
blocks: IGanttBlock[] | null,
|
||||
blockIds: string[] | null,
|
||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock,
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void
|
||||
) => {
|
||||
if (!blocks || !draggingBlockId || !droppedBlockId) return;
|
||||
if (!blockIds || !draggingBlockId || !droppedBlockId) return;
|
||||
|
||||
const sourceBlockIndex = blocks.findIndex((block) => block.id === draggingBlockId);
|
||||
const destinationBlockIndex = dropAtEndOfList
|
||||
? blocks.length
|
||||
: blocks.findIndex((block) => block.id === droppedBlockId);
|
||||
const sourceBlockIndex = blockIds.findIndex((id) => id === draggingBlockId);
|
||||
const destinationBlockIndex = dropAtEndOfList ? blockIds.length : blockIds.findIndex((id) => id === droppedBlockId);
|
||||
|
||||
// return if dropped outside the list
|
||||
if (sourceBlockIndex === -1 || destinationBlockIndex === -1 || sourceBlockIndex === destinationBlockIndex) return;
|
||||
|
||||
let updatedSortOrder = blocks[sourceBlockIndex].sort_order;
|
||||
let updatedSortOrder = getBlockById(blockIds[sourceBlockIndex])?.sort_order;
|
||||
|
||||
// update the sort order to the lowest if dropped at the top
|
||||
if (destinationBlockIndex === 0) updatedSortOrder = blocks[0].sort_order - 1000;
|
||||
if (destinationBlockIndex === 0) updatedSortOrder = getBlockById(blockIds[0])?.sort_order - 1000;
|
||||
// update the sort order to the highest if dropped at the bottom
|
||||
else if (destinationBlockIndex === blocks.length) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
|
||||
else if (destinationBlockIndex === blockIds.length)
|
||||
updatedSortOrder = getBlockById(blockIds[blockIds.length - 1])?.sort_order + 1000;
|
||||
// update the sort order to the average of the two adjacent blocks if dropped in between
|
||||
else {
|
||||
const destinationSortingOrder = blocks[destinationBlockIndex].sort_order;
|
||||
const relativeDestinationSortingOrder = blocks[destinationBlockIndex - 1].sort_order;
|
||||
const destinationSortingOrder = getBlockById(blockIds[destinationBlockIndex])?.sort_order;
|
||||
const relativeDestinationSortingOrder = getBlockById(blockIds[destinationBlockIndex - 1])?.sort_order;
|
||||
|
||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||
}
|
||||
|
||||
// call the block update handler with the updated sort order, new and old index
|
||||
blockUpdateHandler(blocks[sourceBlockIndex].data, {
|
||||
blockUpdateHandler(getBlockById(blockIds[sourceBlockIndex])?.data, {
|
||||
sort_order: {
|
||||
destinationIndex: destinationBlockIndex,
|
||||
newSortOrder: updatedSortOrder,
|
||||
|
@ -98,7 +98,7 @@ export const InboxIssueContentProperties: React.FC<Props> = observer((props) =>
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || "none"}
|
||||
value={issue?.priority}
|
||||
onChange={(val) =>
|
||||
issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { priority: val })
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
|
||||
{/* state */}
|
||||
<div className="h-7">
|
||||
<StateDropdown
|
||||
value={data?.state_id || ""}
|
||||
value={data?.state_id}
|
||||
onChange={(stateId) => handleData("state_id", stateId)}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
@ -59,7 +59,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
|
||||
{/* priority */}
|
||||
<div className="h-7">
|
||||
<PriorityDropdown
|
||||
value={data?.priority || "none"}
|
||||
value={data?.priority}
|
||||
onChange={(priority) => handleData("priority", priority)}
|
||||
buttonVariant="border-with-text"
|
||||
/>
|
||||
|
@ -1,22 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { Search } from "lucide-react";
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
// icons
|
||||
// components
|
||||
// types
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// services
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { PROJECT_ISSUES_LIST } from "@/constants/fetch-keys";
|
||||
import { useProject, useProjectState } from "@/hooks/store";
|
||||
import { IssueService } from "@/services/issue";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -25,7 +26,7 @@ type Props = {
|
||||
onSubmit: (issueId: string) => void;
|
||||
};
|
||||
|
||||
const issueService = new IssueService();
|
||||
const projectService = new ProjectService();
|
||||
|
||||
export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose, onSubmit, value } = props;
|
||||
@ -35,18 +36,27 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
|
||||
// hooks
|
||||
const { getProjectStates } = useProjectState();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
issueService
|
||||
.getIssues(workspaceSlug as string, projectId as string)
|
||||
.then((res) => Object.values(res ?? {}).filter((issue) => issue.id !== issueId))
|
||||
: null
|
||||
);
|
||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const debouncedSearchTerm: string = useDebounce(query, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
|
||||
setIsSearching(true);
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug.toString(), projectId.toString(), {
|
||||
search: debouncedSearchTerm,
|
||||
workspace_search: false,
|
||||
})
|
||||
.then((res: ISearchIssueResponse[]) => setIssues(res))
|
||||
.finally(() => setIsSearching(false));
|
||||
}, [debouncedSearchTerm, isOpen, projectId, workspaceSlug]);
|
||||
|
||||
const filteredIssues = issues.filter((issue) => issue.id !== issueId);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
@ -62,7 +72,52 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const filteredIssues = (query === "" ? issues : issues?.filter((issue) => issue.name.includes(query))) ?? [];
|
||||
const issueList =
|
||||
filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && <h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select issue</h2>}
|
||||
<ul className="text-sm text-custom-text-100">
|
||||
{filteredIssues.map((issue) => {
|
||||
const stateColor = issue.state__color || "";
|
||||
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="div"
|
||||
value={issue.id}
|
||||
className={({ active, selected }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} `
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: stateColor,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
<span className="text-custom-text-200">{issue.name}</span>
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<EmptyState
|
||||
type={
|
||||
query === "" ? EmptyStateType.ISSUE_RELATION_EMPTY_STATE : EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
|
||||
}
|
||||
layout="screen-simple"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||
@ -110,56 +165,15 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select issue</h2>
|
||||
)}
|
||||
<ul className="text-sm text-custom-text-100">
|
||||
{filteredIssues.map((issue) => {
|
||||
const stateColor =
|
||||
getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)
|
||||
?.color || "";
|
||||
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="div"
|
||||
value={issue.id}
|
||||
className={({ active, selected }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} `
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: stateColor,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
<span className="text-custom-text-200">{issue.name}</span>
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
{isSearching ? (
|
||||
<Loader className="space-y-3 p-3">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<EmptyState
|
||||
type={
|
||||
query === ""
|
||||
? EmptyStateType.ISSUE_RELATION_EMPTY_STATE
|
||||
: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
|
||||
}
|
||||
layout="screen-simple"
|
||||
/>
|
||||
</div>
|
||||
<>{issueList}</>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
|
67
web/components/issues/bulk-operations/actions/archive.tsx
Normal file
67
web/components/issues/bulk-operations/actions/archive.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { ArchiveIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
// constants
|
||||
import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppRouter, useIssueDetail, useProjectState } from "@/hooks/store";
|
||||
import { BulkArchiveConfirmationModal } from "../bulk-archive-modal";
|
||||
|
||||
type Props = {
|
||||
handleClearSelection: () => void;
|
||||
selectedEntityIds: string[];
|
||||
};
|
||||
|
||||
export const BulkArchiveIssues: React.FC<Props> = observer((props) => {
|
||||
const { handleClearSelection, selectedEntityIds } = props;
|
||||
// states
|
||||
const [isBulkArchiveModalOpen, setIsBulkArchiveModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { projectId, workspaceSlug } = useAppRouter();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getStateById } = useProjectState();
|
||||
const canAllIssuesBeArchived = selectedEntityIds.every((issueId) => {
|
||||
const issueDetails = getIssueById(issueId);
|
||||
if (!issueDetails) return false;
|
||||
const stateDetails = getStateById(issueDetails.state_id);
|
||||
if (!stateDetails) return false;
|
||||
return ARCHIVABLE_STATE_GROUPS.includes(stateDetails.group);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectId && workspaceSlug && (
|
||||
<BulkArchiveConfirmationModal
|
||||
isOpen={isBulkArchiveModalOpen}
|
||||
handleClose={() => setIsBulkArchiveModalOpen(false)}
|
||||
issueIds={selectedEntityIds}
|
||||
onSubmit={handleClearSelection}
|
||||
projectId={projectId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading="Archive"
|
||||
tooltipContent={canAllIssuesBeArchived ? "" : "The selected issues are not in the right state group to archive"}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("outline-none grid place-items-center", {
|
||||
"cursor-not-allowed text-custom-text-400": !canAllIssuesBeArchived,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (canAllIssuesBeArchived) setIsBulkArchiveModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<ArchiveIcon className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
});
|
45
web/components/issues/bulk-operations/actions/delete.tsx
Normal file
45
web/components/issues/bulk-operations/actions/delete.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/store";
|
||||
import { BulkDeleteConfirmationModal } from "../bulk-delete-modal";
|
||||
|
||||
type Props = {
|
||||
handleClearSelection: () => void;
|
||||
selectedEntityIds: string[];
|
||||
};
|
||||
|
||||
export const BulkDeleteIssues: React.FC<Props> = observer((props) => {
|
||||
const { handleClearSelection, selectedEntityIds } = props;
|
||||
// states
|
||||
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { projectId, workspaceSlug } = useAppRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectId && workspaceSlug && (
|
||||
<BulkDeleteConfirmationModal
|
||||
isOpen={isBulkDeleteModalOpen}
|
||||
handleClose={() => setIsBulkDeleteModalOpen(false)}
|
||||
issueIds={selectedEntityIds}
|
||||
onSubmit={handleClearSelection}
|
||||
projectId={projectId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
)}
|
||||
<Tooltip tooltipHeading="Delete" tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
className="outline-none grid place-items-center"
|
||||
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
});
|
3
web/components/issues/bulk-operations/actions/index.ts
Normal file
3
web/components/issues/bulk-operations/actions/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./archive";
|
||||
export * from "./delete";
|
||||
export * from "./root";
|
18
web/components/issues/bulk-operations/actions/root.tsx
Normal file
18
web/components/issues/bulk-operations/actions/root.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { BulkArchiveIssues } from "./archive";
|
||||
import { BulkDeleteIssues } from "./delete";
|
||||
|
||||
type Props = {
|
||||
handleClearSelection: () => void;
|
||||
selectedEntityIds: string[];
|
||||
};
|
||||
|
||||
export const BulkOperationsActionsRoot: React.FC<Props> = (props) => {
|
||||
const { handleClearSelection, selectedEntityIds } = props;
|
||||
|
||||
return (
|
||||
<div className="h-7 px-3 flex items-center gap-3 flex-shrink-0">
|
||||
<BulkArchiveIssues handleClearSelection={handleClearSelection} selectedEntityIds={selectedEntityIds} />
|
||||
<BulkDeleteIssues handleClearSelection={handleClearSelection} selectedEntityIds={selectedEntityIds} />
|
||||
</div>
|
||||
);
|
||||
};
|
83
web/components/issues/bulk-operations/bulk-archive-modal.tsx
Normal file
83
web/components/issues/bulk-operations/bulk-archive-modal.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore, EModalPosition, EModalWidth } from "@/components/core";
|
||||
// constants
|
||||
import { EErrorCodes, ERROR_DETAILS } from "@/constants/errors";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
issueIds: string[];
|
||||
onSubmit?: () => void;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const BulkArchiveConfirmationModal: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, isOpen, issueIds, onSubmit, projectId, workspaceSlug } = props;
|
||||
// states
|
||||
const [isArchiving, setIsDeleting] = useState(false);
|
||||
// store hooks
|
||||
const storeType = useIssueStoreType();
|
||||
const {
|
||||
issues: { archiveBulkIssues },
|
||||
} = useIssues(storeType);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
archiveBulkIssues &&
|
||||
(await archiveBulkIssues(workspaceSlug, projectId, issueIds)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issues archived successfully.",
|
||||
});
|
||||
onSubmit?.();
|
||||
handleClose();
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorInfo = ERROR_DETAILS[error?.error_code as EErrorCodes] ?? undefined;
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: errorInfo?.title ?? "Error!",
|
||||
message: errorInfo?.message ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsDeleting(false)));
|
||||
};
|
||||
|
||||
const issueVariant = issueIds.length > 1 ? "issues" : "issue";
|
||||
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleSubmit}
|
||||
isSubmitting={isArchiving}
|
||||
isOpen={isOpen}
|
||||
variant="primary"
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.XL}
|
||||
title={`Archive ${issueVariant}`}
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to archive {issueIds.length} {issueVariant}? Sub issues of selected {issueVariant} will
|
||||
also be archived. Once archived {issueIds.length > 1 ? "they" : "it"} can be restored later via the archives
|
||||
section.
|
||||
</>
|
||||
}
|
||||
primaryButtonText={{
|
||||
loading: "Archiving",
|
||||
default: `Archive ${issueVariant}`,
|
||||
}}
|
||||
hideIcon
|
||||
/>
|
||||
);
|
||||
});
|
79
web/components/issues/bulk-operations/bulk-delete-modal.tsx
Normal file
79
web/components/issues/bulk-operations/bulk-delete-modal.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore, EModalPosition, EModalWidth } from "@/components/core";
|
||||
// constants
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
issueIds: string[];
|
||||
onSubmit?: () => void;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const BulkDeleteConfirmationModal: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, isOpen, issueIds, onSubmit, projectId, workspaceSlug } = props;
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// store hooks
|
||||
const storeType = useIssueStoreType();
|
||||
const {
|
||||
issues: { removeBulkIssues },
|
||||
} = useIssues(storeType);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
await removeBulkIssues(workspaceSlug, projectId, issueIds)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issues deleted successfully.",
|
||||
});
|
||||
onSubmit?.();
|
||||
handleClose();
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsDeleting(false));
|
||||
};
|
||||
|
||||
const issueVariant = issueIds.length > 1 ? "issues" : "issue";
|
||||
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleSubmit}
|
||||
isSubmitting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
variant="danger"
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.XL}
|
||||
title={`Delete ${issueVariant}`}
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete {issueIds.length} {issueVariant}? Sub issues of selected {issueVariant} will
|
||||
also be deleted. All of the data related to the {issueVariant} will be permanently removed. This action cannot
|
||||
be undone.
|
||||
</>
|
||||
}
|
||||
primaryButtonText={{
|
||||
loading: "Deleting",
|
||||
default: `Delete ${issueVariant}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
@ -0,0 +1 @@
|
||||
export const BulkOperationsExtraProperties = () => null;
|
@ -50,7 +50,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
||||
|
||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||
if (!issue) return <></>;
|
||||
if (!issue || !issue.project_id) return <></>;
|
||||
|
||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||
|
||||
|
@ -27,7 +27,7 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = observer((props) =>
|
||||
? `ISSUE_PARENT_CHILD_ISSUES_${workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
|
||||
: null,
|
||||
parentIssue && parentIssue.project_id
|
||||
? () => fetchSubIssues(workspaceSlug, parentIssue.project_id, parentIssue.id)
|
||||
? () => fetchSubIssues(workspaceSlug, parentIssue.project_id!, parentIssue.id)
|
||||
: null
|
||||
);
|
||||
|
||||
|
@ -205,7 +205,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<span>State</span>
|
||||
</div>
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
value={issue?.state_id}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!isEditable}
|
||||
@ -234,7 +234,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
|
||||
issue?.assignee_ids?.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
hideIcon={issue.assignee_ids?.length === 0}
|
||||
dropdownArrow
|
||||
@ -248,7 +248,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
value={issue?.priority}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="border-with-text"
|
||||
|
@ -1,18 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { FC, useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useParams } from "next/navigation";
|
||||
import { TGroupedIssues } from "@plane/types";
|
||||
// components
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { CalendarChart } from "@/components/issues";
|
||||
// hooks
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
//constants
|
||||
import { EIssuesStoreType, EIssueGroupByToServerOptions } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { useIssues, useUser } from "@/hooks/store";
|
||||
// hooks
|
||||
import { useIssues, useUser, useCalendarView } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
// ui
|
||||
// types
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { handleDragDrop } from "./utils";
|
||||
@ -25,25 +26,36 @@ type CalendarStoreType =
|
||||
|
||||
interface IBaseCalendarRoot {
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
storeType: CalendarStoreType;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
isCompletedCycle?: boolean;
|
||||
viewId?: string | undefined;
|
||||
}
|
||||
|
||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
const { QuickActions, storeType, addIssuesToView, viewId, isCompletedCycle = false } = props;
|
||||
const { QuickActions, addIssuesToView, isCompletedCycle = false, viewId } = props;
|
||||
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
|
||||
// hooks
|
||||
const storeType = useIssueStoreType() as CalendarStoreType;
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { issues, issuesFilter, issueMap } = useIssues(storeType);
|
||||
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } =
|
||||
useIssuesActions(storeType);
|
||||
const {
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
quickAddIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
removeIssueFromView,
|
||||
archiveIssue,
|
||||
restoreIssue,
|
||||
updateFilters,
|
||||
} = useIssuesActions(storeType);
|
||||
|
||||
const issueCalendarView = useCalendarView();
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
@ -51,6 +63,26 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
|
||||
const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues;
|
||||
|
||||
const layout = displayFilters?.calendar?.layout ?? "month";
|
||||
const { startDate, endDate } = issueCalendarView.getStartAndEndDate(layout) ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
startDate &&
|
||||
endDate &&
|
||||
layout &&
|
||||
fetchIssues(
|
||||
"init-loader",
|
||||
{
|
||||
canGroup: true,
|
||||
perPageCount: layout === "month" ? 4 : 30,
|
||||
before: endDate,
|
||||
after: startDate,
|
||||
groupedBy: EIssueGroupByToServerOptions["target_date"],
|
||||
},
|
||||
viewId
|
||||
);
|
||||
}, [fetchIssues, storeType, startDate, endDate, layout, viewId]);
|
||||
|
||||
const handleDragAndDrop = async (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
@ -74,6 +106,23 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreIssues = useCallback(
|
||||
(dateString: string) => {
|
||||
fetchNextIssues(dateString);
|
||||
},
|
||||
[fetchNextIssues]
|
||||
);
|
||||
|
||||
const getPaginationData = useCallback(
|
||||
(groupId: string | undefined) => issues?.getPaginationData(groupId, undefined),
|
||||
[issues?.getPaginationData]
|
||||
);
|
||||
|
||||
const getGroupIssueCount = useCallback(
|
||||
(groupId: string | undefined) => issues?.getGroupIssueCount(groupId, undefined, false),
|
||||
[issues?.getGroupIssueCount]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-full overflow-hidden bg-custom-background-100 pt-4">
|
||||
@ -83,6 +132,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
layout={displayFilters?.calendar?.layout}
|
||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||
issueCalendarView={issueCalendarView}
|
||||
quickActions={({ issue, parentRef, customActionButton, placement }) => (
|
||||
<QuickActions
|
||||
parentRef={parentRef}
|
||||
@ -97,14 +147,16 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
placements={placement}
|
||||
/>
|
||||
)}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
getPaginationData={getPaginationData}
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
addIssuesToView={addIssuesToView}
|
||||
quickAddCallback={issues.quickAddIssue}
|
||||
viewId={viewId}
|
||||
quickAddCallback={quickAddIssue}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
updateFilters={updateFilters}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ import type {
|
||||
TIssue,
|
||||
TIssueKanbanFilters,
|
||||
TIssueMap,
|
||||
TPaginationData,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
@ -20,20 +21,21 @@ import { Spinner } from "@plane/ui";
|
||||
import { CalendarHeader, CalendarIssueBlocks, CalendarWeekDays, CalendarWeekHeader } from "@/components/issues";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "@/constants/calendar";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssues, useUser } from "@/hooks/store";
|
||||
import { useCalendarView } from "@/hooks/store/use-calendar-view";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// store
|
||||
import { ICycleIssuesFilter } from "@/store/issue/cycle";
|
||||
import { ICalendarStore } from "@/store/issue/issue_calendar_view.store";
|
||||
import { IModuleIssuesFilter } from "@/store/issue/module";
|
||||
import { IProjectIssuesFilter } from "@/store/issue/project";
|
||||
import { IProjectViewIssuesFilter } from "@/store/issue/project-views";
|
||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import type { ICalendarWeek } from "./types";
|
||||
|
||||
@ -43,20 +45,18 @@ type Props = {
|
||||
groupedIssueIds: TGroupedIssues;
|
||||
layout: "month" | "week" | undefined;
|
||||
showWeekends: boolean;
|
||||
issueCalendarView: ICalendarStore;
|
||||
loadMoreIssues: (dateString: string) => void;
|
||||
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
|
||||
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
quickActions: TRenderQuickActions;
|
||||
handleDragAndDrop: (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
destinationDate: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
readOnly?: boolean;
|
||||
updateFilters?: (
|
||||
projectId: string,
|
||||
@ -72,11 +72,14 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
groupedIssueIds,
|
||||
layout,
|
||||
showWeekends,
|
||||
issueCalendarView,
|
||||
loadMoreIssues,
|
||||
handleDragAndDrop,
|
||||
quickActions,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
getPaginationData,
|
||||
getGroupIssueCount,
|
||||
updateFilters,
|
||||
readOnly = false,
|
||||
} = props;
|
||||
@ -88,7 +91,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
issues: { viewFlags },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const issueCalendarView = useCalendarView();
|
||||
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
@ -123,7 +126,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
|
||||
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -133,57 +136,90 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
updateFilters={updateFilters}
|
||||
/>
|
||||
<div
|
||||
className={cn("flex w-full flex-col overflow-y-auto md:h-full", {
|
||||
"vertical-scrollbar scrollbar-lg": windowWidth > 768,
|
||||
})}
|
||||
ref={scrollableContainerRef}
|
||||
>
|
||||
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
||||
<div className="h-full w-full">
|
||||
{layout === "month" && (
|
||||
<div className="grid h-full w-full grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
|
||||
{allWeeksOfActiveMonth &&
|
||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||
<CalendarWeekDays
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
key={weekIndex}
|
||||
week={week}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{layout === "week" && (
|
||||
<CalendarWeekDays
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
week={issueCalendarView.allDaysOfActiveWeek}
|
||||
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.CALENDAR}>
|
||||
<div
|
||||
className={cn("flex md:h-full w-full flex-col overflow-y-auto", {
|
||||
"vertical-scrollbar scrollbar-lg": windowWidth > 768,
|
||||
})}
|
||||
ref={scrollableContainerRef}
|
||||
>
|
||||
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
||||
<div className="h-full w-full">
|
||||
{layout === "month" && (
|
||||
<div className="grid h-full w-full grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
|
||||
{allWeeksOfActiveMonth &&
|
||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||
<CalendarWeekDays
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
key={weekIndex}
|
||||
week={week}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
getPaginationData={getPaginationData}
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{layout === "week" && (
|
||||
<CalendarWeekDays
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
week={issueCalendarView.allDaysOfActiveWeek}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
getPaginationData={getPaginationData}
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* mobile view */}
|
||||
<div className="md:hidden">
|
||||
<p className="p-4 text-xl font-semibold">
|
||||
{`${selectedDate.getDate()} ${
|
||||
MONTHS_LIST[selectedDate.getMonth() + 1].title
|
||||
}, ${selectedDate.getFullYear()}`}
|
||||
</p>
|
||||
<CalendarIssueBlocks
|
||||
date={selectedDate}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
issueIdList={issueIdList}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
getPaginationData={getPaginationData}
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
quickActions={quickActions}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
isDragDisabled
|
||||
isMobileView
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</IssueLayoutHOC>
|
||||
|
||||
{/* mobile view */}
|
||||
<div className="md:hidden">
|
||||
@ -197,20 +233,19 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
quickActions={quickActions}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
getPaginationData={getPaginationData}
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
isMonthLayout={false}
|
||||
showAllIssues
|
||||
isDragDisabled
|
||||
isMobileView
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element
|
||||
import { differenceInCalendarDays } from "date-fns";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types";
|
||||
import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
@ -29,22 +29,19 @@ type Props = {
|
||||
date: ICalendarDate;
|
||||
issues: TIssueMap | undefined;
|
||||
groupedIssueIds: TGroupedIssues;
|
||||
quickActions: TRenderQuickActions;
|
||||
loadMoreIssues: (dateString: string) => void;
|
||||
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
|
||||
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
quickActions: TRenderQuickActions;
|
||||
handleDragAndDrop: (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
destinationDate: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
readOnly?: boolean;
|
||||
selectedDate: Date;
|
||||
setSelectedDate: (date: Date) => void;
|
||||
@ -56,12 +53,14 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
date,
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
loadMoreIssues,
|
||||
getPaginationData,
|
||||
getGroupIssueCount,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
readOnly = false,
|
||||
selectedDate,
|
||||
handleDragAndDrop,
|
||||
@ -69,7 +68,6 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
} = props;
|
||||
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
|
||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||
|
||||
@ -114,7 +112,6 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
|
||||
handleDragAndDrop(sourceData?.id, sourceData?.date, destinationData?.date);
|
||||
setShowAllIssues(true);
|
||||
highlightIssueOnDrop(source?.element?.id, false);
|
||||
},
|
||||
})
|
||||
@ -122,9 +119,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
}, [dayTileRef?.current, formattedDatePayload]);
|
||||
|
||||
if (!formattedDatePayload) return null;
|
||||
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
|
||||
|
||||
const totalIssues = issueIdList?.length ?? 0;
|
||||
const issueIds = groupedIssueIds?.[formattedDatePayload];
|
||||
|
||||
const isToday = date.date.toDateString() === new Date().toDateString();
|
||||
const isSelectedDate = date.date.toDateString() == selectedDate.toDateString();
|
||||
@ -171,18 +166,17 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
<CalendarIssueBlocks
|
||||
date={date.date}
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
showAllIssues={showAllIssues}
|
||||
setShowAllIssues={setShowAllIssues}
|
||||
issueIdList={issueIds}
|
||||
quickActions={quickActions}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
getPaginationData={getPaginationData}
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
isDragDisabled={readOnly}
|
||||
addIssuesToView={addIssuesToView}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
isMonthLayout={isMonthLayout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -205,8 +199,6 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
>
|
||||
{date.date.getDate()}
|
||||
</div>
|
||||
|
||||
{totalIssues > 0 && <div className="mt-1 size-1.5 flex flex-shrink-0 rounded bg-custom-primary-100" />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,33 +1,26 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { TIssue, TIssueMap } from "@plane/types";
|
||||
import { TIssue, TIssueMap, TPaginationData } from "@plane/types";
|
||||
// components
|
||||
import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "@/components/issues";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
date: Date;
|
||||
issues: TIssueMap | undefined;
|
||||
issueIdList: string[] | null;
|
||||
showAllIssues: boolean;
|
||||
setShowAllIssues?: Dispatch<SetStateAction<boolean>>;
|
||||
isMonthLayout: boolean;
|
||||
loadMoreIssues: (dateString: string) => void;
|
||||
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
|
||||
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
|
||||
issueIdList: string[];
|
||||
quickActions: TRenderQuickActions;
|
||||
isDragDisabled?: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
readOnly?: boolean;
|
||||
isMobileView?: boolean;
|
||||
};
|
||||
@ -37,28 +30,36 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
date,
|
||||
issues,
|
||||
issueIdList,
|
||||
showAllIssues,
|
||||
setShowAllIssues,
|
||||
quickActions,
|
||||
loadMoreIssues,
|
||||
isDragDisabled = false,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
readOnly,
|
||||
isMonthLayout,
|
||||
isMobileView = false,
|
||||
} = props;
|
||||
|
||||
const formattedDatePayload = renderFormattedPayloadDate(date);
|
||||
const totalIssues = issueIdList?.length ?? 0;
|
||||
|
||||
const {
|
||||
issues: { getGroupIssueCount, getPaginationData, getIssueLoader },
|
||||
} = useIssuesStore();
|
||||
|
||||
if (!formattedDatePayload) return null;
|
||||
|
||||
const dayIssueCount = getGroupIssueCount(formattedDatePayload, undefined, false);
|
||||
const nextPageResults = getPaginationData(formattedDatePayload, undefined)?.nextPageResults;
|
||||
const isPaginating = !!getIssueLoader(formattedDatePayload);
|
||||
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined && dayIssueCount !== undefined
|
||||
? issueIdList?.length < dayIssueCount
|
||||
: !!nextPageResults;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueIdList?.slice(0, showAllIssues || !isMonthLayout ? issueIdList.length : 4).map((issueId) => (
|
||||
{issueIdList?.map((issueId) => (
|
||||
<div key={issueId} className="relative cursor-pointer p-1 px-2">
|
||||
<CalendarIssueBlockRoot
|
||||
issues={issues}
|
||||
@ -68,17 +69,13 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{totalIssues > 4 && isMonthLayout && (
|
||||
<div className="hidden items-center px-2.5 py-1 md:flex">
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded px-1.5 py-1 text-xs font-medium text-custom-text-400 hover:bg-custom-background-80 hover:text-custom-text-300"
|
||||
onClick={() => setShowAllIssues && setShowAllIssues(!showAllIssues)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
|
||||
{isPaginating && (
|
||||
<div className="p-1 px-2">
|
||||
<div className="flex h-10 md:h-8 w-full items-center justify-between gap-1.5 rounded md:px-1 px-4 py-1.5 bg-custom-background-80 animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
|
||||
<div className="border-b border-custom-border-200 px-1 py-1 md:border-none md:px-2">
|
||||
<CalendarQuickAddIssueForm
|
||||
@ -89,11 +86,21 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
onOpen={() => setShowAllIssues && setShowAllIssues(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldLoadMore && !isPaginating && (
|
||||
<div className="flex items-center px-2.5 py-1">
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
|
||||
onClick={() => loadMoreIssues(formattedDatePayload)}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -27,14 +27,8 @@ type Props = {
|
||||
groupId?: string;
|
||||
subGroupId?: string | null;
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
|
||||
@ -66,7 +60,7 @@ const Inputs = (props: any) => {
|
||||
};
|
||||
|
||||
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, viewId, onOpen } = props;
|
||||
const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, onOpen } = props;
|
||||
|
||||
// router
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
@ -133,14 +127,9 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
|
||||
if (quickAddCallback) {
|
||||
const quickAddPromise = quickAddCallback(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
...payload,
|
||||
},
|
||||
viewId
|
||||
);
|
||||
const quickAddPromise = quickAddCallback(projectId.toString(), {
|
||||
...payload,
|
||||
});
|
||||
setPromiseToast<any>(quickAddPromise, {
|
||||
loading: "Adding issue...",
|
||||
success: {
|
||||
|
@ -33,9 +33,8 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
<BaseCalendarRoot
|
||||
QuickActions={CycleIssueQuickActions}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={cycleId.toString()}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
storeType={EIssuesStoreType.CYCLE}
|
||||
viewId={cycleId?.toString()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -28,9 +28,8 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
QuickActions={ModuleIssueQuickActions}
|
||||
storeType={EIssuesStoreType.MODULE}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={moduleId.toString()}
|
||||
viewId={moduleId?.toString()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { ProjectIssueQuickActions } from "@/components/issues";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// components
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
|
||||
export const CalendarLayout: React.FC = observer(() => (
|
||||
<BaseCalendarRoot QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROJECT} />
|
||||
));
|
||||
export const CalendarLayout: React.FC = observer(() => <BaseCalendarRoot QuickActions={ProjectIssueQuickActions} />);
|
||||
|
@ -1,22 +1,11 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
import { ProjectIssueQuickActions } from "@/components/issues";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// components
|
||||
// types
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
// constants
|
||||
|
||||
export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||
// router
|
||||
const { viewId } = useParams();
|
||||
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
viewId={viewId?.toString()}
|
||||
storeType={EIssuesStoreType.PROJECT_VIEW}
|
||||
/>
|
||||
);
|
||||
});
|
||||
export const ProjectViewCalendarLayout: React.FC = observer(() => (
|
||||
<BaseCalendarRoot QuickActions={ProjectIssueQuickActions} />
|
||||
));
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types";
|
||||
import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
|
||||
// components
|
||||
import { CalendarDayTile } from "@/components/issues";
|
||||
// helpers
|
||||
@ -17,22 +17,19 @@ type Props = {
|
||||
issues: TIssueMap | undefined;
|
||||
groupedIssueIds: TGroupedIssues;
|
||||
week: ICalendarWeek | undefined;
|
||||
quickActions: TRenderQuickActions;
|
||||
quickActions: TRenderQuickActions
|
||||
loadMoreIssues: (dateString: string) => void;
|
||||
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
|
||||
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
handleDragAndDrop: (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
destinationDate: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
readOnly?: boolean;
|
||||
selectedDate: Date;
|
||||
setSelectedDate: (date: Date) => void;
|
||||
@ -45,12 +42,14 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
groupedIssueIds,
|
||||
handleDragAndDrop,
|
||||
week,
|
||||
loadMoreIssues,
|
||||
getPaginationData,
|
||||
getGroupIssueCount,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
readOnly = false,
|
||||
selectedDate,
|
||||
setSelectedDate,
|
||||
@ -79,12 +78,14 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
date={date}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
getPaginationData={getPaginationData}
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
quickActions={quickActions}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
/>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user