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:
Prateek Shourya
2024-08-12 20:55:36 +05:30
committed by GitHub
parent 8197419b69
commit 1d3c2ce996
20 changed files with 192 additions and 191 deletions

View File

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

View File

@@ -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" : ""
}`}
>

View File

@@ -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",
}}
/>
)}

View File

@@ -1,3 +1,2 @@
export * from "./form";
export * from "./create-modal";
export * from "./update-modal";
export * from "./modal";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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