From de0652991f5013fff7f850db8a5e6ac975da8f13 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 12 Aug 2024 13:09:08 +0530 Subject: [PATCH] chore: issue property create options endpoint revamp (#803) * dev: update the issue property option endpoint * chore: optimize create options API call and some bug fixes. * fix: issue type switch on create more toggle. * chore: add loader for issue properties. * chore; fix issue type name and description overflow. * chore: delete property confirmation modal. --------- Co-authored-by: pablohashescobar --- .../plane/ee/views/app/issue_property/base.py | 111 +++++++++++++++++- .../ee/views/app/issue_property/option.py | 39 +++--- .../issue-types/issue-type-list-item.tsx | 4 +- .../properties/attributes/options/root.tsx | 2 +- .../properties/delete-confirmation-modal.tsx | 83 +++++++++++++ .../properties/dropdowns/property-type.tsx | 2 +- .../issue-types/properties/index.ts | 1 + .../properties/property-list-item.tsx | 83 ++++--------- .../issue-types/properties/quick-actions.tsx | 51 ++++---- .../issue-types/properties/root.tsx | 18 ++- .../components/issue-types/values/update.tsx | 4 +- web/ee/components/issues/issue-modal/form.tsx | 7 +- .../issues/issue-modal/issue-type-select.tsx | 6 +- web/ee/constants/issue-properties.ts | 30 ++--- web/ee/helpers/issue-properties.helper.ts | 4 +- .../issue-types/issue-properties.service.ts | 12 +- .../issue-property-options.service.ts | 8 +- web/ee/store/issue-types/issue-property.ts | 22 ++-- web/ee/store/issue-types/issue-type.ts | 20 +++- .../types/issue-types/issue-properties.d.ts | 10 +- .../issue-property-configurations.d.ts | 10 +- 21 files changed, 346 insertions(+), 181 deletions(-) create mode 100644 web/ee/components/issue-types/properties/delete-confirmation-modal.tsx diff --git a/apiserver/plane/ee/views/app/issue_property/base.py b/apiserver/plane/ee/views/app/issue_property/base.py index 63ca9efdbb..8d45f9719c 100644 --- a/apiserver/plane/ee/views/app/issue_property/base.py +++ b/apiserver/plane/ee/views/app/issue_property/base.py @@ -1,5 +1,5 @@ # Django imports -from django.db import IntegrityError +from django.db import IntegrityError, models # Third party imports from rest_framework import status @@ -8,10 +8,11 @@ from rest_framework.response import Response # Module imports from plane.db.models import Issue from plane.ee.views.base import BaseAPIView -from plane.ee.models import IssueProperty +from plane.ee.models import IssueProperty, IssuePropertyOption from plane.ee.permissions import ProjectEntityPermission from plane.ee.serializers import ( IssuePropertySerializer, + IssuePropertyOptionSerializer, ) from plane.payment.flags.flag_decorator import check_feature_flag from plane.payment.flags.flag import FeatureFlag @@ -58,8 +59,9 @@ class IssuePropertyEndpoint(BaseAPIView): issue_type_id, ): try: - # Create a new issue properties - serializer = IssuePropertySerializer(data=request.data) + + # Get the options + options = request.data.pop("options", []) # Check is_active if not request.data.get("is_active"): @@ -81,11 +83,94 @@ class IssuePropertyEndpoint(BaseAPIView): if request.data.get("is_required") is True: request.data["default_value"] = [] + # Create a new issue properties + serializer = IssuePropertySerializer(data=request.data) # Validate the data serializer.is_valid(raise_exception=True) # Save the data serializer.save(project_id=project_id, issue_type_id=issue_type_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) + + issue_property = IssueProperty.objects.get( + project_id=project_id, + issue_type_id=issue_type_id, + pk=serializer.data["id"], + ) + + # Check if the property type is option and create the options + if issue_property.property_type == "OPTION": + workspace_id = issue_property.workspace_id + issue_property_id = issue_property.id + + # Bulk create the options + bulk_create_options = [] + last_id = IssuePropertyOption.objects.filter( + project=project_id, property_id=issue_property_id + ).aggregate(largest=models.Max("sort_order"))["largest"] + + if last_id: + sort_order = last_id + 10000 + else: + sort_order = 10000 + + for option in options: + if not option.get("name"): + return Response( + {"error": "Name of option is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + bulk_create_options.append( + IssuePropertyOption( + name=option.get("name"), + sort_order=sort_order, + property_id=issue_property_id, + description=option.get("description", ""), + logo_props=option.get("logo_props", {}), + is_active=option.get("is_active", True), + is_default=option.get("is_default", False), + parent_id=option.get("parent_id"), + workspace_id=issue_property.workspace_id, + project_id=project_id, + ) + ) + + sort_order += 10000 + + # Create the options + IssuePropertyOption.objects.bulk_create( + bulk_create_options, + batch_size=100, + ) + + # Fetch all the default options + issue_property_options = IssuePropertyOption.objects.filter( + property_id=issue_property_id, + workspace_id=workspace_id, + project_id=project_id, + is_default=True, + ).values_list("id", flat=True) + + # Save the default value + issue_property.default_value = [ + str(option) for option in issue_property_options + ] + issue_property.save() + + serializer = IssuePropertySerializer(issue_property) + options = IssuePropertyOption.objects.filter( + property_id=issue_property.id, + workspace_id=issue_property.workspace_id, + project_id=project_id, + ) + options_serializer = IssuePropertyOptionSerializer( + options, many=True + ) + # generate the response with the new data and options + response = { + "property_detail": serializer.data, + "options": options_serializer.data, + } + return Response(response, status=status.HTTP_201_CREATED) except IntegrityError: return Response( { @@ -114,6 +199,22 @@ class IssuePropertyEndpoint(BaseAPIView): }, status=status.HTTP_400_BAD_REQUEST, ) + # if property type is being changed, reset the defaults + if ( + request.data.get("property_type") + and request.data.get("property_type") + != issue_property.property_type + ): + defaults = { + "relation_type": None, + "default_value": [], + "settings": {}, + "is_multi": False, + "validation_rules": {}, + } + # Update request data with defaults for missing fields + for field, default_value in defaults.items(): + request.data.setdefault(field, default_value) # Check defaults if ( diff --git a/apiserver/plane/ee/views/app/issue_property/option.py b/apiserver/plane/ee/views/app/issue_property/option.py index 885c7c642d..f27cd4b4e6 100644 --- a/apiserver/plane/ee/views/app/issue_property/option.py +++ b/apiserver/plane/ee/views/app/issue_property/option.py @@ -78,37 +78,28 @@ class IssuePropertyOptionEndpoint(BaseAPIView): project=project_id, property_id=issue_property_id ).aggregate(largest=models.Max("sort_order"))["largest"] + # Set the sort order for the new option if last_id: sort_order = last_id + 10000 else: sort_order = 10000 - bulk_property_options = [] - for option in request.data.get("options", []): - bulk_property_options.append( - IssuePropertyOption( - name=option.get("name"), - sort_order=sort_order, - property_id=issue_property_id, - description=option.get("description", ""), - logo_props=option.get("logo_props", {}), - is_active=option.get("is_active", True), - is_default=option.get("is_default", False), - parent_id=option.get("parent_id"), - workspace_id=issue_property.workspace_id, - project_id=project_id, - ) - ) - sort_order += 10000 - - # Bulk create the options - issue_property_options = IssuePropertyOption.objects.bulk_create( - bulk_property_options, batch_size=100 + # Create the issue property option + issue_property_option = IssuePropertyOption.objects.create( + name=request.data.get("name"), + sort_order=sort_order, + property_id=issue_property_id, + description=request.data.get("description", ""), + logo_props=request.data.get("logo_props", {}), + is_active=request.data.get("is_active", True), + is_default=request.data.get("is_default", False), + parent_id=request.data.get("parent_id"), + workspace_id=issue_property.workspace_id, + project_id=project_id, ) - serializer = IssuePropertyOptionSerializer( - issue_property_options, many=True - ) + # Serialize the data + serializer = IssuePropertyOptionSerializer(issue_property_option) # Save the default value return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/web/ee/components/issue-types/issue-type-list-item.tsx b/web/ee/components/issue-types/issue-type-list-item.tsx index 4ea85b579b..4aa92efb1d 100644 --- a/web/ee/components/issue-types/issue-type-list-item.tsx +++ b/web/ee/components/issue-types/issue-type-list-item.tsx @@ -53,9 +53,9 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => { isDefault={issueTypeDetail?.is_default} containerClassName={cn(!issueTypeDetail?.is_active && "opacity-60")} /> -
+
-
{issueTypeDetail?.name}
+
{issueTypeDetail?.name}
{issueTypeDetail?.description} diff --git a/web/ee/components/issue-types/properties/attributes/options/root.tsx b/web/ee/components/issue-types/properties/attributes/options/root.tsx index 59acbe74cb..fb20570627 100644 --- a/web/ee/components/issue-types/properties/attributes/options/root.tsx +++ b/web/ee/components/issue-types/properties/attributes/options/root.tsx @@ -71,7 +71,7 @@ export const IssuePropertyOptionsRoot: FC = observer( const { key, ...payload } = value; await issueProperty - .createPropertyOptions([payload]) + .createPropertyOption(payload) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, diff --git a/web/ee/components/issue-types/properties/delete-confirmation-modal.tsx b/web/ee/components/issue-types/properties/delete-confirmation-modal.tsx new file mode 100644 index 0000000000..d61868d82b --- /dev/null +++ b/web/ee/components/issue-types/properties/delete-confirmation-modal.tsx @@ -0,0 +1,83 @@ +import { observer } from "mobx-react"; +import { AlertTriangle } from "lucide-react"; +import { Button, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; +import { cn } from "@/helpers/common.helper"; +import { useState } from "react"; + +type TProps = { + isOpen: boolean; + onClose: () => void; + onDisable: () => Promise; + onDelete: () => Promise; +}; + +export const DeleteConfirmationModal: React.FC = observer((props) => { + const { isOpen, onClose, onDisable, onDelete } = props; + // states + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleDisable = async () => { + setIsSubmitting(true); + await onDisable().finally(() => { + onClose(); + setIsSubmitting(false); + }); + }; + + const handleDelete = async () => { + setIsSubmitting(true); + await onDelete().finally(() => { + onClose(); + setIsSubmitting(false); + }); + }; + + return ( + +
+ + +
+

Delete this property

+
+
+
+

Deletion of properties may lead to loss of existing data.

+

Do you want to disable the property instead?

+
+
+ +
+ + +
+
+
+ ); +}); diff --git a/web/ee/components/issue-types/properties/dropdowns/property-type.tsx b/web/ee/components/issue-types/properties/dropdowns/property-type.tsx index 2830b1d93d..3f7b416282 100644 --- a/web/ee/components/issue-types/properties/dropdowns/property-type.tsx +++ b/web/ee/components/issue-types/properties/dropdowns/property-type.tsx @@ -19,7 +19,7 @@ import { type TPropertyTypeDropdownProps = { issueTypeId: string; propertyType: EIssuePropertyType | undefined; - propertyRelationType: EIssuePropertyRelationType | undefined; + propertyRelationType: EIssuePropertyRelationType | null | undefined; currentOperationMode: TOperationMode | null; handlePropertyObjectChange: (value: Partial>) => void; error?: string; diff --git a/web/ee/components/issue-types/properties/index.ts b/web/ee/components/issue-types/properties/index.ts index 199d88e3b5..6e65b05eab 100644 --- a/web/ee/components/issue-types/properties/index.ts +++ b/web/ee/components/issue-types/properties/index.ts @@ -7,3 +7,4 @@ export * from "./dropdowns"; export * from "./attributes"; export * from "./mandatory-field-toggle"; export * from "./quick-actions"; +export * from "./delete-confirmation-modal"; 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 3e91a1bc3d..6a7ca2ae7b 100644 --- a/web/ee/components/issue-types/properties/property-list-item.tsx +++ b/web/ee/components/issue-types/properties/property-list-item.tsx @@ -1,4 +1,6 @@ import { useState } from "react"; +import cloneDeep from "lodash/cloneDeep"; +import isEmpty from "lodash/isEmpty"; import isEqual from "lodash/isEqual"; import omitBy from "lodash/omitBy"; import { observer } from "mobx-react"; @@ -24,6 +26,7 @@ import { TCreationListModes, TOperationMode, TIssuePropertyOptionCreateList, + TIssuePropertyOption, } from "@/plane-web/types"; type TIssuePropertyListItem = { @@ -136,65 +139,25 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) => } }; - const handleCreatePropertyOptions = async (propertyId: string | undefined) => { - if (!propertyId || !issuePropertyOptionCreateList || !issuePropertyOptionCreateList.length) return; - - // get issue property details from the store - const issueProperty = issueType?.getPropertyById(propertyId); - if (!issueProperty) return; - - const payload = issuePropertyOptionCreateList - .map((item) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { key, ...rest } = item; - return rest; - }) - .filter((item) => !!item.name); - - await issueProperty - .createPropertyOptions(payload) - .then(async (response) => { - // get the response and set the default value for the property - if (response && response.length) { - const defaultOptionIds = response - .filter((option) => option.id && option.is_default) - .map((option) => option.id as string); - // update the default value for the property - handlePropertyDataChange("default_value", defaultOptionIds); - // sync the property options - await issueProperty.updateProperty(issueTypeId, { - default_value: defaultOptionIds, - }); - } - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: `Property options created successfully.`, - }); - return response; - }) - .catch((error) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: error?.error ?? `Failed to create issue property options. Please try again!`, - }); - }) - .finally(() => { - setIssuePropertyOptionCreateList([]); - }); - }; - const handleCreateProperty = async () => { if (!issuePropertyData) return; + // create property options payload (required for option type) + let optionsPayload: Partial[] = []; + if (issuePropertyData.property_type === EIssuePropertyType.OPTION) { + optionsPayload = issuePropertyOptionCreateList + .map((item) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, ...rest } = item; + return rest; + }) + .filter((item) => !!item.name); + } + setIsSubmitting(true); await issueType - ?.createProperty(issuePropertyData) + ?.createProperty(issuePropertyData, optionsPayload) .then(async (response) => { - if (issuePropertyData.property_type === EIssuePropertyType.OPTION) { - await handleCreatePropertyOptions(response?.id); - } setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", @@ -210,6 +173,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) => }); }) .finally(() => { + setIssuePropertyOptionCreateList([]); key && handleIssuePropertyCreateList("remove", { key, ...issuePropertyData }); setIsSubmitting(false); }); @@ -218,7 +182,9 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) => const handleUpdateProperty = async (data: Partial>) => { if (!data) return; // Construct the payload by filtering out unchanged properties - const payload = omitBy(data, (value, key) => isEqual(value, issuePropertyDetail[key])); + const originalData = cloneDeep(issuePropertyDetail); + const payload = originalData && omitBy(data, (value, key) => isEqual(value, originalData[key])); + if (isEmpty(payload)) return; setIsSubmitting(true); await issueProperty ?.updateProperty(issueTypeId, payload) @@ -395,10 +361,11 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) => setIssuePropertyOperationMode(mode)} + onCreateUpdate={handleCreateUpdate} + onDiscard={handleDiscard} + onDelete={handleDelete} + onDisable={async () => handlePropertyDataChange("is_active", false, true)} + onIssuePropertyOperationMode={(mode) => setIssuePropertyOperationMode(mode)} />
diff --git a/web/ee/components/issue-types/properties/quick-actions.tsx b/web/ee/components/issue-types/properties/quick-actions.tsx index 804e7abfde..a3a9e27a4a 100644 --- a/web/ee/components/issue-types/properties/quick-actions.tsx +++ b/web/ee/components/issue-types/properties/quick-actions.tsx @@ -2,55 +2,44 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { Check, Pencil, Trash2, X } from "lucide-react"; // ui -import { AlertModalCore, Spinner, Tooltip } from "@plane/ui"; +import { Spinner, Tooltip } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; +// plane web components +import { DeleteConfirmationModal } from "@/plane-web/components/issue-types"; // plane web types import { TOperationMode } from "@/plane-web/types"; type TIssuePropertyQuickActions = { currentOperationMode: TOperationMode | null; isSubmitting: boolean; - handleCreateUpdate: () => Promise; - handleDiscard: () => void; - handleDelete: () => Promise; - handleIssuePropertyOperationMode: (mode: TOperationMode) => void; + onCreateUpdate: () => Promise; + onDiscard: () => void; + onDisable: () => Promise; + onDelete: () => Promise; + onIssuePropertyOperationMode: (mode: TOperationMode) => void; }; export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickActions) => { const { currentOperationMode, isSubmitting, - handleCreateUpdate, - handleDiscard, - handleDelete, - handleIssuePropertyOperationMode, + onCreateUpdate, + onDiscard, + onDisable, + onDelete, + onIssuePropertyOperationMode, } = props; // states const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const handleDeleteIssueProperty = async () => { - setIsDeleteLoading(true); - await handleDelete().finally(() => { - setIsDeleteLoading(false); - }); - }; return ( <> - setIsDeleteModalOpen(false)} - handleSubmit={handleDeleteIssueProperty} - isSubmitting={isDeleteLoading} - title="Delete this property" - content={ - <> -

Deletion of properties may lead to loss of existing data or weird behavior.

-

Do you want to disable the property instead?

- - } + onClose={() => setIsDeleteModalOpen(false)} + onDisable={onDisable} + onDelete={onDelete} />
{isSubmitting ? : } @@ -79,7 +68,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct "bg-custom-background-80": isSubmitting, } )} - onClick={handleDiscard} + onClick={onDiscard} disabled={isSubmitting} > @@ -91,7 +80,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct diff --git a/web/ee/components/issue-types/properties/root.tsx b/web/ee/components/issue-types/properties/root.tsx index 6590029e59..e7449089c5 100644 --- a/web/ee/components/issue-types/properties/root.tsx +++ b/web/ee/components/issue-types/properties/root.tsx @@ -1,13 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; import { v4 } from "uuid"; import { Plus } from "lucide-react"; // ui -import { Button } from "@plane/ui"; +import { Button, Loader } from "@plane/ui"; // plane web components import { IssuePropertyList, IssueTypePropertiesEmptyState } from "@/plane-web/components/issue-types"; // plane web hooks -import { useIssueType } from "@/plane-web/hooks/store"; +import { useIssueType, useIssueTypes } from "@/plane-web/hooks/store"; // plane web types import { EIssuePropertyType, TIssueProperty, TCreationListModes } from "@/plane-web/types"; @@ -31,11 +32,15 @@ const defaultIssueProperty: Partial> = { export const IssuePropertiesRoot = observer((props: TIssuePropertiesRoot) => { const { issueTypeId } = props; + // router + const { projectId } = useParams(); // states const [issuePropertyCreateList, setIssuePropertyCreateList] = useState([]); // store hooks + const { getProjectIssuePropertiesLoader } = useIssueTypes(); const issueType = useIssueType(issueTypeId); // derived values + const issuePropertiesLoader = getProjectIssuePropertiesLoader(projectId?.toString()); const properties = issueType?.properties; // refs const containerRef = useRef(null); @@ -73,7 +78,14 @@ export const IssuePropertiesRoot = observer((props: TIssuePropertiesRoot) => {
Properties
- {(properties && properties?.length > 0) || issuePropertyCreateList.length > 0 ? ( + {issuePropertiesLoader === "init-loader" ? ( + + + + + + + ) : (properties && properties?.length > 0) || issuePropertyCreateList.length > 0 ? ( diff --git a/web/ee/components/issues/issue-modal/form.tsx b/web/ee/components/issues/issue-modal/form.tsx index aafef9ac74..fa0b8cfb77 100644 --- a/web/ee/components/issues/issue-modal/form.tsx +++ b/web/ee/components/issues/issue-modal/form.tsx @@ -301,7 +301,12 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange={handleFormChange} /> {projectId && ( - + )}
)} 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 be3a650d68..baeff3070b 100644 --- a/web/ee/components/issues/issue-modal/issue-type-select.tsx +++ b/web/ee/components/issues/issue-modal/issue-type-select.tsx @@ -16,12 +16,13 @@ import { useFlag } from "@/plane-web/hooks/store"; type TIssueTypeSelectProps = { control: Control; - projectId: string; + projectId: string | null; disabled?: boolean; + handleFormChange: () => void; }; export const IssueTypeSelect: React.FC = observer((props) => { - const { control, projectId, disabled = false } = props; + const { control, projectId, disabled = false, handleFormChange } = props; // router const { workspaceSlug } = useParams(); // store hooks @@ -52,6 +53,7 @@ export const IssueTypeSelect: React.FC = observer((props) disabled={disabled} handleIssueTypeChange={(issueTypeId) => { onChange(issueTypeId); + handleFormChange(); }} /> )} diff --git a/web/ee/constants/issue-properties.ts b/web/ee/constants/issue-properties.ts index 7ce67aa0bf..21f3d93809 100644 --- a/web/ee/constants/issue-properties.ts +++ b/web/ee/constants/issue-properties.ts @@ -61,10 +61,10 @@ export const ISSUE_PROPERTY_TYPE_DETAILS: Partial< }, }, property_type: EIssuePropertyType.TEXT, - relation_type: undefined, - is_multi: undefined, + relation_type: null, + is_multi: false, is_required: false, - default_value: undefined, + default_value: [], settings: { display_format: "single-line", } as TIssuePropertySettingsMap[EIssuePropertyType.TEXT], @@ -82,10 +82,10 @@ export const ISSUE_PROPERTY_TYPE_DETAILS: Partial< }, }, property_type: EIssuePropertyType.DECIMAL, - relation_type: undefined, - is_multi: undefined, + relation_type: null, + is_multi: false, is_required: false, - default_value: undefined, + default_value: [], settings: undefined, }, }, @@ -101,10 +101,10 @@ export const ISSUE_PROPERTY_TYPE_DETAILS: Partial< }, }, property_type: EIssuePropertyType.OPTION, - relation_type: undefined, + relation_type: null, is_multi: false, is_required: false, - default_value: undefined, + default_value: [], settings: undefined, }, }, @@ -120,10 +120,10 @@ export const ISSUE_PROPERTY_TYPE_DETAILS: Partial< }, }, property_type: EIssuePropertyType.BOOLEAN, - relation_type: undefined, - is_multi: undefined, + relation_type: null, + is_multi: false, is_required: false, - default_value: undefined, + default_value: [], settings: undefined, }, }, @@ -139,10 +139,10 @@ export const ISSUE_PROPERTY_TYPE_DETAILS: Partial< }, }, property_type: EIssuePropertyType.DATETIME, - relation_type: undefined, - is_multi: undefined, + relation_type: null, + is_multi: false, is_required: false, - default_value: undefined, + default_value: [], settings: undefined, }, }, @@ -161,7 +161,7 @@ export const ISSUE_PROPERTY_TYPE_DETAILS: Partial< relation_type: EIssuePropertyRelationType.USER, is_multi: false, is_required: false, - default_value: undefined, + default_value: [], settings: undefined, }, }, diff --git a/web/ee/helpers/issue-properties.helper.ts b/web/ee/helpers/issue-properties.helper.ts index 344b96e04e..3ed5ba0571 100644 --- a/web/ee/helpers/issue-properties.helper.ts +++ b/web/ee/helpers/issue-properties.helper.ts @@ -18,14 +18,14 @@ import { // Get the key for the issue property type based on the property type and relation type export const getIssuePropertyTypeKey = ( issuePropertyType: EIssuePropertyType | undefined, - issuePropertyRelationType: EIssuePropertyRelationType | undefined + issuePropertyRelationType: EIssuePropertyRelationType | null | undefined ) => `${issuePropertyType}${issuePropertyRelationType ? `_${issuePropertyRelationType}` : ""}` as TIssuePropertyTypeKeys; // Get the display name for the issue property type based on the property type and relation type export const getIssuePropertyTypeDisplayName = ( issuePropertyType: EIssuePropertyType | undefined, - issuePropertyRelationType: EIssuePropertyRelationType | undefined + issuePropertyRelationType: EIssuePropertyRelationType | null | undefined ) => { const propertyTypeKey = getIssuePropertyTypeKey(issuePropertyType, issuePropertyRelationType); return ISSUE_PROPERTY_TYPE_DETAILS[propertyTypeKey]?.displayName || "--"; diff --git a/web/ee/services/issue-types/issue-properties.service.ts b/web/ee/services/issue-types/issue-properties.service.ts index 48f2cb07b1..1651556123 100644 --- a/web/ee/services/issue-types/issue-properties.service.ts +++ b/web/ee/services/issue-types/issue-properties.service.ts @@ -1,7 +1,7 @@ // helpers import { API_BASE_URL } from "@/helpers/common.helper"; // plane web types -import { EIssuePropertyType, TIssueProperty } from "@/plane-web/types"; +import { EIssuePropertyType, TIssueProperty, TIssuePropertyOption, TIssuePropertyResponse } from "@/plane-web/types"; // services import { APIService } from "@/services/api.service"; @@ -22,11 +22,15 @@ export class IssuePropertiesService extends APIService { workspaceSlug: string, projectId: string, issueTypeId: string, - data: Partial> - ): Promise> { + data: Partial>, + options?: Partial[] | undefined + ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-types/${issueTypeId}/issue-properties/`, - data + { + ...data, + options: options ?? [], + } ) .then((response) => response?.data) .catch((error) => { diff --git a/web/ee/services/issue-types/issue-property-options.service.ts b/web/ee/services/issue-types/issue-property-options.service.ts index 830cff0df2..8a2b08b032 100644 --- a/web/ee/services/issue-types/issue-property-options.service.ts +++ b/web/ee/services/issue-types/issue-property-options.service.ts @@ -22,13 +22,11 @@ export class IssuePropertyOptionsService extends APIService { workspaceSlug: string, projectId: string, issuePropertyId: string, - data: Partial[] - ): Promise { + data: Partial + ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-properties/${issuePropertyId}/options/`, - { - options: data, - } + data ) .then((response) => response?.data) .catch((error) => { diff --git a/web/ee/store/issue-types/issue-property.ts b/web/ee/store/issue-types/issue-property.ts index 867ca4af9c..7b91882467 100644 --- a/web/ee/store/issue-types/issue-property.ts +++ b/web/ee/store/issue-types/issue-property.ts @@ -30,9 +30,7 @@ export interface IIssueProperty extends TIssueProp // actions updateProperty: (issueTypeId: string, propertyData: Partial>) => Promise; addPropertyOption: (propertyOptionData: TIssuePropertyOption) => void; - createPropertyOptions: ( - propertyOptions: Partial[] - ) => Promise; + createPropertyOption: (propertyOption: Partial) => Promise; deletePropertyOption: (propertyOptionId: string) => Promise; } @@ -45,7 +43,7 @@ export class IssueProperty implements IIssueProper logo_props: TLogoProps | undefined = undefined; sort_order: number | undefined = undefined; property_type: T | undefined = undefined; - relation_type: EIssuePropertyRelationType | undefined = undefined; + relation_type: EIssuePropertyRelationType | null | undefined = undefined; is_required: boolean | undefined = undefined; default_value: string[] | undefined = undefined; settings: TIssuePropertySettingsMap[T] | undefined = undefined; @@ -92,7 +90,7 @@ export class IssueProperty implements IIssueProper // actions updateProperty: action, addPropertyOption: action, - createPropertyOptions: action, + createPropertyOption: action, deletePropertyOption: action, }); @@ -211,26 +209,24 @@ export class IssueProperty implements IIssueProper /** * @description Create a new property option - * @param {Partial[]} propertyOptions + * @param {Partial} propertyOption */ - createPropertyOptions = async (propertyOptions: Partial[]) => { + createPropertyOption = async (propertyOption: Partial) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; try { - const issuePropertyOptions = await this.propertyOptionService.create( + const issuePropertyOption = await this.propertyOptionService.create( workspaceSlug, projectId, this.id, - propertyOptions + propertyOption ); runInAction(() => { - issuePropertyOptions.forEach((option) => { - this.addPropertyOption(option); - }); + this.addPropertyOption(issuePropertyOption); }); - return issuePropertyOptions; + return issuePropertyOption; } catch (error) { console.error("IssueProperty -> createPropertyOption -> error", error); throw error; diff --git a/web/ee/store/issue-types/issue-type.ts b/web/ee/store/issue-types/issue-type.ts index 01815fceca..29e449b962 100644 --- a/web/ee/store/issue-types/issue-type.ts +++ b/web/ee/store/issue-types/issue-type.ts @@ -25,7 +25,8 @@ export interface IIssueType extends TIssueType { updateType: (issueTypeData: Partial) => Promise; addProperty: (propertyData: TIssueProperty, propertyOptions?: TIssuePropertyOption[]) => void; createProperty: ( - propertyData: Partial> + propertyData: Partial>, + propertyOptions?: Partial[] ) => Promise | undefined>; deleteProperty: (propertyId: string) => Promise; } @@ -196,16 +197,25 @@ export class IssueType implements IIssueType { * @description Create an issue property * @param propertyData Issue property data */ - createProperty = async (propertyData: Partial>) => { + createProperty = async ( + propertyData: Partial>, + propertyOptions?: Partial[] + ) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return; try { - const issueProperty = await this.issuePropertyService.create(workspaceSlug, projectId, this.id, propertyData); + const issueProperty = await this.issuePropertyService.create( + workspaceSlug, + projectId, + this.id, + propertyData, + propertyOptions + ); runInAction(() => { - this.addProperty(issueProperty); + this.addProperty(issueProperty.property_detail, issueProperty.options); }); - return issueProperty; + return issueProperty.property_detail; } catch (error) { console.error("IssueType.createProperty -> error", error); throw error; diff --git a/web/ee/types/issue-types/issue-properties.d.ts b/web/ee/types/issue-types/issue-properties.d.ts index 2dad0e4040..2b002389cb 100644 --- a/web/ee/types/issue-types/issue-properties.d.ts +++ b/web/ee/types/issue-types/issue-properties.d.ts @@ -1,7 +1,7 @@ // types import { TLogoProps } from "@plane/types"; // plane web types -import { TIssuePropertySettingsMap } from "@/plane-web/types/issue-types"; +import { TIssuePropertyOption, TIssuePropertySettingsMap } from "@/plane-web/types/issue-types"; // Issue property types export enum EIssuePropertyType { @@ -27,7 +27,7 @@ type TBaseIssueProperty = { description: string | undefined; logo_props: TLogoProps | undefined; sort_order: number | undefined; - relation_type: EIssuePropertyRelationType | undefined; + relation_type: EIssuePropertyRelationType | null | undefined; is_required: boolean | undefined; default_value: string[] | undefined; is_active: boolean | undefined; @@ -44,3 +44,9 @@ export interface TIssueProperty extends TBaseIssue property_type: T | undefined; settings: TIssuePropertySettingsMap[T] | undefined; } + +// Issue property response +export interface TIssuePropertyResponse { + property_detail: TIssueProperty; + options: TIssuePropertyOption[]; +} diff --git a/web/ee/types/issue-types/issue-property-configurations.d.ts b/web/ee/types/issue-types/issue-property-configurations.d.ts index 18e6a1aa33..fc06b910ca 100644 --- a/web/ee/types/issue-types/issue-property-configurations.d.ts +++ b/web/ee/types/issue-types/issue-property-configurations.d.ts @@ -20,11 +20,11 @@ export type TissuePropertyTypeDetails = { dataToUpdate: { logo_props: TLogoProps; property_type: EIssuePropertyType; - relation_type: EIssuePropertyRelationType | undefined; - is_multi: boolean | undefined; - is_required: boolean | undefined; - default_value: string[] | undefined; - settings: TIssuePropertySettingsMap[T] | undefined; + relation_type: EIssuePropertyRelationType | null; + is_multi: boolean; + is_required: boolean; + default_value: string[]; + settings: TIssuePropertySettingsMap[T]; }; };