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 <nikhilschacko@gmail.com>
This commit is contained in:
Prateek Shourya
2024-08-12 13:09:08 +05:30
committed by GitHub
parent d6021388bc
commit de0652991f
21 changed files with 346 additions and 181 deletions

View File

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

View File

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

View File

@@ -53,9 +53,9 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
isDefault={issueTypeDetail?.is_default}
containerClassName={cn(!issueTypeDetail?.is_active && "opacity-60")}
/>
<div className="flex flex-col items-start justify-start">
<div className="flex flex-col items-start justify-start whitespace-normal">
<div className="flex gap-4 text-left">
<div className="text-sm text-custom-text-100 font-medium">{issueTypeDetail?.name}</div>
<div className="text-sm text-custom-text-100 font-medium line-clamp-1">{issueTypeDetail?.name}</div>
</div>
<div className="text-sm text-custom-text-300 text-left line-clamp-1">
{issueTypeDetail?.description}

View File

@@ -71,7 +71,7 @@ export const IssuePropertyOptionsRoot: FC<TIssuePropertyOptionsRoot> = observer(
const { key, ...payload } = value;
await issueProperty
.createPropertyOptions([payload])
.createPropertyOption(payload)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,

View File

@@ -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<void>;
onDelete: () => Promise<void>;
};
export const DeleteConfirmationModal: React.FC<TProps> = observer((props) => {
const { isOpen, onClose, onDisable, onDelete } = props;
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const handleDisable = async () => {
setIsSubmitting(true);
await onDisable().finally(() => {
onClose();
setIsSubmitting(false);
});
};
const handleDelete = async () => {
setIsSubmitting(true);
await onDelete().finally(() => {
onClose();
setIsSubmitting(false);
});
};
return (
<ModalCore
isOpen={isOpen}
handleClose={onClose}
position={EModalPosition.CENTER}
width={EModalWidth.XXL}
className="py-5 px-6"
>
<div className="flex flex-col sm:flex-row items-center gap-4">
<span className={cn("flex-shrink-0 grid place-items-center rounded-full size-10 bg-red-500/10 text-red-500")}>
<AlertTriangle className="size-6" aria-hidden="true" />
</span>
<div className="text-center sm:text-left">
<h3 className="text-lg font-medium">Delete this property</h3>
</div>
</div>
<div className="py-4 text-center sm:text-left text-sm text-custom-text-200">
<p>Deletion of properties may lead to loss of existing data.</p>
<p>Do you want to disable the property instead?</p>
</div>
<div className="px-1 pt-4 flex flex-col-reverse sm:flex-row sm:justify-between gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={onClose} disabled={isSubmitting}>
Cancel
</Button>
<div className="flex flex-col sm:flex-row gap-2 items-center sm:justify-end">
<Button
variant="outline-primary"
size="sm"
onClick={handleDisable}
className="w-full"
disabled={isSubmitting}
>
Yes, disable it
</Button>
<Button
variant="danger"
size="sm"
tabIndex={1}
onClick={handleDelete}
className="w-full"
disabled={isSubmitting}
>
No, delete it
</Button>
</div>
</div>
</ModalCore>
);
});

View File

@@ -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<TIssueProperty<EIssuePropertyType>>) => void;
error?: string;

View File

@@ -7,3 +7,4 @@ export * from "./dropdowns";
export * from "./attributes";
export * from "./mandatory-field-toggle";
export * from "./quick-actions";
export * from "./delete-confirmation-modal";

View File

@@ -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<TIssuePropertyOption>[] = [];
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<TIssueProperty<EIssuePropertyType>>) => {
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) =>
<IssuePropertyQuickActions
currentOperationMode={issuePropertyOperationMode}
isSubmitting={isSubmitting}
handleCreateUpdate={handleCreateUpdate}
handleDiscard={handleDiscard}
handleDelete={handleDelete}
handleIssuePropertyOperationMode={(mode) => setIssuePropertyOperationMode(mode)}
onCreateUpdate={handleCreateUpdate}
onDiscard={handleDiscard}
onDelete={handleDelete}
onDisable={async () => handlePropertyDataChange("is_active", false, true)}
onIssuePropertyOperationMode={(mode) => setIssuePropertyOperationMode(mode)}
/>
</div>
</div>

View File

@@ -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<void>;
handleDiscard: () => void;
handleDelete: () => Promise<void>;
handleIssuePropertyOperationMode: (mode: TOperationMode) => void;
onCreateUpdate: () => Promise<void>;
onDiscard: () => void;
onDisable: () => Promise<void>;
onDelete: () => Promise<void>;
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 (
<>
<AlertModalCore
<DeleteConfirmationModal
isOpen={isDeleteModalOpen}
handleClose={() => setIsDeleteModalOpen(false)}
handleSubmit={handleDeleteIssueProperty}
isSubmitting={isDeleteLoading}
title="Delete this property"
content={
<>
<p>Deletion of properties may lead to loss of existing data or weird behavior.</p>
<p>Do you want to disable the property instead?</p>
</>
}
onClose={() => setIsDeleteModalOpen(false)}
onDisable={onDisable}
onDelete={onDelete}
/>
<div
className={cn("items-center justify-end gap-1.5 pr-2", {
@@ -65,7 +54,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
className={cn(
"p-1 border-[0.5px] border-custom-border-300 rounded bg-custom-background-100 hover:bg-custom-background-90"
)}
onClick={handleCreateUpdate}
onClick={onCreateUpdate}
disabled={isSubmitting}
>
{isSubmitting ? <Spinner width="12px" height="12px" /> : <Check size={12} className="text-green-600" />}
@@ -79,7 +68,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
"bg-custom-background-80": isSubmitting,
}
)}
onClick={handleDiscard}
onClick={onDiscard}
disabled={isSubmitting}
>
<X size={12} className="text-red-500" />
@@ -91,7 +80,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
<Tooltip className="w-full shadow" tooltipContent="Edit" position="bottom">
<button
className="p-1 border-[0.5px] border-custom-border-300 rounded bg-custom-background-100 hover:bg-custom-background-90"
onClick={() => handleIssuePropertyOperationMode("update")}
onClick={() => onIssuePropertyOperationMode("update")}
>
<Pencil size={12} className="text-custom-text-300" />
</button>

View File

@@ -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<TIssueProperty<EIssuePropertyType>> = {
export const IssuePropertiesRoot = observer((props: TIssuePropertiesRoot) => {
const { issueTypeId } = props;
// router
const { projectId } = useParams();
// states
const [issuePropertyCreateList, setIssuePropertyCreateList] = useState<TIssuePropertyCreateList[]>([]);
// store hooks
const { getProjectIssuePropertiesLoader } = useIssueTypes();
const issueType = useIssueType(issueTypeId);
// derived values
const issuePropertiesLoader = getProjectIssuePropertiesLoader(projectId?.toString());
const properties = issueType?.properties;
// refs
const containerRef = useRef<HTMLDivElement>(null);
@@ -73,7 +78,14 @@ export const IssuePropertiesRoot = observer((props: TIssuePropertiesRoot) => {
<div className="w-full flex gap-2 items-center px-6">
<div className="text-base font-semibold">Properties</div>
</div>
{(properties && properties?.length > 0) || issuePropertyCreateList.length > 0 ? (
{issuePropertiesLoader === "init-loader" ? (
<Loader className="w-full space-y-4 p-6">
<Loader.Item height="30px" width="100%" />
<Loader.Item height="30px" width="100%" />
<Loader.Item height="30px" width="100%" />
<Loader.Item height="30px" width="100%" />
</Loader>
) : (properties && properties?.length > 0) || issuePropertyCreateList.length > 0 ? (
<IssuePropertyList
issueTypeId={issueTypeId}
issuePropertyCreateList={issuePropertyCreateList}

View File

@@ -38,7 +38,7 @@ export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPrope
const projectDetails = getProjectById(projectId);
const issueTypeDetails = issueType?.asJSON;
const activeProperties = issueType?.activeProperties;
const issueProperties = getProjectIssuePropertiesLoader(projectId);
const issuePropertiesLoader = getProjectIssuePropertiesLoader(projectId);
const isIssueTypeEnabled = isIssueTypeDisplayEnabled && projectDetails?.is_issue_type_enabled;
// fetch issue custom property values
@@ -93,7 +93,7 @@ export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPrope
// if issue types are not enabled, return null
if (!isIssueTypeEnabled) return null;
if (issueProperties === "init-loader") {
if (issuePropertiesLoader === "init-loader") {
return (
<Loader className="space-y-4 py-4">
<Loader.Item height="30px" />

View File

@@ -301,7 +301,12 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
handleFormChange={handleFormChange}
/>
{projectId && (
<IssueTypeSelect control={control} projectId={projectId} disabled={!!data?.sourceIssueId} />
<IssueTypeSelect
control={control}
projectId={projectId}
disabled={!!data?.sourceIssueId}
handleFormChange={handleFormChange}
/>
)}
</div>
)}

View File

@@ -16,12 +16,13 @@ import { useFlag } from "@/plane-web/hooks/store";
type TIssueTypeSelectProps = {
control: Control<TIssue>;
projectId: string;
projectId: string | null;
disabled?: boolean;
handleFormChange: () => void;
};
export const IssueTypeSelect: React.FC<TIssueTypeSelectProps> = 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<TIssueTypeSelectProps> = observer((props)
disabled={disabled}
handleIssueTypeChange={(issueTypeId) => {
onChange(issueTypeId);
handleFormChange();
}}
/>
)}

View File

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

View File

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

View File

@@ -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<TIssueProperty<EIssuePropertyType>>
): Promise<TIssueProperty<EIssuePropertyType>> {
data: Partial<TIssueProperty<EIssuePropertyType>>,
options?: Partial<TIssuePropertyOption>[] | undefined
): Promise<TIssuePropertyResponse> {
return this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-types/${issueTypeId}/issue-properties/`,
data
{
...data,
options: options ?? [],
}
)
.then((response) => response?.data)
.catch((error) => {

View File

@@ -22,13 +22,11 @@ export class IssuePropertyOptionsService extends APIService {
workspaceSlug: string,
projectId: string,
issuePropertyId: string,
data: Partial<TIssuePropertyOption>[]
): Promise<TIssuePropertyOption[]> {
data: Partial<TIssuePropertyOption>
): Promise<TIssuePropertyOption> {
return this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-properties/${issuePropertyId}/options/`,
{
options: data,
}
data
)
.then((response) => response?.data)
.catch((error) => {

View File

@@ -30,9 +30,7 @@ export interface IIssueProperty<T extends EIssuePropertyType> extends TIssueProp
// actions
updateProperty: (issueTypeId: string, propertyData: Partial<TIssueProperty<T>>) => Promise<void>;
addPropertyOption: (propertyOptionData: TIssuePropertyOption) => void;
createPropertyOptions: (
propertyOptions: Partial<TIssuePropertyOption>[]
) => Promise<TIssuePropertyOption[] | undefined>;
createPropertyOption: (propertyOption: Partial<TIssuePropertyOption>) => Promise<TIssuePropertyOption | undefined>;
deletePropertyOption: (propertyOptionId: string) => Promise<void>;
}
@@ -45,7 +43,7 @@ export class IssueProperty<T extends EIssuePropertyType> 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<T extends EIssuePropertyType> implements IIssueProper
// actions
updateProperty: action,
addPropertyOption: action,
createPropertyOptions: action,
createPropertyOption: action,
deletePropertyOption: action,
});
@@ -211,26 +209,24 @@ export class IssueProperty<T extends EIssuePropertyType> implements IIssueProper
/**
* @description Create a new property option
* @param {Partial<TIssuePropertyOption>[]} propertyOptions
* @param {Partial<TIssuePropertyOption>} propertyOption
*/
createPropertyOptions = async (propertyOptions: Partial<TIssuePropertyOption>[]) => {
createPropertyOption = async (propertyOption: Partial<TIssuePropertyOption>) => {
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;

View File

@@ -25,7 +25,8 @@ export interface IIssueType extends TIssueType {
updateType: (issueTypeData: Partial<TIssueType>) => Promise<TIssueType | undefined>;
addProperty: (propertyData: TIssueProperty<EIssuePropertyType>, propertyOptions?: TIssuePropertyOption[]) => void;
createProperty: (
propertyData: Partial<TIssueProperty<EIssuePropertyType>>
propertyData: Partial<TIssueProperty<EIssuePropertyType>>,
propertyOptions?: Partial<TIssuePropertyOption>[]
) => Promise<TIssueProperty<EIssuePropertyType> | undefined>;
deleteProperty: (propertyId: string) => Promise<void>;
}
@@ -196,16 +197,25 @@ export class IssueType implements IIssueType {
* @description Create an issue property
* @param propertyData Issue property data
*/
createProperty = async (propertyData: Partial<TIssueProperty<EIssuePropertyType>>) => {
createProperty = async (
propertyData: Partial<TIssueProperty<EIssuePropertyType>>,
propertyOptions?: Partial<TIssuePropertyOption>[]
) => {
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;

View File

@@ -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<T extends EIssuePropertyType> extends TBaseIssue
property_type: T | undefined;
settings: TIssuePropertySettingsMap[T] | undefined;
}
// Issue property response
export interface TIssuePropertyResponse {
property_detail: TIssueProperty<EIssuePropertyType>;
options: TIssuePropertyOption[];
}

View File

@@ -20,11 +20,11 @@ export type TissuePropertyTypeDetails<T extends EIssuePropertyType> = {
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];
};
};