Skip to content

Commit

Permalink
[WEB-1184] feat: issue bulk operations (#4530)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored and sriramveeraghanta committed May 31, 2024
1 parent 6a00fcc commit 3ecb104
Show file tree
Hide file tree
Showing 63 changed files with 1,857 additions and 437 deletions.
13 changes: 9 additions & 4 deletions admin/components/admin-sidebar/help-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { observer } from "mobx-react-lite";
import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// ui
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// helpers
import { cn, WEB_BASE_URL } from "@/helpers/common.helper";
// hooks
import { WEB_BASE_URL } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
// assets
import packageJson from "package.json";
Expand Down Expand Up @@ -42,9 +44,12 @@ export const HelpSection: FC = observer(() => {

return (
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isSidebarCollapsed ? "flex-col" : ""
}`}
className={cn(
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-28",
{
"flex-col h-auto py-1.5": isSidebarCollapsed,
}
)}
>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
Expand Down
12 changes: 12 additions & 0 deletions apiserver/plane/app/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
IssueUserDisplayPropertyEndpoint,
IssueViewSet,
LabelViewSet,
BulkIssueOperationsEndpoint,
BulkArchiveIssuesEndpoint,
)

urlpatterns = [
Expand Down Expand Up @@ -81,6 +83,11 @@
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/",
Expand Down Expand Up @@ -298,4 +305,9 @@
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-operation-issues/",
BulkIssueOperationsEndpoint.as_view(),
name="bulk-operations-issues",
),
]
6 changes: 3 additions & 3 deletions apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,7 @@
IssueActivityEndpoint,
)

from .issue.archive import (
IssueArchiveViewSet,
)
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint

from .issue.attachment import (
IssueAttachmentEndpoint,
Expand Down Expand Up @@ -154,6 +152,8 @@
)


from .issue.bulk_operations import BulkIssueOperationsEndpoint

from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
Expand Down
58 changes: 57 additions & 1 deletion apiserver/plane/app/views/issue/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from rest_framework import status

# Module imports
from .. import BaseViewSet
from .. import BaseViewSet, BaseAPIView
from plane.app.serializers import (
IssueSerializer,
IssueFlatSerializer,
Expand All @@ -49,6 +49,7 @@
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter


class IssueArchiveViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
Expand Down Expand Up @@ -351,3 +352,58 @@ def unarchive(self, request, slug, project_id, pk=None):
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,
)
Loading

0 comments on commit 3ecb104

Please sign in to comment.