diff --git a/apiserver/plane/ee/views/app/issue/bulk_operations.py b/apiserver/plane/ee/views/app/issue/bulk_operations.py index 86f3121362..d5aa3f06ac 100644 --- a/apiserver/plane/ee/views/app/issue/bulk_operations.py +++ b/apiserver/plane/ee/views/app/issue/bulk_operations.py @@ -31,6 +31,7 @@ from plane.db.models import ( CycleIssue, ModuleIssue, ) +from plane.ee.models import IssueProperty from plane.bgtasks.issue_activities_task import issue_activity from plane.ee.bgtasks import bulk_issue_activity from plane.payment.flags.flag_decorator import check_feature_flag @@ -238,6 +239,54 @@ class BulkIssueOperationsEndpoint(BaseAPIView): ) issue.target_date = properties.get("target_date") + # 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") + + # Issue Type + if properties.get("type_id", False): + issue_activities.append( + { + "type": "issue.activity.updated", + "requested_data": json.dumps( + {"type_id": properties.get("type_id")} + ), + "current_instance": json.dumps( + {"type_id": str(issue.type_id)} + ), + "issue_id": str(issue.id), + "actor_id": str(request.user.id), + "project_id": str(project_id), + "epoch": epoch, + } + ) + issue.type_id = properties.get("type_id") + bulk_update_issues.append(issue) # Labels @@ -381,35 +430,6 @@ class BulkIssueOperationsEndpoint(BaseAPIView): } ) - # 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, @@ -420,6 +440,7 @@ class BulkIssueOperationsEndpoint(BaseAPIView): "state_id", "completed_at", "estimate_point_id", + "type_id", ], batch_size=100, ) diff --git a/web/ee/components/issue-types/dropdowns/issue-type.tsx b/web/ee/components/issue-types/dropdowns/issue-type.tsx index 6eb96a28b0..f666c9fddf 100644 --- a/web/ee/components/issue-types/dropdowns/issue-type.tsx +++ b/web/ee/components/issue-types/dropdowns/issue-type.tsx @@ -1,20 +1,39 @@ import { observer } from "mobx-react"; // ui -import { CustomSearchSelect, Loader } from "@plane/ui"; +import { CustomSearchSelect, LayersIcon, Loader } from "@plane/ui"; +// ce imports +import { TIssueTypeDropdownVariant } from "@/ce/components/issues"; +// helpers +import { cn } from "@/helpers/common.helper"; // plane web types import { IssueTypeLogo } from "@/plane-web/components/issue-types"; // plane web hooks import { useIssueTypes } from "@/plane-web/hooks/store"; +export type TIssueTypeOptionTooltip = { + [issueTypeId: string]: string; // issue type id --> tooltip content +}; + type TIssueTypeDropdownProps = { issueTypeId: string | null; projectId: string; disabled?: boolean; + variant?: TIssueTypeDropdownVariant; + placeholder?: string; + optionTooltip?: TIssueTypeOptionTooltip; handleIssueTypeChange: (value: string) => void; }; export const IssueTypeDropdown = observer((props: TIssueTypeDropdownProps) => { - const { issueTypeId, projectId, disabled = false, handleIssueTypeChange } = props; + const { + issueTypeId, + projectId, + disabled = false, + variant = "sm", + placeholder = "Issue type", + handleIssueTypeChange, + optionTooltip, + } = props; // store hooks const { loader: issueTypesLoader, getProjectIssueTypes } = useIssueTypes(); // derived values @@ -28,12 +47,20 @@ export const IssueTypeDropdown = observer((props: TIssueTypeDropdownProps) => { content: (
-
{issueTypeDetail.name}
+
+ {issueTypeDetail.name} +
), + tooltip: optionTooltip?.[issueTypeId] ?? undefined, })); - if (!issueTypeId || issueTypesLoader === "init-loader") { + if (issueTypesLoader === "init-loader") { return ( @@ -45,12 +72,35 @@ export const IssueTypeDropdown = observer((props: TIssueTypeDropdownProps) => { - -
{allIssueTypes[issueTypeId]?.name}
+
+ {!issueTypeId && ( + + )} + {issueTypeId && ( + + )} +
+ {issueTypeId ? allIssueTypes[issueTypeId]?.name : placeholder} +
} options={issueTypeOptions} diff --git a/web/ee/components/issue-types/properties/property-list-item.tsx b/web/ee/components/issue-types/properties/property-list-item.tsx index 6d913074f1..1eefac57ba 100644 --- a/web/ee/components/issue-types/properties/property-list-item.tsx +++ b/web/ee/components/issue-types/properties/property-list-item.tsx @@ -176,7 +176,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) => }) .finally(() => { resetOptions(); - key && handleIssuePropertyCreateList("remove", { key, ...issuePropertyData }); + if (key) handleIssuePropertyCreateList("remove", { key, ...issuePropertyData }); setIsSubmitting(false); }); }; @@ -271,7 +271,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) => }); }) .finally(() => { - key && handleIssuePropertyCreateList("remove", { key, ...issuePropertyData }); + if (key) handleIssuePropertyCreateList("remove", { key, ...issuePropertyData }); setIsSubmitting(false); }); }; diff --git a/web/ee/components/issues/bulk-operations/properties.tsx b/web/ee/components/issues/bulk-operations/properties.tsx index 69e33a5381..987dd27dbf 100644 --- a/web/ee/components/issues/bulk-operations/properties.tsx +++ b/web/ee/components/issues/bulk-operations/properties.tsx @@ -29,6 +29,9 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper" import { useProjectEstimates } from "@/hooks/store"; import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { TSelectionHelper, TSelectionSnapshot } from "@/hooks/use-multiple-select"; +// plane web components +import { IssueTypeSelect } from "@/plane-web/components/issues/issue-modal"; +// plane web hooks import { useFlag } from "@/plane-web/hooks/store/use-flag"; type Props = { @@ -47,6 +50,7 @@ const defaultValues: TBulkIssueProperties = { cycle_id: "", module_ids: [], estimate_point: null, + type_id: null, }; export const IssueBulkOperationsProperties: React.FC = observer((props) => { @@ -59,7 +63,7 @@ export const IssueBulkOperationsProperties: React.FC = observer((props) = const { issues: { bulkUpdateProperties }, } = useIssuesStore(); - const { currentActiveEstimateId } = useProjectEstimates(); + const { currentActiveEstimateId, areEstimateEnabledByProjectId } = useProjectEstimates(); const isAdvancedBulkOpsEnabled = useFlag(workspaceSlug?.toString(), "BULK_OPS_ADVANCED"); // form info @@ -260,7 +264,7 @@ export const IssueBulkOperationsProperties: React.FC = observer((props) = )} /> )} - {projectId && currentActiveEstimateId && ( + {projectId && currentActiveEstimateId && areEstimateEnabledByProjectId(projectId?.toString()) && ( = observer((props) = )} /> )} + {projectId && ( + + )} )} diff --git a/web/ee/components/issues/issue-modal/additional-properties.tsx b/web/ee/components/issues/issue-modal/additional-properties.tsx index e796468b41..64bf9aa4fe 100644 --- a/web/ee/components/issues/issue-modal/additional-properties.tsx +++ b/web/ee/components/issues/issue-modal/additional-properties.tsx @@ -41,7 +41,7 @@ export const IssueAdditionalProperties: React.FC {isIssueTypeDisplayEnabled && ( <> - {!issueTypeId || issuePropertiesLoader === "init-loader" ? ( + {issuePropertiesLoader === "init-loader" ? ( @@ -49,12 +49,16 @@ export const IssueAdditionalProperties: React.FC ) : ( - + <> + {issueTypeId && ( + + )} + )} )} diff --git a/web/ee/components/issues/issue-modal/issue-type-select.tsx b/web/ee/components/issues/issue-modal/issue-type-select.tsx index c973fb9640..0debbd9131 100644 --- a/web/ee/components/issues/issue-modal/issue-type-select.tsx +++ b/web/ee/components/issues/issue-modal/issue-type-select.tsx @@ -1,58 +1,87 @@ "use client"; -import React from "react"; +import React, { useMemo } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { Control, Controller } from "react-hook-form"; +import { Controller, FieldPath } from "react-hook-form"; import { ChevronRight } from "lucide-react"; -// types -import { TIssue } from "@plane/types"; +// helpers +import { TIssueFields, TIssueTypeSelectProps } from "@/ce/components/issues"; +import { cn } from "@/helpers/common.helper"; // plane web components -import { IssueTypeDropdown } from "@/plane-web/components/issue-types/dropdowns"; +import { IssueTypeDropdown, TIssueTypeOptionTooltip } from "@/plane-web/components/issue-types/dropdowns"; // plane web hooks import { useIssueTypes } from "@/plane-web/hooks/store"; -type TIssueTypeSelectProps = { - control: Control; - projectId: string | null; - disabled?: boolean; - renderChevron?: boolean; - handleFormChange: () => void; -}; - -export const IssueTypeSelect: React.FC = observer((props) => { - const { control, projectId, disabled = false, handleFormChange } = props; +export const IssueTypeSelect = observer(>(props: TIssueTypeSelectProps) => { + const { + control, + projectId, + disabled = false, + variant = "sm", + placeholder, + isRequired = true, + renderChevron = false, + dropDownContainerClassName, + showMandatoryFieldInfo = false, // Show info about mandatory fields + handleFormChange, + } = props; // router const { workspaceSlug } = useParams(); // plane web store hooks - const { isIssueTypeEnabledForProject } = useIssueTypes(); + const { isIssueTypeEnabledForProject, getIssueTypeIdsWithMandatoryProperties } = useIssueTypes(); // derived values const isIssueTypeDisplayEnabled = !!projectId && isIssueTypeEnabledForProject(workspaceSlug?.toString(), projectId, "ISSUE_TYPE_DISPLAY"); + // Information for issue types with mandatory fields + let optionTooltip: TIssueTypeOptionTooltip = {}; + if (showMandatoryFieldInfo) { + // Get issue types with mandatory properties + const issueTypeIdsWithMandatoryProperties = useMemo(() => { + if (!projectId) return []; + return getIssueTypeIdsWithMandatoryProperties(projectId); + }, [getIssueTypeIdsWithMandatoryProperties, projectId]); + // Create a map of information for issue types with mandatory field + optionTooltip = useMemo(() => { + if (issueTypeIdsWithMandatoryProperties.length === 0) return {}; + return issueTypeIdsWithMandatoryProperties.reduce((acc, issueTypeId) => { + acc[issueTypeId] = + "This issue type includes mandatory properties that will initially be blank when an issue is converted to this type."; + return acc; + }, {} as TIssueTypeOptionTooltip); + }, [issueTypeIdsWithMandatoryProperties]); + } return ( <> {isIssueTypeDisplayEnabled && ( <> -
-
- +