Merge branch 'preview' of github.com:makeplane/plane-ee into preview

This commit is contained in:
sriram veeraghanta
2024-07-29 20:40:55 +05:30
3 changed files with 283 additions and 5 deletions

View File

@@ -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)

View File

@@ -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]
[

View File

@@ -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