mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 21:40:18 +02:00
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:
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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/`,
|
||||
|
||||
Reference in New Issue
Block a user