mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
Issue types properties improvement (#816)
* chore: fix properties overflow in peek overview full screnn. * style: improve issue type enable/ disable UI and UX: * chore: merge create and update issue type modal. * chore: remove unwanted toast messages. * chore: handle options integrity error while creating/ updating property. * chore: empty options error validation.
This commit is contained in:
@@ -194,11 +194,20 @@ class IssuePropertyEndpoint(BaseAPIView):
|
||||
|
||||
# Check if the property type is option and create the options
|
||||
if issue_property.property_type == "OPTION":
|
||||
self.create_options(issue_property, options)
|
||||
self.update_property_default_options(issue_property)
|
||||
# Reset the default options if the property is required
|
||||
if issue_property.is_required:
|
||||
self.reset_options_default(issue_property)
|
||||
try:
|
||||
self.create_options(issue_property, options)
|
||||
self.update_property_default_options(issue_property)
|
||||
# Reset the default options if the property is required
|
||||
if issue_property.is_required:
|
||||
self.reset_options_default(issue_property)
|
||||
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{
|
||||
"error": "An option with the same name already exists in this property",
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
# generate the response with the new data and options
|
||||
@@ -282,13 +291,22 @@ class IssuePropertyEndpoint(BaseAPIView):
|
||||
serializer.save()
|
||||
|
||||
if issue_property.property_type == "OPTION":
|
||||
self.handle_options_create_update(
|
||||
issue_property, options, slug, project_id
|
||||
)
|
||||
self.update_property_default_options(issue_property)
|
||||
# Reset the default options if the property is required
|
||||
if issue_property.is_required:
|
||||
self.reset_options_default(issue_property)
|
||||
try:
|
||||
self.handle_options_create_update(
|
||||
issue_property, options, slug, project_id
|
||||
)
|
||||
self.update_property_default_options(issue_property)
|
||||
# Reset the default options if the property is required
|
||||
if issue_property.is_required:
|
||||
self.reset_options_default(issue_property)
|
||||
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{
|
||||
"error": "An option with the same name already exists in this property",
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
response = {
|
||||
**serializer.data,
|
||||
|
||||
@@ -241,7 +241,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`h-full !w-[400px] flex-shrink-0 border-l border-custom-border-200 p-4 py-5 ${
|
||||
className={`h-full !w-[400px] flex-shrink-0 border-l border-custom-border-200 p-4 py-5 overflow-hidden vertical-scrollbar scrollbar-sm ${
|
||||
is_archived ? "pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { FC } from "react";
|
||||
import { TLogoProps } from "@plane/types";
|
||||
// ui
|
||||
import { LayersIcon, LUCIDE_ICONS_LIST } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
@@ -37,7 +38,7 @@ export const IssueTypeLogo: FC<Props> = (props) => {
|
||||
width={size}
|
||||
height={size}
|
||||
style={{
|
||||
color: icon_props?.color,
|
||||
color: icon_props?.color ?? "#ffffff",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -45,7 +46,7 @@ export const IssueTypeLogo: FC<Props> = (props) => {
|
||||
style={{
|
||||
height: size,
|
||||
width: size,
|
||||
color: icon_props?.color,
|
||||
color: icon_props?.color ?? "#ffffff",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./form";
|
||||
export * from "./create-modal";
|
||||
export * from "./update-modal";
|
||||
export * from "./modal";
|
||||
|
||||
@@ -5,47 +5,54 @@ import { EModalPosition, EModalWidth, ModalCore, setToast, TOAST_TYPE } from "@p
|
||||
import { getRandomIconName } from "@/helpers/emoji.helper";
|
||||
// plane web components
|
||||
import { CreateOrUpdateIssueTypeForm } from "@/plane-web/components/issue-types/";
|
||||
// hooks
|
||||
// plane web helpers
|
||||
import { getRandomBackgroundColor } from "@/plane-web/helpers/issue-type.helper";
|
||||
import { useIssueTypes } from "@/plane-web/hooks/store";
|
||||
// plane web
|
||||
import { useIssueType, useIssueTypes } from "@/plane-web/hooks/store";
|
||||
// plane web types
|
||||
import { TIssueType } from "@/plane-web/types";
|
||||
// plane web helpers
|
||||
|
||||
type Props = {
|
||||
issueTypeId: string | null;
|
||||
isModalOpen: boolean;
|
||||
handleModalClose: () => void;
|
||||
};
|
||||
|
||||
export const defaultIssueTypeData: Partial<TIssueType> = {
|
||||
const defaultIssueTypeData: Partial<TIssueType> = {
|
||||
id: undefined,
|
||||
name: "",
|
||||
description: "",
|
||||
};
|
||||
|
||||
export const CreateIssueTypeModal: FC<Props> = (props) => {
|
||||
const { isModalOpen, handleModalClose } = props;
|
||||
export const CreateOrUpdateIssueTypeModal: FC<Props> = (props) => {
|
||||
const { issueTypeId, isModalOpen, handleModalClose } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [issueTypeFormData, setIssueTypeFormData] = useState<Partial<TIssueType>>(defaultIssueTypeData);
|
||||
const [issueTypeFormData, setIssueTypeFormData] = useState<Partial<TIssueType> | undefined>(undefined);
|
||||
// store hooks
|
||||
const { createType } = useIssueTypes();
|
||||
const issueType = useIssueType(issueTypeId);
|
||||
// derived values
|
||||
const issueTypeDetail = issueType?.asJSON;
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen) {
|
||||
setIssueTypeFormData({
|
||||
...defaultIssueTypeData,
|
||||
logo_props: {
|
||||
in_use: "icon",
|
||||
icon: {
|
||||
name: getRandomIconName(),
|
||||
color: "#ffffff",
|
||||
background_color: getRandomBackgroundColor(),
|
||||
if (issueTypeDetail) {
|
||||
setIssueTypeFormData(issueTypeDetail);
|
||||
} else {
|
||||
setIssueTypeFormData({
|
||||
...defaultIssueTypeData,
|
||||
logo_props: {
|
||||
in_use: "icon",
|
||||
icon: {
|
||||
name: getRandomIconName(),
|
||||
background_color: getRandomBackgroundColor(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
}, [issueTypeDetail, isModalOpen]);
|
||||
|
||||
// handlers
|
||||
const handleFormDataChange = <T extends keyof TIssueType>(key: T, value: TIssueType[T]) =>
|
||||
@@ -56,7 +63,8 @@ export const CreateIssueTypeModal: FC<Props> = (props) => {
|
||||
handleModalClose();
|
||||
};
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
const handleCreateIssueType = async () => {
|
||||
if (!issueTypeFormData) return;
|
||||
setIsSubmitting(true);
|
||||
await createType(issueTypeFormData)
|
||||
.then(() => {
|
||||
@@ -79,6 +87,32 @@ export const CreateIssueTypeModal: FC<Props> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateIssueType = async () => {
|
||||
if (!issueTypeFormData) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
await issueType
|
||||
?.updateType(issueTypeFormData)
|
||||
.then(() => {
|
||||
handleModalClearAndClose();
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: `Issue type ${issueTypeFormData?.name} updated successfully.`,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: `Failed to update issue type. Please try again!`,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
if (!isModalOpen) return null;
|
||||
|
||||
return (
|
||||
@@ -89,11 +123,11 @@ export const CreateIssueTypeModal: FC<Props> = (props) => {
|
||||
width={EModalWidth.XXL}
|
||||
>
|
||||
<CreateOrUpdateIssueTypeForm
|
||||
formData={issueTypeFormData}
|
||||
formData={issueTypeFormData ?? defaultIssueTypeData}
|
||||
isSubmitting={isSubmitting}
|
||||
handleFormDataChange={handleFormDataChange}
|
||||
handleModalClose={handleModalClearAndClose}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleFormSubmit={issueTypeId ? handleUpdateIssueType : handleCreateIssueType}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
@@ -1,79 +0,0 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// plane web components
|
||||
import { CreateOrUpdateIssueTypeForm, defaultIssueTypeData } from "@/plane-web/components/issue-types/";
|
||||
// hooks
|
||||
import { useIssueType } from "@/plane-web/hooks/store";
|
||||
// plane web types
|
||||
import { TIssueType } from "@/plane-web/types";
|
||||
|
||||
type Props = {
|
||||
data: Partial<TIssueType>;
|
||||
isModalOpen: boolean;
|
||||
handleModalClose: () => void;
|
||||
};
|
||||
|
||||
export const UpdateIssueTypeModal: FC<Props> = (props) => {
|
||||
const { data, isModalOpen, handleModalClose } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [issueTypeFormData, setIssueTypeFormData] = useState<Partial<TIssueType> | undefined>(undefined);
|
||||
// store hooks
|
||||
const issueType = useIssueType(data?.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen) {
|
||||
setIssueTypeFormData(data);
|
||||
}
|
||||
}, [data, isModalOpen]);
|
||||
|
||||
// handlers
|
||||
const handleFormDataChange = <T extends keyof TIssueType>(key: T, value: TIssueType[T]) =>
|
||||
setIssueTypeFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
if (!issueTypeFormData) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
await issueType
|
||||
?.updateType(issueTypeFormData)
|
||||
.then(() => {
|
||||
handleModalClose();
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: `Issue type ${issueTypeFormData?.name} updated successfully.`,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: `Failed to update issue type. Please try again!`,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
if (!isModalOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalCore
|
||||
isOpen={isModalOpen}
|
||||
handleClose={handleModalClose}
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.XXL}
|
||||
>
|
||||
<CreateOrUpdateIssueTypeForm
|
||||
formData={issueTypeFormData ?? defaultIssueTypeData}
|
||||
isSubmitting={isSubmitting}
|
||||
handleFormDataChange={handleFormDataChange}
|
||||
handleModalClose={handleModalClose}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
@@ -13,10 +13,11 @@ type TIssueTypeListItem = {
|
||||
issueTypeId: string;
|
||||
isOpen: boolean;
|
||||
onToggle: (issueTypeId: string) => void;
|
||||
onEditIssueTypeIdChange: (issueTypeId: string) => void;
|
||||
};
|
||||
|
||||
export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
|
||||
const { issueTypeId, isOpen, onToggle } = props;
|
||||
const { issueTypeId, isOpen, onToggle, onEditIssueTypeIdChange } = props;
|
||||
// store hooks
|
||||
const issueType = useIssueType(issueTypeId);
|
||||
// derived values
|
||||
@@ -62,18 +63,27 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex gap-4">
|
||||
{issueTypeDetail?.is_default && (
|
||||
<div className="py-1 px-4 text-xs rounded font-medium text-custom-text-300 bg-custom-background-80/70">
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
{!issueTypeDetail?.is_active && (
|
||||
<div className="flex-shrink-0 py-0.5 px-2 text-xs rounded font-medium text-red-600 bg-red-600/10">
|
||||
Disabled
|
||||
</div>
|
||||
)}
|
||||
{!issueTypeDetail?.is_default && <IssueTypeQuickActions issueTypeId={issueTypeId} />}
|
||||
<div className="flex-shrink-0 flex">
|
||||
<div className="w-20">
|
||||
{issueTypeDetail?.is_default && (
|
||||
<div className="py-1 px-2 text-xs rounded font-medium text-custom-text-300 bg-custom-background-80/70">
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
{!issueTypeDetail?.is_active && (
|
||||
<div className="flex-shrink-0 py-1 px-2 text-xs rounded font-medium text-red-600 bg-red-600/10">
|
||||
Disabled
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!issueTypeDetail?.is_default && (
|
||||
<IssueTypeQuickActions
|
||||
issueTypeId={issueTypeId}
|
||||
onEditIssueTypeIdChange={onEditIssueTypeIdChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ import { IssueTypeListItem } from "@/plane-web/components/issue-types";
|
||||
// plane web hooks
|
||||
import { useIssueTypes } from "@/plane-web/hooks/store";
|
||||
|
||||
export const IssueTypesList = observer(() => {
|
||||
type TIssueTypesList = {
|
||||
onEditIssueTypeIdChange: (issueTypeId: string) => void;
|
||||
};
|
||||
|
||||
export const IssueTypesList = observer((props: TIssueTypesList) => {
|
||||
const { onEditIssueTypeIdChange } = props;
|
||||
// router
|
||||
const { projectId } = useParams();
|
||||
// states
|
||||
@@ -48,6 +53,7 @@ export const IssueTypesList = observer(() => {
|
||||
(issueTypeId === currentProjectDefaultIssueType?.id && currentProjectIssueTypeIds.length === 1)
|
||||
}
|
||||
onToggle={handleIssueTypeListToggle}
|
||||
onEditIssueTypeIdChange={onEditIssueTypeIdChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IssuePropertyOptionsRoot,
|
||||
PropertyMultiSelect,
|
||||
PropertySettingsConfiguration,
|
||||
TIssuePropertyFormError,
|
||||
} from "@/plane-web/components/issue-types/properties";
|
||||
// plane web constants
|
||||
import { ISSUE_PROPERTY_SETTINGS_CONFIGURATIONS } from "@/plane-web/constants/issue-properties";
|
||||
@@ -22,10 +23,11 @@ type TDropdownAttributesProps = {
|
||||
value: TIssueProperty<EIssuePropertyType.OPTION>[K],
|
||||
shouldSync?: boolean
|
||||
) => void;
|
||||
error?: TIssuePropertyFormError;
|
||||
};
|
||||
|
||||
export const DropdownAttributes = observer((props: TDropdownAttributesProps) => {
|
||||
const { issueTypeId, dropdownPropertyDetail, currentOperationMode, onDropdownDetailChange } = props;
|
||||
const { issueTypeId, dropdownPropertyDetail, currentOperationMode, onDropdownDetailChange, error } = props;
|
||||
// store hooks
|
||||
const issueType = useIssueType(issueTypeId);
|
||||
// derived values
|
||||
@@ -65,7 +67,7 @@ export const DropdownAttributes = observer((props: TDropdownAttributesProps) =>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<IssuePropertyOptionsRoot issuePropertyId={dropdownPropertyDetail.id} />
|
||||
<IssuePropertyOptionsRoot issuePropertyId={dropdownPropertyDetail.id} error={error?.options} />
|
||||
<div className="pt-2 px-1">
|
||||
<div className="text-xs font-medium text-custom-text-300">Default • Optional</div>
|
||||
<DefaultOptionSelect isMultiSelect={dropdownPropertyDetail.is_multi} isDisabled={isOptionDefaultDisabled} />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TIssuePropertyOptionCreateUpdateData } from "@/plane-web/types";
|
||||
type TIssuePropertyCreateOptionItem = {
|
||||
propertyOptionCreateListData: TIssuePropertyOptionCreateUpdateData;
|
||||
updateCreateListData: (value: TIssuePropertyOptionCreateUpdateData) => void;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const IssuePropertyCreateOptionItem = observer(
|
||||
@@ -15,13 +16,14 @@ export const IssuePropertyCreateOptionItem = observer(
|
||||
props: TIssuePropertyCreateOptionItem,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
const { propertyOptionCreateListData, updateCreateListData } = props;
|
||||
const { propertyOptionCreateListData, updateCreateListData, error} = props;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="w-full px-1 pr-2">
|
||||
<IssuePropertyOptionItem
|
||||
propertyOptionData={propertyOptionCreateListData}
|
||||
updateOptionData={updateCreateListData}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,20 +11,21 @@ type TIssuePropertyOptionItem = {
|
||||
optionId?: string;
|
||||
propertyOptionData: TIssuePropertyOptionCreateUpdateData;
|
||||
updateOptionData: (value: TIssuePropertyOptionCreateUpdateData) => void;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const IssuePropertyOptionItem: FC<TIssuePropertyOptionItem> = observer((props) => {
|
||||
const { optionId, propertyOptionData, updateOptionData } = props;
|
||||
const { optionId, propertyOptionData, updateOptionData, error: optionsError } = props;
|
||||
// derived values
|
||||
const { key, ...propertyOptionCreateData } = propertyOptionData;
|
||||
// states
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(optionsError);
|
||||
const [optionData, setOptionData] = useState<Partial<TIssuePropertyOption>>(propertyOptionCreateData);
|
||||
|
||||
useEffect(() => {
|
||||
if (optionId && !optionData.name) setError("Option name is required.");
|
||||
else setError(undefined);
|
||||
}, [optionId, optionData]);
|
||||
else setError(optionsError ?? undefined);
|
||||
}, [optionId, optionData, optionsError]);
|
||||
|
||||
// handle create/ update operation
|
||||
const handleCreateUpdate = async () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { GripVertical } from "lucide-react";
|
||||
import { Sortable, Tooltip } from "@plane/ui";
|
||||
// plane web components
|
||||
import {
|
||||
IssuePropertyCreateOptionItem,
|
||||
IssuePropertyOptionItem,
|
||||
@@ -15,10 +14,11 @@ import { TIssuePropertyOptionCreateUpdateData } from "@/plane-web/types";
|
||||
|
||||
type TIssuePropertyOptionsRoot = {
|
||||
issuePropertyId: string | undefined;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const IssuePropertyOptionsRoot: FC<TIssuePropertyOptionsRoot> = observer((props) => {
|
||||
const { issuePropertyId } = props;
|
||||
const { issuePropertyId, error } = props;
|
||||
// store hooks
|
||||
const { propertyOptions, handlePropertyOptionsList } = usePropertyOptions();
|
||||
// derived values
|
||||
@@ -116,6 +116,7 @@ export const IssuePropertyOptionsRoot: FC<TIssuePropertyOptionsRoot> = observer(
|
||||
scrollIntoElementView();
|
||||
}, 0);
|
||||
}}
|
||||
error={index === 0 ? error : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// plane web components
|
||||
import { SelectedAttributeProperties } from "@/plane-web/components/issue-types/properties";
|
||||
import { SelectedAttributeProperties, TIssuePropertyFormError } from "@/plane-web/components/issue-types/properties";
|
||||
// plane web helpers
|
||||
import { getIssuePropertyAttributeDisplayName } from "@/plane-web/helpers/issue-properties.helper";
|
||||
// plane web types
|
||||
@@ -25,14 +25,16 @@ type TPropertyAttributesDropdownProps = {
|
||||
shouldSync?: boolean
|
||||
) => void;
|
||||
disabled?: boolean;
|
||||
error?: TIssuePropertyFormError;
|
||||
};
|
||||
|
||||
export const PropertyAttributesDropdown = observer((props: TPropertyAttributesDropdownProps) => {
|
||||
const { issueTypeId, propertyDetail, currentOperationMode, onPropertyDetailChange, disabled } = props;
|
||||
const { issueTypeId, propertyDetail, currentOperationMode, onPropertyDetailChange, disabled, error } = props;
|
||||
// states
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// derived values
|
||||
const doesAttributeHasError = Boolean(error?.is_multi) || Boolean(error?.options);
|
||||
const attributeDisplayName = getIssuePropertyAttributeDisplayName(propertyDetail);
|
||||
// list of property types that should not be allowed to change attributes
|
||||
const DISABLE_ATTRIBUTE_CHANGE_LIST = [EIssuePropertyType.BOOLEAN, EIssuePropertyType.DATETIME];
|
||||
@@ -63,6 +65,7 @@ export const PropertyAttributesDropdown = observer((props: TPropertyAttributesDr
|
||||
{
|
||||
"bg-custom-background-90 cursor-not-allowed": disabled,
|
||||
"py-2": !attributeDisplayName,
|
||||
"border-red-500": doesAttributeHasError,
|
||||
}
|
||||
)}
|
||||
disabled={disabled}
|
||||
@@ -86,6 +89,7 @@ export const PropertyAttributesDropdown = observer((props: TPropertyAttributesDr
|
||||
propertyDetail={propertyDetail}
|
||||
currentOperationMode={currentOperationMode}
|
||||
onPropertyDetailChange={onPropertyDetailChange}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Panel>,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MemberPickerAttributes,
|
||||
NumberAttributes,
|
||||
TextAttributes,
|
||||
TIssuePropertyFormError,
|
||||
} from "@/plane-web/components/issue-types/properties";
|
||||
// plane web helpers
|
||||
import { getIssuePropertyTypeKey } from "@/plane-web/helpers/issue-properties.helper";
|
||||
@@ -23,10 +24,11 @@ type TSelectedPropertyAttributesProps = {
|
||||
shouldSync?: boolean
|
||||
) => void;
|
||||
disabled?: boolean;
|
||||
error?: TIssuePropertyFormError;
|
||||
};
|
||||
|
||||
export const SelectedAttributeProperties = observer((props: TSelectedPropertyAttributesProps) => {
|
||||
const { issueTypeId, propertyDetail, currentOperationMode, onPropertyDetailChange } = props;
|
||||
const { issueTypeId, propertyDetail, currentOperationMode, onPropertyDetailChange, error } = props;
|
||||
|
||||
const ISSUE_PROPERTY_ATTRIBUTE_DETAILS: Partial<Record<TIssuePropertyTypeKeys, JSX.Element>> = {
|
||||
TEXT: (
|
||||
@@ -51,6 +53,7 @@ export const SelectedAttributeProperties = observer((props: TSelectedPropertyAtt
|
||||
dropdownPropertyDetail={propertyDetail as Partial<TIssueProperty<EIssuePropertyType.OPTION>>}
|
||||
currentOperationMode={currentOperationMode}
|
||||
onDropdownDetailChange={onPropertyDetailChange}
|
||||
error={error}
|
||||
/>
|
||||
),
|
||||
BOOLEAN: (
|
||||
|
||||
@@ -36,11 +36,13 @@ type TIssuePropertyListItem = {
|
||||
handleIssuePropertyCreateList: (mode: TCreationListModes, value: TIssuePropertyCreateList) => void;
|
||||
};
|
||||
|
||||
export type TIssuePropertyError = {
|
||||
export type TIssuePropertyFormError = {
|
||||
[key in keyof TIssueProperty<EIssuePropertyType>]?: string;
|
||||
} & {
|
||||
options?: string;
|
||||
};
|
||||
|
||||
const defaultIssuePropertyError: TIssuePropertyError = {
|
||||
const defaultIssuePropertyError: TIssuePropertyFormError = {
|
||||
display_name: "",
|
||||
property_type: "",
|
||||
};
|
||||
@@ -69,7 +71,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
|
||||
);
|
||||
const [issuePropertyData, setIssuePropertyData] =
|
||||
useState<Partial<TIssueProperty<EIssuePropertyType>>>(issuePropertyDetail);
|
||||
const [issuePropertyError, setIssuePropertyError] = useState<TIssuePropertyError>(defaultIssuePropertyError);
|
||||
const [issuePropertyError, setIssuePropertyError] = useState<TIssuePropertyFormError>(defaultIssuePropertyError);
|
||||
// derived values
|
||||
// check if mandatory field is disabled for the property
|
||||
const isMandatoryFieldDisabled =
|
||||
@@ -103,6 +105,11 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
|
||||
error.property_type = "You must select a property type.";
|
||||
hasError = true;
|
||||
}
|
||||
const nonEmptyPropertyOptions = propertyOptions.filter((option) => !!option.name);
|
||||
if (issuePropertyData.property_type === EIssuePropertyType.OPTION && nonEmptyPropertyOptions.length === 0) {
|
||||
error.options = "You must add at least one option.";
|
||||
hasError = true;
|
||||
}
|
||||
setIssuePropertyError(error);
|
||||
return hasError;
|
||||
};
|
||||
@@ -336,6 +343,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
|
||||
currentOperationMode={issuePropertyOperationMode}
|
||||
onPropertyDetailChange={handlePropertyDataChange}
|
||||
disabled={!issuePropertyData.property_type}
|
||||
error={issuePropertyError}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-20 text-center">
|
||||
|
||||
@@ -107,7 +107,6 @@ export const IssuePropertiesRoot = observer((props: TIssuePropertiesRoot) => {
|
||||
...defaultIssueProperty,
|
||||
})
|
||||
}
|
||||
disabled={!issueType?.is_active}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
Add new property
|
||||
|
||||
@@ -7,20 +7,18 @@ import { Pencil } from "lucide-react";
|
||||
import { setPromiseToast, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// plane web components
|
||||
import { UpdateIssueTypeModal } from "@/plane-web/components/issue-types";
|
||||
// plane web hooks
|
||||
import { useIssueType } from "@/plane-web/hooks/store";
|
||||
|
||||
type Props = {
|
||||
issueTypeId: string;
|
||||
onEditIssueTypeIdChange: (issueTypeId: string) => void;
|
||||
};
|
||||
|
||||
export const IssueTypeQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { issueTypeId } = props;
|
||||
const { issueTypeId, onEditIssueTypeIdChange } = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreateUpdateModalOpen, setIsCreateUpdateModalOpen] = useState<boolean>(false);
|
||||
// store hooks
|
||||
const issueType = useIssueType(issueTypeId);
|
||||
// derived values
|
||||
@@ -56,13 +54,11 @@ export const IssueTypeQuickActions: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateIssueTypeModal
|
||||
data={issueTypeDetail}
|
||||
isModalOpen={isCreateUpdateModalOpen}
|
||||
handleModalClose={() => setIsCreateUpdateModalOpen(false)}
|
||||
/>
|
||||
<div className={cn("flex items-center justify-center gap-3")}>
|
||||
{issueType?.is_active && (
|
||||
<div className={cn("flex items-center justify-center gap-1 px-2")}>
|
||||
<div className="w-12">
|
||||
<ToggleSwitch value={!!isIssueTypeEnabled} onChange={handleEnableDisable} disabled={isLoading} />
|
||||
</div>
|
||||
<div className="w-6">
|
||||
<Tooltip className="w-full shadow" tooltipContent="Edit" position="bottom">
|
||||
<button
|
||||
className={cn(
|
||||
@@ -73,15 +69,14 @@ export const IssueTypeQuickActions: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsCreateUpdateModalOpen(true);
|
||||
onEditIssueTypeIdChange(issueTypeId);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Pencil size={14} className="text-custom-text-300" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ToggleSwitch value={!!isIssueTypeEnabled} onChange={handleEnableDisable} disabled={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { CreateIssueTypeModal, IssueTypeEmptyState, IssueTypesList } from "@/plane-web/components/issue-types";
|
||||
import { IssueTypeEmptyState, IssueTypesList, CreateOrUpdateIssueTypeModal } from "@/plane-web/components/issue-types";
|
||||
// plane web hooks
|
||||
import { useFlag } from "@/plane-web/hooks/store";
|
||||
|
||||
@@ -16,11 +16,17 @@ export const IssueTypesRoot = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// states
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [editIssueTypeId, setEditIssueTypeId] = useState<string | null>(null);
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
// derived values
|
||||
const isIssueTypeSettingsEnabled = useFlag(workspaceSlug?.toString(), "ISSUE_TYPE_SETTINGS");
|
||||
|
||||
const handleEditIssueTypeIdChange = (issueTypeId: string) => {
|
||||
setEditIssueTypeId(issueTypeId);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-full pb-8">
|
||||
<div className="flex items-center justify-between border-b border-custom-border-100 py-3.5 pr-4 gap-14">
|
||||
@@ -33,13 +39,20 @@ export const IssueTypesRoot = observer(() => {
|
||||
</div>
|
||||
<div className="my-2 pr-4 h-full overflow-y-scroll vertical-scrollbar scrollbar-sm">
|
||||
{isIssueTypeSettingsEnabled && currentProjectDetails?.is_issue_type_enabled ? (
|
||||
<IssueTypesList />
|
||||
<IssueTypesList onEditIssueTypeIdChange={handleEditIssueTypeIdChange} />
|
||||
) : (
|
||||
<IssueTypeEmptyState workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
)}
|
||||
</div>
|
||||
{/* Modal */}
|
||||
<CreateIssueTypeModal isModalOpen={isModalOpen} handleModalClose={() => setIsModalOpen(false)} />
|
||||
<CreateOrUpdateIssueTypeModal
|
||||
issueTypeId={editIssueTypeId}
|
||||
isModalOpen={isModalOpen}
|
||||
handleModalClose={() => {
|
||||
setEditIssueTypeId(null);
|
||||
setIsModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -66,28 +66,18 @@ export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPrope
|
||||
[propertyId]: value,
|
||||
}));
|
||||
// update the property value
|
||||
await issuePropertyValuesService
|
||||
.update(workspaceSlug, projectId, issueId, propertyId, value)
|
||||
.then(() => {
|
||||
// TODO: remove
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Property update successfully.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
// revert the value if update fails
|
||||
setIssuePropertyValues((prev) => ({
|
||||
...prev,
|
||||
[propertyId]: beforeUpdateValue,
|
||||
}));
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.error ?? "Property could not be update. Please try again.",
|
||||
});
|
||||
await issuePropertyValuesService.update(workspaceSlug, projectId, issueId, propertyId, value).catch((error) => {
|
||||
// revert the value if update fails
|
||||
setIssuePropertyValues((prev) => ({
|
||||
...prev,
|
||||
[propertyId]: beforeUpdateValue,
|
||||
}));
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.error ?? "Property could not be update. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// if issue types are not enabled, return null
|
||||
|
||||
@@ -289,12 +289,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
.then(() => {
|
||||
// reset issue property values
|
||||
setIssuePropertyValues({});
|
||||
// TODO: remove
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Custom properties created successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
|
||||
Reference in New Issue
Block a user