diff --git a/web/core/components/issues/issue-detail/label/root.tsx b/web/core/components/issues/issue-detail/label/root.tsx index f71e9ba3ce2..f31defafd44 100644 --- a/web/core/components/issues/issue-detail/label/root.tsx +++ b/web/core/components/issues/issue-detail/label/root.tsx @@ -7,13 +7,12 @@ import { IIssueLabel, TIssue, TIssueServiceType } from "@plane/types"; // components import { TOAST_TYPE, setToast } from "@plane/ui"; // hooks -import { useIssueDetail, useLabel, useProjectInbox, useUserPermissions } from "@/hooks/store"; +import { useIssueDetail, useLabel, useProjectInbox } from "@/hooks/store"; // ui // types -import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./"; +import { LabelList, IssueLabelSelectRoot } from "./"; // TODO: Fix this import statement, as core should not import from ee // eslint-disable-next-line import/order -import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; export type TIssueLabel = { workspaceSlug: string; @@ -47,9 +46,7 @@ export const IssueLabel: FC = observer((props) => { issue: { getIssueById }, } = useIssueDetail(issueServiceType); const { getIssueInboxByIssueId } = useProjectInbox(); - const { allowPermissions } = useUserPermissions(); - const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const issue = isInboxIssue ? getIssueInboxByIssueId(issueId)?.issue : getIssueById(issueId); const labelOperations: TLabelOperations = useMemo( @@ -113,16 +110,6 @@ export const IssueLabel: FC = observer((props) => { labelOperations={labelOperations} /> )} - - {!disabled && canCreateLabel && ( - - )} ); }); diff --git a/web/core/components/issues/issue-detail/label/select/label-select.tsx b/web/core/components/issues/issue-detail/label/select/label-select.tsx index 7fd700398c1..39c76180937 100644 --- a/web/core/components/issues/issue-detail/label/select/label-select.tsx +++ b/web/core/components/issues/issue-detail/label/select/label-select.tsx @@ -1,33 +1,40 @@ import { Fragment, useState } from "react"; import { observer } from "mobx-react"; import { usePopper } from "react-popper"; -import { Check, Search, Tag } from "lucide-react"; +import { Check, Loader, Search, Tag } from "lucide-react"; import { Combobox } from "@headlessui/react"; // helpers +import { IIssueLabel } from "@plane/types"; +import { getRandomLabelColor } from "@/constants/label"; import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks -import { useLabel } from "@/hooks/store"; +import { useLabel, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; -// components - +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; +//constants export interface IIssueLabelSelect { workspaceSlug: string; projectId: string; issueId: string; values: string[]; onSelect: (_labelIds: string[]) => void; + onAddLabel: (workspaceSlug: string, projectId: string, data: Partial) => Promise; } export const IssueLabelSelect: React.FC = observer((props) => { - const { workspaceSlug, projectId, issueId, values, onSelect } = props; + const { workspaceSlug, projectId, issueId, values, onSelect, onAddLabel } = props; // store hooks const { isMobile } = usePlatformOS(); const { fetchProjectLabels, getProjectLabels } = useLabel(); + const { allowPermissions } = useUserPermissions(); // states const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const [isLoading, setIsLoading] = useState(false); const [query, setQuery] = useState(""); + const [submitting, setSubmitting] = useState(false); + + const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const projectLabels = getProjectLabels(projectId); @@ -83,11 +90,25 @@ export const IssueLabelSelect: React.FC = observer((props) => ); - const searchInputKeyDown = (e: React.KeyboardEvent) => { + const searchInputKeyDown = async (e: React.KeyboardEvent) => { if (query !== "" && e.key === "Escape") { e.stopPropagation(); setQuery(""); } + + if (query !== "" && e.key === "Enter") { + e.stopPropagation(); + e.preventDefault(); + await handleAddLabel(query); + } + }; + + const handleAddLabel = async (labelName: string) => { + setSubmitting(true); + const label = await onAddLabel(workspaceSlug, projectId, { name: labelName, color: getRandomLabelColor() }); + onSelect([...values, label.id]); + setQuery(""); + setSubmitting(false); }; if (!issueId || !values) return <>; @@ -159,10 +180,19 @@ export const IssueLabelSelect: React.FC = observer((props) => )} )) + ) : submitting ? ( + + ) : canCreateLabel ? ( +

{ + handleAddLabel(query); + }} + className="text-left text-custom-text-200 cursor-pointer" + > + + Add "{query}" to labels +

) : ( - -

No matching results

-
+

No matching results.

)} diff --git a/web/core/components/issues/issue-detail/label/select/root.tsx b/web/core/components/issues/issue-detail/label/select/root.tsx index 00f96522b1c..a57a58742c2 100644 --- a/web/core/components/issues/issue-detail/label/select/root.tsx +++ b/web/core/components/issues/issue-detail/label/select/root.tsx @@ -26,6 +26,7 @@ export const IssueLabelSelectRoot: FC = (props) => { issueId={issueId} values={values} onSelect={handleLabel} + onAddLabel={labelOperations.createLabel} /> ); }; diff --git a/web/core/components/issues/issue-layouts/kanban/block.tsx b/web/core/components/issues/issue-layouts/kanban/block.tsx index d8f4307e855..5fbee1c0a73 100644 --- a/web/core/components/issues/issue-layouts/kanban/block.tsx +++ b/web/core/components/issues/issue-layouts/kanban/block.tsx @@ -6,6 +6,7 @@ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-d import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane helpers +import { EIssueServiceType } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; // types import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; @@ -26,7 +27,6 @@ import { IssueIdentifier } from "@/plane-web/components/issues"; import { TRenderQuickActions } from "../list/list-view-types"; import { IssueProperties } from "../properties/all-properties"; import { getIssueBlockId } from "../utils"; -import { EIssueServiceType } from "@plane/constants"; interface IssueBlockProps { issueId: string; diff --git a/web/core/components/issues/issue-layouts/properties/labels.tsx b/web/core/components/issues/issue-layouts/properties/labels.tsx index 823137cb88f..56f9b651bef 100644 --- a/web/core/components/issues/issue-layouts/properties/labels.tsx +++ b/web/core/components/issues/issue-layouts/properties/labels.tsx @@ -1,11 +1,11 @@ "use client"; -import { Fragment, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search, Tags } from "lucide-react"; +import { Check, ChevronDown, Loader, Search, Tags } from "lucide-react"; import { Combobox } from "@headlessui/react"; // plane helpers import { useOutsideClickDetector } from "@plane/hooks"; @@ -14,9 +14,12 @@ import { IIssueLabel } from "@plane/types"; // ui import { ComboDropDown, Tooltip } from "@plane/ui"; // hooks -import { useLabel } from "@/hooks/store"; +import { getRandomLabelColor } from "@/constants/label"; +import { useLabel, useUserPermissions } from "@/hooks/store"; import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; +// constants export interface IIssuePropertyLabels { projectId: string | null; @@ -62,6 +65,7 @@ export const IssuePropertyLabels: React.FC = observer((pro // states const [query, setQuery] = useState(""); const [isOpen, setIsOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); // refs const dropdownRef = useRef(null); const inputRef = useRef(null); @@ -70,9 +74,12 @@ export const IssuePropertyLabels: React.FC = observer((pro const [popperElement, setPopperElement] = useState(null); const [isLoading, setIsLoading] = useState(false); // store hooks - const { fetchProjectLabels, getProjectLabels } = useLabel(); + const { fetchProjectLabels, getProjectLabels, createLabel } = useLabel(); const { isMobile } = usePlatformOS(); const storeLabels = getProjectLabels(projectId); + const { allowPermissions } = useUserPermissions(); + + const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const onOpen = () => { if (!storeLabels && workspaceSlug && projectId) @@ -102,11 +109,17 @@ export const IssuePropertyLabels: React.FC = observer((pro useOutsideClickDetector(dropdownRef, handleClose); - const searchInputKeyDown = (e: React.KeyboardEvent) => { + const searchInputKeyDown = async (e: React.KeyboardEvent) => { if (query !== "" && e.key === "Escape") { e.stopPropagation(); setQuery(""); } + + if (query !== "" && e.key === "Enter") { + e.stopPropagation(); + e.preventDefault(); + await handleAddLabel(query); + } }; useEffect(() => { @@ -249,6 +262,15 @@ export const IssuePropertyLabels: React.FC = observer((pro ); + const handleAddLabel = async (labelName: string) => { + if (!projectId) return; + setSubmitting(true); + const label = await createLabel(workspaceSlug, projectId, { name: labelName, color: getRandomLabelColor() }); + onChange([...value, label.id]); + setQuery(""); + setSubmitting(false); + }; + return ( = observer((pro )} )) + ) : submitting ? ( + + ) : canCreateLabel ? ( +

{ + handleAddLabel(query); + }} + className="text-left text-custom-text-200 cursor-pointer" + > + + Add "{query}" to labels +

) : ( - -

No matching results

-
+

No matching results.

)}