mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 05:49:40 +02:00
[WEB-1955] Chore/bulk ops (#693)
* wip * fix: types and css fixes * fix: unwanted changes removed * chore: added cycle, module and estimate * fix: type fixes * fix: TBulkIssueProperties keys * fix: added condition for estimate points --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
# Third Party imports
|
||||
@@ -22,6 +21,7 @@ from plane.db.models import (
|
||||
Label,
|
||||
Project,
|
||||
User,
|
||||
Cycle,
|
||||
)
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
@@ -128,6 +128,67 @@ def track_assignees(
|
||||
)
|
||||
|
||||
|
||||
def create_cycle_issue_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
requested_data = (
|
||||
json.loads(requested_data) if requested_data is not None else None
|
||||
)
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
if requested_data.get("cycle_id") and current_instance.get("cycle_id"):
|
||||
new_cycle = Cycle.objects.filter(
|
||||
pk=requested_data.get("cycle_id")
|
||||
).first()
|
||||
old_cycle = Cycle.objects.filter(
|
||||
pk=current_instance.get("cycle_id")
|
||||
).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=old_cycle.name,
|
||||
new_value=new_cycle.name,
|
||||
field="cycles",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
old_identifier=old_cycle.id,
|
||||
new_identifier=new_cycle.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
if requested_data.get("cycle_id"):
|
||||
cycle = Cycle.objects.filter(pk=requested_data.get("cycle_id")).first()
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=cycle.name,
|
||||
field="cycles",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"added cycle {cycle.name}",
|
||||
new_identifier=cycle.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_issue_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
@@ -201,6 +262,7 @@ def bulk_issue_activity(
|
||||
|
||||
ACTIVITY_MAPPER = {
|
||||
"issue.activity.updated": update_issue_activity,
|
||||
"cycle.activity.created": create_cycle_issue_activity,
|
||||
}
|
||||
|
||||
func = ACTIVITY_MAPPER.get(type)
|
||||
|
||||
@@ -3,8 +3,13 @@ import json
|
||||
from datetime import datetime
|
||||
|
||||
# Django imports
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import Q, Value, UUIDField, F
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -23,6 +28,8 @@ from plane.db.models import (
|
||||
IssueAssignee,
|
||||
Workspace,
|
||||
IssueSubscriber,
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.ee.bgtasks import bulk_issue_activity
|
||||
@@ -50,7 +57,35 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||
)
|
||||
.select_related("state")
|
||||
.prefetch_related("labels", "assignees")
|
||||
.prefetch_related("labels", "assignees", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.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())),
|
||||
),
|
||||
)
|
||||
)
|
||||
# Current epoch
|
||||
epoch = int(timezone.now().timestamp())
|
||||
@@ -64,6 +99,7 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||
bulk_update_issues = []
|
||||
bulk_issue_activities = []
|
||||
bulk_update_issue_labels = []
|
||||
bulk_update_issue_modules = []
|
||||
bulk_update_issue_assignees = []
|
||||
|
||||
properties = request.data.get("properties", {})
|
||||
@@ -271,6 +307,104 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||
}
|
||||
)
|
||||
|
||||
# Module
|
||||
if properties.get("module_ids", []):
|
||||
for module_id in properties.get(
|
||||
"module_ids",
|
||||
):
|
||||
issue_module_ids = [str(uuid) for uuid in issue.module_ids]
|
||||
if module_id not in issue_module_ids:
|
||||
bulk_update_issue_modules.append(
|
||||
ModuleIssue(
|
||||
issue=issue,
|
||||
module_id=module_id,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
)
|
||||
)
|
||||
issue_activities.append(
|
||||
{
|
||||
"type": "module.activity.created",
|
||||
"requested_data": json.dumps(
|
||||
{"module_id": module_id}
|
||||
),
|
||||
"current_instance": None,
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
|
||||
# Cycles
|
||||
if properties.get("cycle_id", False):
|
||||
if str(issue.cycle_id) != properties.get("cycle_id"):
|
||||
if issue.cycle_id is not None:
|
||||
# Old cycle issue to delete
|
||||
CycleIssue.objects.filter(
|
||||
issue_id=issue.id, cycle_id=issue.cycle_id
|
||||
).delete()
|
||||
# New issues to create
|
||||
_ = CycleIssue.objects.create(
|
||||
created_by_id=request.user.id,
|
||||
updated_by_id=request.user.id,
|
||||
cycle_id=properties.get("cycle_id"),
|
||||
issue=issue,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
bulk_issue_activities.append(
|
||||
{
|
||||
"type": "cycle.activity.created",
|
||||
"requested_data": json.dumps(
|
||||
{"cycle_id": properties.get("cycle_id")}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{
|
||||
"cycle_id": (
|
||||
str(issue.cycle_id)
|
||||
if issue.cycle_id
|
||||
else None
|
||||
)
|
||||
}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
|
||||
# Estimate Point
|
||||
if properties.get("estimate_point", False):
|
||||
issue_activities.append(
|
||||
{
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": json.dumps(
|
||||
{
|
||||
"estimate_point": properties.get(
|
||||
"estimate_point"
|
||||
)
|
||||
}
|
||||
),
|
||||
"current_instance": json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(issue.estimate_point_id)
|
||||
if issue.estimate_point_id
|
||||
else issue.estimate_point_id
|
||||
)
|
||||
}
|
||||
),
|
||||
"issue_id": str(issue.id),
|
||||
"actor_id": str(request.user.id),
|
||||
"project_id": str(project_id),
|
||||
"epoch": epoch,
|
||||
}
|
||||
)
|
||||
issue.estimate_point_id = properties.get("estimate_point")
|
||||
|
||||
# Bulk update all the objects
|
||||
Issue.objects.bulk_update(
|
||||
bulk_update_issues,
|
||||
@@ -280,6 +414,7 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||
"target_date",
|
||||
"state_id",
|
||||
"completed_at",
|
||||
"estimate_point_id",
|
||||
],
|
||||
batch_size=100,
|
||||
)
|
||||
@@ -297,6 +432,13 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||
ignore_conflicts=True,
|
||||
batch_size=100,
|
||||
)
|
||||
|
||||
# Create new modules
|
||||
ModuleIssue.objects.bulk_create(
|
||||
bulk_update_issue_modules,
|
||||
ignore_conflicts=True,
|
||||
batch_size=100,
|
||||
)
|
||||
# update the issue activity
|
||||
[issue_activity.delay(**activity) for activity in issue_activities]
|
||||
[
|
||||
|
||||
@@ -10,7 +10,15 @@ import { TBulkIssueProperties } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { DateDropdown, MemberDropdown, PriorityDropdown, StateDropdown } from "@/components/dropdowns";
|
||||
import {
|
||||
DateDropdown,
|
||||
MemberDropdown,
|
||||
PriorityDropdown,
|
||||
StateDropdown,
|
||||
EstimateDropdown,
|
||||
CycleDropdown,
|
||||
ModuleDropdown,
|
||||
} from "@/components/dropdowns";
|
||||
import { IssueLabelSelect } from "@/components/issues/select";
|
||||
import { CreateLabelModal } from "@/components/labels";
|
||||
// constants
|
||||
@@ -18,6 +26,7 @@ import { EErrorCodes, ERROR_DETAILS } from "@/constants/errors";
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useProjectEstimates } from "@/hooks/store";
|
||||
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
||||
import { TSelectionHelper, TSelectionSnapshot } from "@/hooks/use-multiple-select";
|
||||
|
||||
@@ -34,6 +43,9 @@ const defaultValues: TBulkIssueProperties = {
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
label_ids: [],
|
||||
cycle_id: "",
|
||||
module_ids: [],
|
||||
estimate_point: null,
|
||||
};
|
||||
|
||||
export const IssueBulkOperationsProperties: React.FC<Props> = observer((props) => {
|
||||
@@ -46,6 +58,7 @@ export const IssueBulkOperationsProperties: React.FC<Props> = observer((props) =
|
||||
const {
|
||||
issues: { bulkUpdateProperties },
|
||||
} = useIssuesStore();
|
||||
const { currentActiveEstimateId } = useProjectEstimates();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
@@ -213,7 +226,7 @@ export const IssueBulkOperationsProperties: React.FC<Props> = observer((props) =
|
||||
projectId={projectId.toString()}
|
||||
onChange={onChange}
|
||||
setIsOpen={() => setCreateLabelModal(true)}
|
||||
buttonClassName="text-custom-text-300"
|
||||
buttonClassName="text-custom-text-300 "
|
||||
placement="top-start"
|
||||
/>
|
||||
</div>
|
||||
@@ -221,6 +234,67 @@ export const IssueBulkOperationsProperties: React.FC<Props> = observer((props) =
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{projectId && (
|
||||
<Controller
|
||||
name="cycle_id"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<>
|
||||
<CycleDropdown
|
||||
value={value as string | null}
|
||||
onChange={onChange}
|
||||
projectId={projectId.toString()}
|
||||
buttonVariant="border-with-text"
|
||||
buttonClassName="text-custom-text-300 py-1 rounded"
|
||||
disabled={isUpdateDisabled}
|
||||
placement="top-start"
|
||||
placeholder="Cycle"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{projectId && currentActiveEstimateId && (
|
||||
<Controller
|
||||
name="estimate_point"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<EstimateDropdown
|
||||
value={value as string | null}
|
||||
onChange={onChange}
|
||||
projectId={projectId.toString()}
|
||||
buttonVariant="border-with-text"
|
||||
buttonClassName="text-custom-text-300 py-1"
|
||||
disabled={isUpdateDisabled}
|
||||
placement="top-start"
|
||||
placeholder="Estimates"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{projectId && (
|
||||
<Controller
|
||||
name="module_ids"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<>
|
||||
<ModuleDropdown
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
projectId={projectId.toString()}
|
||||
buttonVariant="border-with-text"
|
||||
buttonClassName="text-custom-text-300 border-none px-2 py-1"
|
||||
buttonContainerClassName="border-[0.5px] border-custom-border-300 rounded"
|
||||
disabled={isUpdateDisabled}
|
||||
placement="top-start"
|
||||
placeholder="Module"
|
||||
showCount
|
||||
multiple
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isDirty && (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user