From a19ff3a9b436624f8bb47173002ec5ec16ab4a3a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 3 Sep 2025 17:57:03 +0530 Subject: [PATCH 1/6] chore: remove create label modal --- .../components/labels/create-label-modal.tsx | 218 ------------------ apps/web/core/components/labels/index.ts | 1 - 2 files changed, 219 deletions(-) delete mode 100644 apps/web/core/components/labels/create-label-modal.tsx diff --git a/apps/web/core/components/labels/create-label-modal.tsx b/apps/web/core/components/labels/create-label-modal.tsx deleted file mode 100644 index 413429c8a19..00000000000 --- a/apps/web/core/components/labels/create-label-modal.tsx +++ /dev/null @@ -1,218 +0,0 @@ -"use client"; - -import React, { useEffect } from "react"; -import { observer } from "mobx-react"; -import { TwitterPicker } from "react-color"; -import { Controller, useForm } from "react-hook-form"; -import { ChevronDown } from "lucide-react"; -import { Dialog, Popover, Transition } from "@headlessui/react"; -// plane imports -import { ETabIndices, LABEL_COLOR_OPTIONS, getRandomLabelColor } from "@plane/constants"; -// types -import type { IIssueLabel, IState } from "@plane/types"; -// ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -// helpers -import { getTabIndex } from "@plane/utils"; -// hooks - -import { usePlatformOS } from "@/hooks/use-platform-os"; - -// types -type Props = { - createLabel: (data: Partial) => Promise; - handleClose: () => void; - isOpen: boolean; - onSuccess?: (response: IIssueLabel) => void; -}; - -const defaultValues: Partial = { - name: "", - color: "rgb(var(--color-text-200))", -}; - -export const CreateLabelModal: React.FC = observer((props) => { - const { createLabel, handleClose, isOpen, onSuccess } = props; - // store hooks - const { isMobile } = usePlatformOS(); - // form info - const { - formState: { errors, isSubmitting }, - handleSubmit, - watch, - control, - reset, - setValue, - setFocus, - } = useForm({ - defaultValues, - }); - - const { getIndex } = getTabIndex(ETabIndices.CREATE_LABEL, isMobile); - - /** - * For setting focus on name input - */ - useEffect(() => { - setFocus("name"); - }, [setFocus, isOpen]); - - useEffect(() => { - if (isOpen) setValue("color", getRandomLabelColor()); - }, [setValue, isOpen]); - - const onClose = () => { - handleClose(); - reset(defaultValues); - }; - - const onSubmit = async (formData: IIssueLabel) => { - await createLabel(formData) - .then((res) => { - onClose(); - if (onSuccess) onSuccess(res); - }) - .catch((error) => { - setToast({ - title: "Error!", - type: TOAST_TYPE.ERROR, - message: error?.detail ?? "Something went wrong. Please try again later.", - }); - reset(formData); - }); - }; - - return ( - - - -
- - -
-
- - -
{ - e.preventDefault(); - e.stopPropagation(); - handleSubmit(onSubmit)(e); - }} - > -
- - Create Label - -
- - {({ open, close }) => ( - <> - - {watch("color") && watch("color") !== "" && ( - - )} - - - - - ( - { - onChange(value.hex); - close(); - }} - /> - )} - /> - - - - )} - -
- ( - - )} - /> -
-
-
-
- - -
-
-
-
-
-
-
-
- ); -}); diff --git a/apps/web/core/components/labels/index.ts b/apps/web/core/components/labels/index.ts index 9195f8649b2..7fe388345fd 100644 --- a/apps/web/core/components/labels/index.ts +++ b/apps/web/core/components/labels/index.ts @@ -1,4 +1,3 @@ -export * from "./create-label-modal"; export * from "./create-update-label-inline"; export * from "./delete-label-modal"; export * from "./project-setting-label-group"; From 800fcd25d391bbd76ce935610999a5b62ea22311 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 3 Sep 2025 17:58:35 +0530 Subject: [PATCH 2/6] fix: label spinner --- .../issues/issue-layouts/properties/label-dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx b/apps/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx index 20d137e9d87..357423e0496 100644 --- a/apps/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx +++ b/apps/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx @@ -277,7 +277,7 @@ export const LabelDropdown = (props: ILabelDropdownProps) => { )) ) : submitting ? ( - + ) : canCreateLabel ? (

{ From def03c028a8e73972a298788d5ad44c40c3bf33c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 3 Sep 2025 19:35:20 +0530 Subject: [PATCH 3/6] chore: add label flow improvements --- .../modals/create-modal/issue-properties.tsx | 1 - .../components/default-properties.tsx | 13 ++--- .../components/issues/issue-modal/form.tsx | 17 +----- .../core/components/issues/select/base.tsx | 58 ++++++++++++++----- .../components/issues/select/dropdown.tsx | 19 +++++- 5 files changed, 70 insertions(+), 38 deletions(-) diff --git a/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx b/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx index 2a36099378a..b0661990974 100644 --- a/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx +++ b/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx @@ -89,7 +89,6 @@ export const InboxIssueProperties: FC = observer((props) {/* labels */}

{}} value={data?.label_ids || []} onChange={(labelIds) => handleData("label_ids", labelIds)} projectId={projectId} diff --git a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx index 6b3c060d7b7..f37bcd97cc1 100644 --- a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -42,7 +42,6 @@ type TIssueDefaultPropertiesProps = { parentId: string | null; isDraft: boolean; handleFormChange: () => void; - setLabelModal: React.Dispatch>; setSelectedParentIssue: (issue: ISearchIssueResponse) => void; }; @@ -58,8 +57,7 @@ export const IssueDefaultProperties: React.FC = ob parentId, isDraft, handleFormChange, - setLabelModal, - setSelectedParentIssue, + setSelectedParentIssue } = props; // states const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); @@ -74,7 +72,8 @@ export const IssueDefaultProperties: React.FC = ob const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile); - const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + const canCreateLabel = + projectId && allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId); const minDate = getDate(startDate); minDate?.setDate(minDate.getDate()); @@ -147,15 +146,14 @@ export const IssueDefaultProperties: React.FC = ob render={({ field: { value, onChange } }) => (
{ onChange(labelIds); handleFormChange(); }} - projectId={projectId ?? undefined} + projectId={projectDetails?.id ?? undefined} tabIndex={getIndex("label_ids")} - createLabelEnabled={canCreateLabel} + createLabelEnabled={!!canCreateLabel} />
)} @@ -333,6 +331,7 @@ export const IssueDefaultProperties: React.FC = ob }} projectId={projectId ?? undefined} issueId={isDraft ? undefined : id} + searchEpic /> )} /> diff --git a/apps/web/core/components/issues/issue-modal/form.tsx b/apps/web/core/components/issues/issue-modal/form.tsx index b84ce03a00c..ea37d99aef6 100644 --- a/apps/web/core/components/issues/issue-modal/form.tsx +++ b/apps/web/core/components/issues/issue-modal/form.tsx @@ -28,12 +28,10 @@ import { IssueProjectSelect, IssueTitleInput, } from "@/components/issues/issue-modal/components"; -import { CreateLabelModal } from "@/components/labels"; // helpers // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useIssueDetail } from "@/hooks/store/use-issue-detail"; -import { useLabel } from "@/hooks/store/use-label"; import { useProject } from "@/hooks/store/use-project"; import { useProjectState } from "@/hooks/store/use-project-state"; import { useWorkspaceDraftIssues } from "@/hooks/store/workspace-draft"; @@ -68,6 +66,7 @@ export interface IssueFormProps { handleDraftAndClose?: () => void; isProjectSelectionDisabled?: boolean; storeType: EIssuesStoreType; + convertToWorkItem?: boolean; } export const IssueFormRoot: FC = observer((props) => { @@ -97,7 +96,6 @@ export const IssueFormRoot: FC = observer((props) => { } = props; // states - const [labelModal, setLabelModal] = useState(false); const [gptAssistantModal, setGptAssistantModal] = useState(false); const [isMoving, setIsMoving] = useState(false); @@ -126,7 +124,6 @@ export const IssueFormRoot: FC = observer((props) => { } = useIssueModal(); const { isMobile } = usePlatformOS(); const { moveIssue } = useWorkspaceDraftIssues(); - const { createLabel } = useLabel(); const { issue: { getIssueById }, @@ -363,17 +360,6 @@ export const IssueFormRoot: FC = observer((props) => { return ( - {projectId && ( - setLabelModal(false)} - onSuccess={(response) => { - setValue<"label_ids">("label_ids", [...watch("label_ids"), response.id]); - handleFormChange(); - }} - /> - )}
= observer((props) => { parentId={watch("parent_id")} isDraft={isDraft} handleFormChange={handleFormChange} - setLabelModal={setLabelModal} setSelectedParentIssue={setSelectedParentIssue} />
diff --git a/apps/web/core/components/issues/select/base.tsx b/apps/web/core/components/issues/select/base.tsx index 8c48cf78e8d..128e28c072d 100644 --- a/apps/web/core/components/issues/select/base.tsx +++ b/apps/web/core/components/issues/select/base.tsx @@ -2,8 +2,9 @@ import React, { useEffect, useRef, useState } from "react"; import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; import { usePopper } from "react-popper"; -import { Check, Component, Plus, Search, Tag } from "lucide-react"; +import { Check, Component, Loader, Search, Tag } from "lucide-react"; import { Combobox } from "@headlessui/react"; +import { getRandomLabelColor } from "@plane/constants"; // plane imports import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; @@ -26,7 +27,7 @@ export type TWorkItemLabelSelectBaseProps = { onChange: (value: string[]) => void; onDropdownOpen?: () => void; placement?: Placement; - setIsOpen: React.Dispatch>; + createLabel?: (data: Partial) => Promise; tabIndex?: number; value: string[]; }; @@ -43,7 +44,7 @@ export const WorkItemLabelSelectBase: React.FC = onChange, onDropdownOpen, placement, - setIsOpen, + createLabel, tabIndex, value, } = props; @@ -55,6 +56,7 @@ export const WorkItemLabelSelectBase: React.FC = const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); // plane hooks const { t } = useTranslation(); // store hooks @@ -88,6 +90,17 @@ export const WorkItemLabelSelectBase: React.FC = onChange(val); }; + const searchInputKeyDown = async (e: React.KeyboardEvent) => { + e.stopPropagation(); + if (query !== "" && e.key === "Escape") { + setQuery(""); + } + + if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && createLabelEnabled) { + e.preventDefault(); + await handleAddLabel(query); + } + }; const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); const handleOnClick = (e: React.MouseEvent) => { @@ -104,6 +117,15 @@ export const WorkItemLabelSelectBase: React.FC = } }, [isDropdownOpen, isMobile]); + const handleAddLabel = async (labelName: string) => { + if (!createLabel) return; + setSubmitting(true); + const label = await createLabel({ name: labelName, color: getRandomLabelColor() }); + onChange([...value, label.id]); + setQuery(""); + setSubmitting(false); + }; + return ( = onChange={(event) => setQuery(event.target.value)} placeholder={t("search")} displayValue={(assigned: any) => assigned?.name} + onKeyDown={searchInputKeyDown} />
@@ -242,22 +265,31 @@ export const WorkItemLabelSelectBase: React.FC =
); }) + ) : submitting ? ( + + ) : createLabelEnabled ? ( +

{ + if (!query.length) return; + handleAddLabel(query); + }} + className={`text-left text-custom-text-200 ${query.length ? "cursor-pointer" : "cursor-default"}`} + > + {/* TODO: translate here */} + {query.length ? ( + <> + + Add "{query}" to labels + + ) : ( + t("label.create.type") + )} +

) : (

{t("no_matching_results")}

) ) : (

{t("loading")}

)} - {createLabelEnabled && ( - - )}
diff --git a/apps/web/core/components/issues/select/dropdown.tsx b/apps/web/core/components/issues/select/dropdown.tsx index be66d709ab6..08d792b77f5 100644 --- a/apps/web/core/components/issues/select/dropdown.tsx +++ b/apps/web/core/components/issues/select/dropdown.tsx @@ -1,8 +1,11 @@ import React from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +import { EUserPermissionsLevel } from "@plane/constants"; +import { EUserPermissions, IIssueLabel } from "@plane/types"; // hooks import { useLabel } from "@/hooks/store/use-label"; +import { useUserPermissions } from "@/hooks/store/user"; // local imports import { TWorkItemLabelSelectBaseProps, WorkItemLabelSelectBase } from "./base"; @@ -15,21 +18,35 @@ export const IssueLabelSelect: React.FC = observer((p // router const { workspaceSlug } = useParams(); // store hooks - const { getProjectLabelIds, getLabelById, fetchProjectLabels } = useLabel(); + const { allowPermissions } = useUserPermissions(); + const { getProjectLabelIds, getLabelById, fetchProjectLabels, createLabel } = useLabel(); // derived values const projectLabelIds = getProjectLabelIds(projectId); + const canCreateLabel = + projectId && + allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug?.toString(), projectId); + const onDropdownOpen = () => { if (projectLabelIds === undefined && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId); }; + const handleCreateLabel = (data: Partial) => { + if (!workspaceSlug || !projectId) { + throw new Error("Workspace slug or project ID is missing"); + } + return createLabel(workspaceSlug.toString(), projectId, data); + }; + return ( ); }); From 5f926190dcecadf342d2286efb7e0e9e422809cd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 3 Sep 2025 19:43:15 +0530 Subject: [PATCH 4/6] chore: code refactor --- .../issues/issue-modal/components/default-properties.tsx | 1 - apps/web/core/components/issues/issue-modal/form.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx index f37bcd97cc1..ddde41e896b 100644 --- a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -331,7 +331,6 @@ export const IssueDefaultProperties: React.FC = ob }} projectId={projectId ?? undefined} issueId={isDraft ? undefined : id} - searchEpic /> )} /> diff --git a/apps/web/core/components/issues/issue-modal/form.tsx b/apps/web/core/components/issues/issue-modal/form.tsx index ea37d99aef6..c4fbb6d4132 100644 --- a/apps/web/core/components/issues/issue-modal/form.tsx +++ b/apps/web/core/components/issues/issue-modal/form.tsx @@ -66,7 +66,6 @@ export interface IssueFormProps { handleDraftAndClose?: () => void; isProjectSelectionDisabled?: boolean; storeType: EIssuesStoreType; - convertToWorkItem?: boolean; } export const IssueFormRoot: FC = observer((props) => { From 76ebf19773f162da557babf625aa682935a5e0dc Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 3 Sep 2025 20:21:13 +0530 Subject: [PATCH 5/6] chore: code refactor --- .../components/default-properties.tsx | 2 +- .../core/components/issues/select/base.tsx | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx index ddde41e896b..96cff535267 100644 --- a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -151,7 +151,7 @@ export const IssueDefaultProperties: React.FC = ob onChange(labelIds); handleFormChange(); }} - projectId={projectDetails?.id ?? undefined} + projectId={projectId ?? undefined} tabIndex={getIndex("label_ids")} createLabelEnabled={!!canCreateLabel} /> diff --git a/apps/web/core/components/issues/select/base.tsx b/apps/web/core/components/issues/select/base.tsx index 128e28c072d..07ee8d91793 100644 --- a/apps/web/core/components/issues/select/base.tsx +++ b/apps/web/core/components/issues/select/base.tsx @@ -91,14 +91,23 @@ export const WorkItemLabelSelectBase: React.FC = }; const searchInputKeyDown = async (e: React.KeyboardEvent) => { - e.stopPropagation(); - if (query !== "" && e.key === "Escape") { + const q = query.trim(); + if (q !== "" && e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); setQuery(""); + return; } - - if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && createLabelEnabled) { + if ( + q !== "" && + e.key === "Enter" && + !e.nativeEvent.isComposing && + createLabelEnabled && + filteredOptions.length === 0 && + !submitting + ) { e.preventDefault(); - await handleAddLabel(query); + await handleAddLabel(q); } }; const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); @@ -118,12 +127,20 @@ export const WorkItemLabelSelectBase: React.FC = }, [isDropdownOpen, isMobile]); const handleAddLabel = async (labelName: string) => { - if (!createLabel) return; + if (!createLabel || submitting) return; + const name = labelName.trim(); + if (!name) return; setSubmitting(true); - const label = await createLabel({ name: labelName, color: getRandomLabelColor() }); - onChange([...value, label.id]); - setQuery(""); - setSubmitting(false); + try { + const existing = labelsList.find((l) => l.name.toLowerCase() === name.toLowerCase()); + const idToAdd = existing ? existing.id : (await createLabel({ name, color: getRandomLabelColor() })).id; + onChange(Array.from(new Set([...value, idToAdd]))); + setQuery(""); + } catch (e) { + console.error("Failed to create label", e); + } finally { + setSubmitting(false); + } }; return ( @@ -266,7 +283,7 @@ export const WorkItemLabelSelectBase: React.FC = ); }) ) : submitting ? ( - + ) : createLabelEnabled ? (

{ From cbecc141448822a33b8902220fd100961616a41a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 4 Sep 2025 15:02:57 +0530 Subject: [PATCH 6/6] chore: code refactor --- .../issues/issue-modal/components/default-properties.tsx | 2 +- packages/utils/src/string.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx index 96cff535267..53abf7bd8e5 100644 --- a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -57,7 +57,7 @@ export const IssueDefaultProperties: React.FC = ob parentId, isDraft, handleFormChange, - setSelectedParentIssue + setSelectedParentIssue, } = props; // states const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index fdd0a73496f..7735011511e 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -1,5 +1,5 @@ -import type { Content, JSONContent } from "@plane/types"; import DOMPurify from "isomorphic-dompurify"; +import type { Content, JSONContent } from "@plane/types"; /** * @description Adds space between camelCase words