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 && (
<>
-
-
-
-
+
+
+ )}
+
control={control}
- name="type_id"
+ name={"type_id" as FieldPath}
rules={{
- required: true,
+ required: isRequired,
}}
render={({ field: { value, onChange } }) => (
-
+
{projectId && (
{
- onChange(issueTypeId);
- handleFormChange();
+ // If it's not set as required, then allow issue type to be null (unset issue type)
+ const newValue = !isRequired && value === issueTypeId ? null : issueTypeId;
+ onChange(newValue);
+ handleFormChange?.();
}}
/>
)}
diff --git a/web/ee/store/issue-types/issue-types.store.ts b/web/ee/store/issue-types/issue-types.store.ts
index e95ad1718c..31074dacb3 100644
--- a/web/ee/store/issue-types/issue-types.store.ts
+++ b/web/ee/store/issue-types/issue-types.store.ts
@@ -40,6 +40,7 @@ export interface IIssueTypesStore {
getProjectIssueTypes: (projectId: string, activeOnly: boolean) => Record; // issue type id -> issue type
getProjectDefaultIssueType: (projectId: string) => IIssueType | undefined;
getIssueTypeProperties: (issueTypeId: string) => IIssueProperty[];
+ getIssueTypeIdsWithMandatoryProperties: (projectId: string) => string[];
isIssueTypeEnabledForProject: (
workspaceSlug: string,
projectId: string,
@@ -170,6 +171,19 @@ export class IssueTypes implements IIssueTypesStore {
return issueType.properties;
});
+ /**
+ * @description Get issue type ids with mandatory properties
+ * @param projectId
+ * @returns {string[]}
+ */
+ getIssueTypeIdsWithMandatoryProperties = computedFn((projectId: string) => {
+ const projectIssueTypes = this.getProjectIssueTypes(projectId, false);
+ return Object.keys(projectIssueTypes).filter((issueTypeId) => {
+ const issueType = projectIssueTypes[issueTypeId];
+ return issueType.activeProperties.some((property) => property.is_required);
+ });
+ });
+
/**
* @description Check if issue type is enabled for the project
* @param workspaceSlug