Issue types properties improvement (#822)

* chore: update dropdown options component and fix overflow issue.

* chore: minor fix in issue type logo prop.

* chore: add integrity error message for issue types.

* style: UI and UX copy improvements.

* chore: options duplicate check and sanitization before API call.

* chore: change options create/ update priority.

* chore: remove integrity error message for issue type.

* chore: update options proeprty dropdown button.
This commit is contained in:
Prateek Shourya
2024-08-14 14:59:48 +05:30
committed by GitHub
parent 244d84129e
commit fa2ce51efc
19 changed files with 188 additions and 98 deletions

View File

@@ -7,6 +7,8 @@ from plane.ee.serializers import IssuePropertyActivitySerializer
from plane.ee.models import IssuePropertyActivity
from plane.ee.views.base import BaseAPIView
from plane.ee.permissions import ProjectEntityPermission
from plane.payment.flags.flag_decorator import check_feature_flag
from plane.payment.flags.flag import FeatureFlag
class IssuePropertyActivityEndpoint(BaseAPIView):
@@ -15,6 +17,7 @@ class IssuePropertyActivityEndpoint(BaseAPIView):
ProjectEntityPermission,
]
@check_feature_flag(FeatureFlag.ISSUE_TYPE_DISPLAY)
def get(self, request, slug, project_id, issue_id):
# Get the order by
order_by = request.GET.get("order_by", "-created_at")

View File

@@ -68,9 +68,6 @@ class IssuePropertyEndpoint(BaseAPIView):
else:
bulk_create_options.append(option)
if bulk_create_options:
self.create_options(issue_property, bulk_create_options)
if bulk_update_options:
for option in bulk_update_options:
issue_property_option = IssuePropertyOption.objects.get(
@@ -83,9 +80,11 @@ class IssuePropertyEndpoint(BaseAPIView):
issue_property_option, data=option, partial=True
)
option_serializer.is_valid(raise_exception=True)
option_serializer.save()
if bulk_create_options:
self.create_options(issue_property, bulk_create_options)
def reset_options_default(self, issue_property):
# Reset all the default options
IssuePropertyOption.objects.filter(

View File

@@ -16,7 +16,7 @@ const IssueTypesSettingsPage = observer(() => {
const { currentProjectDetails } = useProject();
// derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Issue types` : undefined;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Issue Types` : undefined;
return (
<>

View File

@@ -20,6 +20,9 @@ export const IssueTypeLogo: FC<Props> = (props) => {
// derived values
const LucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === icon_props?.name);
// if no value, return empty fragment
if (!icon_props?.name && !isDefault) return <></>;
return (
<>
<span
@@ -38,23 +41,16 @@ export const IssueTypeLogo: FC<Props> = (props) => {
color: icon_props?.color ?? "#ffffff", // fallback color
}}
/>
) : LucideIcon ? (
<LucideIcon.element
style={{
height: size,
width: size,
color: icon_props?.color ?? "#ffffff", // fallback color
}}
/>
) : (
// fallback icon
<OctagonAlert
style={{
height: size,
width: size,
color: icon_props?.color ?? "#dc2626",
}}
/>
LucideIcon && (
<LucideIcon.element
style={{
height: size,
width: size,
color: icon_props?.color ?? "#ffffff", // fallback color
}}
/>
)
)}
</span>
</>

View File

@@ -75,11 +75,11 @@ export const CreateOrUpdateIssueTypeModal: FC<Props> = (props) => {
message: `Issue type created successfully.`,
});
})
.catch(() => {
.catch((error) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: `Failed to create issue type. Please try again!`,
message: error?.error ?? `Failed to create issue type. Please try again!`,
});
})
.finally(() => {
@@ -101,11 +101,11 @@ export const CreateOrUpdateIssueTypeModal: FC<Props> = (props) => {
message: `Issue type ${issueTypeFormData?.name} updated successfully.`,
});
})
.catch(() => {
.catch((error) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: `Failed to update issue type. Please try again!`,
message: error?.error ?? `Failed to update issue type. Please try again!`,
});
})
.finally(() => {

View File

@@ -4,6 +4,8 @@ import Image from "next/image";
import { useTheme } from "next-themes";
// ui
import { Button, setToast, TOAST_TYPE } from "@plane/ui";
// components
import { ComicBoxButton } from "@/components/empty-state";
// helpers
import { cn } from "@/helpers/common.helper";
// plane web hooks
@@ -55,11 +57,11 @@ export const IssueTypeEmptyState: FC<TIssueTypeEmptyState> = observer((props) =>
<div className={cn("flex flex-col gap-5 md:min-w-[24rem] max-w-[45rem]")}>
<div className="flex flex-col gap-1.5 flex-shrink">
<h3 className="text-xl font-semibold">
{isIssueTypeSettingsEnabled ? "Enable issue types" : "Upgrade to enable issue types"}
{isIssueTypeSettingsEnabled ? "Enable Issue Types" : "Upgrade to enable Issue Types."}
</h3>
<p className="text-sm text-custom-text-200">
Issue types distinguish different kinds of work in unique ways, helping you to identify, categorize, and
report on your team{""}s work more effectively.
Shape issues to your work with Issue Types. Customize with icons, backgrounds, and properties and configure
them for this project.
</p>
</div>
<Image
@@ -72,9 +74,13 @@ export const IssueTypeEmptyState: FC<TIssueTypeEmptyState> = observer((props) =>
/>
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
{isIssueTypeSettingsEnabled ? (
<Button disabled={isLoading} onClick={() => handleEnableIssueTypes()}>
{isLoading ? "Setting up..." : "Enable"}
</Button>
<ComicBoxButton
label={isLoading ? "Setting up" : "Enable"}
title="Once enabled, Issue Types can't be disabled."
description="Plane's Issues will become the default issue type for this project and will show up with its icon and background in this project."
disabled={isLoading}
onClick={() => handleEnableIssueTypes()}
/>
) : (
<Button disabled={isLoading} onClick={() => toggleProPlanModal(true)}>
Upgrade

View File

@@ -61,8 +61,13 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
containerClassName={cn(!issueTypeDetail?.is_active && "opacity-60")}
/>
<div className="flex flex-col items-start justify-start whitespace-normal">
<div className="flex gap-4 text-left">
<div className="flex gap-4 text-left items-center">
<div className="text-sm text-custom-text-100 font-medium line-clamp-1">{issueTypeDetail?.name}</div>
{!issueTypeDetail?.is_active && (
<div className="py-0.5 px-3 text-xs rounded font-medium text-custom-text-300 bg-custom-background-80/70">
Disabled
</div>
)}
</div>
<div className="text-sm text-custom-text-300 text-left line-clamp-1">
{issueTypeDetail?.description}
@@ -70,18 +75,15 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
</div>
</div>
<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>
{issueTypeDetail?.is_default && (
<div
className={cn(
"py-0.5 px-2 text-xs rounded text-custom-primary-100 bg-transparent border border-custom-primary-100 cursor-default font-medium"
)}
>
Default
</div>
)}
<div>
{!issueTypeDetail?.is_default && (
<IssueTypeQuickActions

View File

@@ -1,9 +1,12 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { Info } from "lucide-react";
// ui
import { Input } from "@plane/ui";
// plane web hooks
import { Input, Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// plane web hooks
import { usePropertyOptions } from "@/plane-web/hooks/store";
// plane web types
import { TIssuePropertyOption, TIssuePropertyOptionCreateUpdateData } from "@/plane-web/types";
@@ -16,6 +19,8 @@ type TIssuePropertyOptionItem = {
export const IssuePropertyOptionItem: FC<TIssuePropertyOptionItem> = observer((props) => {
const { optionId, propertyOptionData, updateOptionData, error: optionsError } = props;
// store hooks
const { propertyOptions } = usePropertyOptions();
// derived values
const { key, ...propertyOptionCreateData } = propertyOptionData;
// states
@@ -27,10 +32,23 @@ export const IssuePropertyOptionItem: FC<TIssuePropertyOptionItem> = observer((p
else setError(optionsError ?? undefined);
}, [optionId, optionData, optionsError]);
const checkForDuplicate = ({ identifier, value }: { identifier: string | undefined; value: string }) => {
if (!value) return;
// check for duplicate option name
const isDuplicate = propertyOptions.find(
(option) => option.id !== identifier && option.key !== identifier && option.name === value
);
if (isDuplicate) setError("Option with same name already exists.");
else setError(undefined);
return isDuplicate;
};
// handle create/ update operation
const handleCreateUpdate = async () => {
// return if option name is same as previous or empty
if (!optionData.name) return;
// check for duplicate option name
if (checkForDuplicate({ identifier: optionData.id ?? key, value: optionData.name })) return;
// handle option data update
updateOptionData({ key, ...optionData });
};
@@ -42,18 +60,25 @@ export const IssuePropertyOptionItem: FC<TIssuePropertyOptionItem> = observer((p
};
return (
<Input
id={`option-${optionId}-${key}`}
value={optionData.name}
onChange={(e) => handleOptionDataChange("name", e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !!optionData.name && e.currentTarget.blur()}
onBlur={() => handleCreateUpdate()}
placeholder={"Add option"}
className={cn("w-full text-sm bg-custom-background-100 border-[0.5px] rounded", {
"border-custom-border-300": !Boolean(error),
})}
inputSize="xs"
hasError={Boolean(error)}
/>
<div className="relative w-full flex items-center">
<Input
id={`option-${optionId}-${key}`}
value={optionData.name}
onChange={(e) => handleOptionDataChange("name", e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !!optionData.name && e.currentTarget.blur()}
onBlur={() => handleCreateUpdate()}
placeholder={"Add option"}
className={cn("w-full text-sm bg-custom-background-100 border-[0.5px] rounded", {
"border-custom-border-300": !Boolean(error),
})}
inputSize="xs"
hasError={Boolean(error)}
/>
{Boolean(error) && (
<Tooltip tooltipContent={error} className="text-xs" position="left">
<Info className="absolute right-1.5 h-3 w-3 stroke-red-600 hover:cursor-pointer" />
</Tooltip>
)}
</div>
);
});

View File

@@ -48,7 +48,12 @@ export const PropertyAttributesDropdown = observer((props: TPropertyAttributesDr
(propertyDetail.property_type && DISABLE_ATTRIBUTE_CHANGE_LIST.includes(propertyDetail.property_type))
) {
return (
<span className="px-2 py-0.5 font-medium text-custom-text-300 bg-custom-background-80/40 rounded">
<span
className={cn(
"px-2 py-0.5 font-medium text-custom-text-300 rounded",
!currentOperationMode && "bg-custom-background-80/40"
)}
>
{attributeDisplayName ?? ""}
</span>
);

View File

@@ -58,7 +58,12 @@ export const PropertyMandatoryFieldToggle = (props: TPropertyMandatoryFieldToggl
position="bottom"
>
<span>
<Checkbox checked={value} onChange={() => handleMandatoryFieldChange(!value)} disabled={isDisabled} />
<Checkbox
checked={value}
onChange={() => handleMandatoryFieldChange(!value)}
disabled={isDisabled}
className={!value ? "bg-custom-background-100" : ""}
/>
</span>
</Tooltip>
</div>

View File

@@ -3,6 +3,7 @@ import cloneDeep from "lodash/cloneDeep";
import isEmpty from "lodash/isEmpty";
import isEqual from "lodash/isEqual";
import omitBy from "lodash/omitBy";
import uniqBy from "lodash/uniqBy";
import { observer } from "mobx-react";
// ui
import { Logo, TOAST_TYPE, ToggleSwitch, Tooltip, setPromiseToast, setToast } from "@plane/ui";
@@ -26,6 +27,7 @@ import {
TCreationListModes,
TOperationMode,
TIssuePropertyOption,
TIssuePropertyOptionCreateUpdateData,
} from "@/plane-web/types";
type TIssuePropertyListItem = {
@@ -114,6 +116,26 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
return hasError;
};
function sanitizeOptionsData(
options: Partial<TIssuePropertyOptionCreateUpdateData>[]
): Partial<TIssuePropertyOptionCreateUpdateData>[] {
// Extract the existing and new options
const existingOptions = options.filter((option) => option.id);
const newOptions = options.filter((option) => !option.id && option.key);
// Extract existing option names
const existingOptionNames = new Set(existingOptions.map((option) => option.name?.toLowerCase()));
// Filter new options to remove duplicates based on name and ensure no name exists in both lists
const sanitizedNewOptions = uniqBy(
newOptions.filter((option) => {
const name = option.name?.toLowerCase();
return name && !existingOptionNames.has(name);
}),
"name"
);
// Combine sanitized new options with existing options (for update use case)
return [...existingOptions, ...sanitizedNewOptions];
}
// handlers
const handleCreateProperty = async () => {
if (!issuePropertyData) return;
@@ -121,7 +143,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
// create property options payload (required for option type)
let optionsPayload: Partial<TIssuePropertyOption>[] = [];
if (issuePropertyData.property_type === EIssuePropertyType.OPTION) {
optionsPayload = propertyOptions
optionsPayload = sanitizeOptionsData(propertyOptions)
.map((item) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { key, ...rest } = item;
@@ -168,7 +190,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
const originalOptionsData = cloneDeep(sortedActivePropertyOptions);
let optionsPayload: Partial<TIssuePropertyOption>[] = [];
if (issuePropertyData.property_type === EIssuePropertyType.OPTION) {
optionsPayload = propertyOptions
optionsPayload = sanitizeOptionsData(propertyOptions)
.filter((item) => !!item.name && !isEmpty(item))
.map((option) => {
delete option.key;
@@ -332,16 +354,16 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
return (
<div
className={cn(
"w-full h-8 flex items-center gap-2 group px-1 py-1.5 my-1.5 text-sm rounded hover:bg-custom-background-90 cursor-default",
"w-full h-8 flex items-center gap-2 group px-2 py-3 my-2 text-sm rounded hover:bg-custom-background-90 cursor-default",
{
"bg-custom-background-90": issuePropertyOperationMode,
"bg-toast-text-warning/20 hover:bg-toast-text-warning/20": issuePropertyOperationMode,
}
)}
>
<div className="whitespace-nowrap w-48 grow flex items-center gap-1.5 text-sm font-medium">
<div className="whitespace-nowrap w-48 grow flex items-center gap-2 text-sm font-medium">
{issuePropertyData?.logo_props && (
<div className="flex-shrink-0 size-4 grid place-items-center">
<Logo logo={issuePropertyData.logo_props} size={15} type="lucide" customColor="text-custom-text-200" />
<div className="flex-shrink-0 size-5 grid place-items-center">
<Logo logo={issuePropertyData.logo_props} size={16} type="lucide" customColor="text-custom-text-200" />
</div>
)}
<div className="w-full truncate overflow-hidden">

View File

@@ -33,7 +33,7 @@ export const IssuePropertyList: FC<TIssuePropertyList> = observer((props) => {
<div className="overflow-x-auto horizontal-scrollbar scrollbar-sm">
<div className="inline-block min-w-full py-2 align-middle">
<div className="w-full">
<div className="flex items-center mx-7 px-1 gap-1.5 border-b-[0.5px] border-custom-border-200">
<div className="flex items-center mx-6 px-3 gap-1.5 border-b-[0.5px] border-custom-border-200">
<div className="w-48 grow py-1.5 text-left text-sm font-medium text-custom-text-300 truncate">Name</div>
<div className="w-36 py-1.5 text-left text-sm font-medium text-custom-text-300 truncate">Type</div>
<div className="w-36 py-1.5 text-left text-sm font-medium text-custom-text-300 truncate">Attributes</div>

View File

@@ -42,7 +42,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
onDelete={onDelete}
/>
<div
className={cn("items-center justify-end gap-1.5 pr-2", {
className={cn("items-center justify-end gap-1.5", {
"hidden group-hover:flex": !currentOperationMode,
flex: currentOperationMode,
})}
@@ -57,7 +57,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
onClick={onCreateUpdate}
disabled={isSubmitting}
>
{isSubmitting ? <Spinner width="12px" height="12px" /> : <Check size={12} className="text-green-600" />}
{isSubmitting ? <Spinner width="16px" height="16px" /> : <Check size={16} className="text-green-600" />}
</button>
</Tooltip>
<Tooltip className="w-full shadow" tooltipContent="Discard" position="bottom">
@@ -71,7 +71,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
onClick={onDiscard}
disabled={isSubmitting}
>
<X size={12} className="text-red-500" />
<X size={16} className="text-red-500" />
</button>
</Tooltip>
</>
@@ -80,9 +80,9 @@ 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={() => onIssuePropertyOperationMode("update")}
onClick={() => onIssuePropertyOperationMode("update")}
>
<Pencil size={12} className="text-custom-text-300" />
<Pencil size={16} className="text-custom-text-300" />
</button>
</Tooltip>
<Tooltip className="w-full shadow" tooltipContent="Delete" position="bottom">
@@ -90,7 +90,7 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
className="p-1 border-[0.5px] border-custom-border-300 rounded bg-custom-background-100 hover:bg-custom-background-90"
onClick={() => setIsDeleteModalOpen(true)}
>
<Trash2 size={12} className="text-red-500" />
<Trash2 size={16} className="text-red-500" />
</button>
</Tooltip>
</>

View File

@@ -54,16 +54,7 @@ export const IssueTypeQuickActions: React.FC<Props> = observer((props) => {
return (
<>
<div className={cn("flex items-center justify-center gap-1 px-2")}>
<Tooltip
className="shadow"
tooltipContent={!!isIssueTypeEnabled ? "Click to disable" : "Click to enable"}
position="bottom"
>
<div className="w-12">
<ToggleSwitch value={!!isIssueTypeEnabled} onChange={handleEnableDisable} disabled={isLoading} />
</div>
</Tooltip>
<div className={cn("flex items-center justify-center px-2")}>
<div className="w-6">
<Tooltip className="w-full shadow" tooltipContent="Edit" position="bottom">
<button
@@ -79,10 +70,19 @@ export const IssueTypeQuickActions: React.FC<Props> = observer((props) => {
}}
disabled={isLoading}
>
<Pencil size={14} className="text-custom-text-300" />
<Pencil size={16} className="text-custom-text-300" />
</button>
</Tooltip>
</div>
<Tooltip
className="shadow"
tooltipContent={!!isIssueTypeEnabled ? "Click to disable" : "Click to enable"}
position="bottom"
>
<div className="w-12">
<ToggleSwitch value={!!isIssueTypeEnabled} onChange={handleEnableDisable} disabled={isLoading} />
</div>
</Tooltip>
</div>
</>
);

View File

@@ -30,7 +30,7 @@ export const IssueTypesRoot = observer(() => {
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">
<h3 className="text-xl font-medium">Issue types</h3>
<h3 className="text-xl font-medium">Issue Types</h3>
{isIssueTypeSettingsEnabled && currentProjectDetails?.is_issue_type_enabled && (
<Button variant="primary" size="sm" onClick={() => setIsModalOpen(true)}>
Add issue type

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { isEqual } from "lodash";
import { observer } from "mobx-react";
// components
import { ChevronDown } from "lucide-react";
import { CustomSearchSelect } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
@@ -81,14 +82,36 @@ export const OptionValueSelect = observer((props: TOptionValueSelectProps) => {
content: option.name,
}));
// sort all the options to top which is available in value.
customSearchOptions?.sort((a, b) => {
const aIndex = a.value ? value.indexOf(a.value) : -1;
const bIndex = b.value ? value.indexOf(b.value) : -1;
if (aIndex === -1 && bIndex === -1) {
return 0;
}
if (aIndex === -1) {
return 1;
}
if (bIndex === -1) {
return -1;
}
return aIndex - bIndex;
});
const customSearchSelectProps = {
label: getDisplayName(),
options: customSearchOptions,
className: "group w-full h-full flex",
optionsClassName: "w-48",
chevronClassName: "h-3.5 w-3.5 hidden group-hover:inline",
buttonClassName: cn(
"rounded text-sm bg-custom-background-100 border-custom-border-200",
customButton: (
<>
<span className="text-sm truncate whitespace-nowrap">{getDisplayName()}</span>
{!isDisabled && (
<ChevronDown className={cn("flex-shrink-0 h-3.5 w-3.5 hidden group-hover:inline")} aria-hidden="true" />
)}
</>
),
customButtonClassName: cn(
"items-center rounded px-2 py-1 text-sm bg-custom-background-100 border-custom-border-200",
{
"border-[0.5px]": variant === "create" || Boolean(error),
"border-0": variant === "update",

View File

@@ -174,15 +174,15 @@ export const PropertyValueSelect = observer((props: TPropertyValueSelectProps) =
return (
<>
{variant === "create" && (
<div className={cn("w-full flex items-start justify-center gap-1.5 py-1")}>
<div className={cn("w-full flex flex-shrink-0 items-start justify-center py-1")}>
<div className={cn("w-2/5 h-8 flex flex-shrink-0 gap-1.5 items-center")}>
<IssuePropertyDetail />
</div>
<div className="w-full h-full min-h-8 flex flex-col gap-0.5">{CurrentPropertyAttribute}</div>
<div className="w-3/5 h-full min-h-8 flex flex-col gap-0.5 pl-3">{CurrentPropertyAttribute}</div>
</div>
)}
{variant === "update" && (
<div className={cn("flex w-full items-start gap-x-3 gap-y-1 min-h-8")}>
<div className={cn("flex w-full items-start gap-y-1 min-h-8")}>
<div
className={cn(
"flex items-center h-8 gap-1 flex-shrink-0 text-sm text-custom-text-300",
@@ -191,7 +191,12 @@ export const PropertyValueSelect = observer((props: TPropertyValueSelectProps) =
>
<IssuePropertyDetail />
</div>
<div className="relative h-full min-h-8 w-full flex-grow flex flex-col gap-0.5">
<div
className={cn("relative h-full min-h-8 flex flex-col gap-0.5 pl-3", {
"w-3/4": isPeekOverview,
"w-3/5": !isPeekOverview,
})}
>
{CurrentPropertyAttribute}
</div>
</div>

View File

@@ -359,9 +359,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
/>
)}
</div>
<div className="px-4 py-2 border-t-[0.5px] border-custom-border-200">
<div className="px-4 py-3 border-t-[0.5px] border-custom-border-200 shadow-custom-shadow-xs rounded-b-lg">
<div className="pb-3 border-b-[0.5px] border-custom-border-200">
<div className="text-base font-medium pb-2">Properties</div>
<IssueDefaultProperties
control={control}
id={data?.id}

View File

@@ -11,7 +11,7 @@ export const PROJECT_SETTINGS = {
...PROJECT_SETTINGS_CE,
"issue-types": {
key: "issue-types",
label: "Issue types",
label: "Issue Types",
href: `/settings/issue-types/`,
access: EUserProjectRoles.MEMBER,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/issue-types/`,