[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:
Vamsi Krishna 2024-12-17 14:29:56 +05:30 committed by GitHub
parent 4507802aba
commit 8e6d885731
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 82 additions and 33 deletions

View file

@ -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>
); );
}); });

View file

@ -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">&quot;{query}&quot;</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>

View file

@ -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}
/> />
); );
}; };

View file

@ -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;

View file

@ -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">&quot;{query}&quot;</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>