[WEB-2678]feat: added functionality to add labels directly from dropdown (#6211)
* enhancement:added functionality to add features directly from dropdown * fix: fixed import order * fix: fixed lint errors
This commit is contained in:
parent
4507802aba
commit
8e6d885731
5 changed files with 82 additions and 33 deletions
|
|
@ -7,13 +7,12 @@ import { IIssueLabel, TIssue, TIssueServiceType } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useLabel, useProjectInbox, useUserPermissions } from "@/hooks/store";
|
import { useIssueDetail, useLabel, useProjectInbox } from "@/hooks/store";
|
||||||
// ui
|
// ui
|
||||||
// types
|
// types
|
||||||
import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./";
|
import { LabelList, IssueLabelSelectRoot } from "./";
|
||||||
// TODO: Fix this import statement, as core should not import from ee
|
// TODO: Fix this import statement, as core should not import from ee
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
|
|
||||||
|
|
||||||
export type TIssueLabel = {
|
export type TIssueLabel = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -47,9 +46,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail(issueServiceType);
|
} = useIssueDetail(issueServiceType);
|
||||||
const { getIssueInboxByIssueId } = useProjectInbox();
|
const { getIssueInboxByIssueId } = useProjectInbox();
|
||||||
const { allowPermissions } = useUserPermissions();
|
|
||||||
|
|
||||||
const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
|
||||||
const issue = isInboxIssue ? getIssueInboxByIssueId(issueId)?.issue : getIssueById(issueId);
|
const issue = isInboxIssue ? getIssueInboxByIssueId(issueId)?.issue : getIssueById(issueId);
|
||||||
|
|
||||||
const labelOperations: TLabelOperations = useMemo(
|
const labelOperations: TLabelOperations = useMemo(
|
||||||
|
|
@ -113,16 +110,6 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||||
labelOperations={labelOperations}
|
labelOperations={labelOperations}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!disabled && canCreateLabel && (
|
|
||||||
<LabelCreate
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
issueId={issueId}
|
|
||||||
values={issue?.label_ids || []}
|
|
||||||
labelOperations={labelOperations}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,40 @@
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePopper } from "react-popper";
|
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";
|
import { Combobox } from "@headlessui/react";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { IIssueLabel } from "@plane/types";
|
||||||
|
import { getRandomLabelColor } from "@/constants/label";
|
||||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel } from "@/hooks/store";
|
import { useLabel, useUserPermissions } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// components
|
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
|
||||||
|
//constants
|
||||||
export interface IIssueLabelSelect {
|
export interface IIssueLabelSelect {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
values: string[];
|
values: string[];
|
||||||
onSelect: (_labelIds: string[]) => void;
|
onSelect: (_labelIds: string[]) => void;
|
||||||
|
onAddLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
|
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, values, onSelect } = props;
|
const { workspaceSlug, projectId, issueId, values, onSelect, onAddLabel } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const { fetchProjectLabels, getProjectLabels } = useLabel();
|
const { fetchProjectLabels, getProjectLabels } = useLabel();
|
||||||
|
const { allowPermissions } = useUserPermissions();
|
||||||
// states
|
// states
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||||
|
|
||||||
const projectLabels = getProjectLabels(projectId);
|
const projectLabels = getProjectLabels(projectId);
|
||||||
|
|
||||||
|
|
@ -83,11 +90,25 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (query !== "" && e.key === "Escape") {
|
if (query !== "" && e.key === "Escape") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setQuery("");
|
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 <></>;
|
if (!issueId || !values) return <></>;
|
||||||
|
|
@ -159,10 +180,19 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
|
||||||
)}
|
)}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))
|
))
|
||||||
|
) : submitting ? (
|
||||||
|
<Loader className="spin h-3.5 w-3.5" />
|
||||||
|
) : canCreateLabel ? (
|
||||||
|
<p
|
||||||
|
onClick={() => {
|
||||||
|
handleAddLabel(query);
|
||||||
|
}}
|
||||||
|
className="text-left text-custom-text-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
+ Add <span className="text-custom-text-100">"{query}"</span> to labels
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-2 p-1">
|
<p className="text-left text-custom-text-200 ">No matching results.</p>
|
||||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export const IssueLabelSelectRoot: FC<TIssueLabelSelectRoot> = (props) => {
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
values={values}
|
values={values}
|
||||||
onSelect={handleLabel}
|
onSelect={handleLabel}
|
||||||
|
onAddLabel={labelOperations.createLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-d
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// plane helpers
|
// plane helpers
|
||||||
|
import { EIssueServiceType } from "@plane/constants";
|
||||||
import { useOutsideClickDetector } from "@plane/hooks";
|
import { useOutsideClickDetector } from "@plane/hooks";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/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 { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import { IssueProperties } from "../properties/all-properties";
|
import { IssueProperties } from "../properties/all-properties";
|
||||||
import { getIssueBlockId } from "../utils";
|
import { getIssueBlockId } from "../utils";
|
||||||
import { EIssueServiceType } from "@plane/constants";
|
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
issueId: string;
|
issueId: string;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Fragment, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Placement } from "@popperjs/core";
|
import { Placement } from "@popperjs/core";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { usePopper } from "react-popper";
|
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";
|
import { Combobox } from "@headlessui/react";
|
||||||
// plane helpers
|
// plane helpers
|
||||||
import { useOutsideClickDetector } from "@plane/hooks";
|
import { useOutsideClickDetector } from "@plane/hooks";
|
||||||
|
|
@ -14,9 +14,12 @@ import { IIssueLabel } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { ComboDropDown, Tooltip } from "@plane/ui";
|
import { ComboDropDown, Tooltip } from "@plane/ui";
|
||||||
// hooks
|
// 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 { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
|
||||||
|
// constants
|
||||||
|
|
||||||
export interface IIssuePropertyLabels {
|
export interface IIssuePropertyLabels {
|
||||||
projectId: string | null;
|
projectId: string | null;
|
||||||
|
|
@ -62,6 +65,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||||
// states
|
// states
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
@ -70,9 +74,12 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchProjectLabels, getProjectLabels } = useLabel();
|
const { fetchProjectLabels, getProjectLabels, createLabel } = useLabel();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const storeLabels = getProjectLabels(projectId);
|
const storeLabels = getProjectLabels(projectId);
|
||||||
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
|
const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
if (!storeLabels && workspaceSlug && projectId)
|
if (!storeLabels && workspaceSlug && projectId)
|
||||||
|
|
@ -102,11 +109,17 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (query !== "" && e.key === "Escape") {
|
if (query !== "" && e.key === "Escape") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setQuery("");
|
setQuery("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query !== "" && e.key === "Enter") {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
await handleAddLabel(query);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -249,6 +262,15 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<ComboDropDown
|
<ComboDropDown
|
||||||
as="div"
|
as="div"
|
||||||
|
|
@ -314,10 +336,19 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||||
)}
|
)}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))
|
))
|
||||||
|
) : submitting ? (
|
||||||
|
<Loader className="spin h-3.5 w-3.5" />
|
||||||
|
) : canCreateLabel ? (
|
||||||
|
<p
|
||||||
|
onClick={() => {
|
||||||
|
handleAddLabel(query);
|
||||||
|
}}
|
||||||
|
className="text-left text-custom-text-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
+ Add <span className="text-custom-text-100">"{query}"</span> to labels
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-2 p-1">
|
<p className="text-left text-custom-text-200 ">No matching results.</p>
|
||||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue