Skip to content

Commit 8e6d885

Browse files
[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
1 parent 4507802 commit 8e6d885

File tree

5 files changed

+82
-33
lines changed

5 files changed

+82
-33
lines changed

web/core/components/issues/issue-detail/label/root.tsx

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ import { IIssueLabel, TIssue, TIssueServiceType } from "@plane/types";
77
// components
88
import { TOAST_TYPE, setToast } from "@plane/ui";
99
// hooks
10-
import { useIssueDetail, useLabel, useProjectInbox, useUserPermissions } from "@/hooks/store";
10+
import { useIssueDetail, useLabel, useProjectInbox } from "@/hooks/store";
1111
// ui
1212
// types
13-
import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./";
13+
import { LabelList, IssueLabelSelectRoot } from "./";
1414
// TODO: Fix this import statement, as core should not import from ee
1515
// eslint-disable-next-line import/order
16-
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
1716

1817
export type TIssueLabel = {
1918
workspaceSlug: string;
@@ -47,9 +46,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
4746
issue: { getIssueById },
4847
} = useIssueDetail(issueServiceType);
4948
const { getIssueInboxByIssueId } = useProjectInbox();
50-
const { allowPermissions } = useUserPermissions();
5149

52-
const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
5350
const issue = isInboxIssue ? getIssueInboxByIssueId(issueId)?.issue : getIssueById(issueId);
5451

5552
const labelOperations: TLabelOperations = useMemo(
@@ -113,16 +110,6 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
113110
labelOperations={labelOperations}
114111
/>
115112
)}
116-
117-
{!disabled && canCreateLabel && (
118-
<LabelCreate
119-
workspaceSlug={workspaceSlug}
120-
projectId={projectId}
121-
issueId={issueId}
122-
values={issue?.label_ids || []}
123-
labelOperations={labelOperations}
124-
/>
125-
)}
126113
</div>
127114
);
128115
});

web/core/components/issues/issue-detail/label/select/label-select.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
11
import { Fragment, useState } from "react";
22
import { observer } from "mobx-react";
33
import { usePopper } from "react-popper";
4-
import { Check, Search, Tag } from "lucide-react";
4+
import { Check, Loader, Search, Tag } from "lucide-react";
55
import { Combobox } from "@headlessui/react";
66
// helpers
7+
import { IIssueLabel } from "@plane/types";
8+
import { getRandomLabelColor } from "@/constants/label";
79
import { getTabIndex } from "@/helpers/tab-indices.helper";
810
// hooks
9-
import { useLabel } from "@/hooks/store";
11+
import { useLabel, useUserPermissions } from "@/hooks/store";
1012
import { usePlatformOS } from "@/hooks/use-platform-os";
11-
// components
12-
13+
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
14+
//constants
1315
export interface IIssueLabelSelect {
1416
workspaceSlug: string;
1517
projectId: string;
1618
issueId: string;
1719
values: string[];
1820
onSelect: (_labelIds: string[]) => void;
21+
onAddLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<any>;
1922
}
2023

2124
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
22-
const { workspaceSlug, projectId, issueId, values, onSelect } = props;
25+
const { workspaceSlug, projectId, issueId, values, onSelect, onAddLabel } = props;
2326
// store hooks
2427
const { isMobile } = usePlatformOS();
2528
const { fetchProjectLabels, getProjectLabels } = useLabel();
29+
const { allowPermissions } = useUserPermissions();
2630
// states
2731
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
2832
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
2933
const [isLoading, setIsLoading] = useState<boolean>(false);
3034
const [query, setQuery] = useState("");
35+
const [submitting, setSubmitting] = useState<boolean>(false);
36+
37+
const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
3138

3239
const projectLabels = getProjectLabels(projectId);
3340

@@ -83,11 +90,25 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
8390
</div>
8491
);
8592

86-
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
93+
const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
8794
if (query !== "" && e.key === "Escape") {
8895
e.stopPropagation();
8996
setQuery("");
9097
}
98+
99+
if (query !== "" && e.key === "Enter") {
100+
e.stopPropagation();
101+
e.preventDefault();
102+
await handleAddLabel(query);
103+
}
104+
};
105+
106+
const handleAddLabel = async (labelName: string) => {
107+
setSubmitting(true);
108+
const label = await onAddLabel(workspaceSlug, projectId, { name: labelName, color: getRandomLabelColor() });
109+
onSelect([...values, label.id]);
110+
setQuery("");
111+
setSubmitting(false);
91112
};
92113

93114
if (!issueId || !values) return <></>;
@@ -159,10 +180,19 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
159180
)}
160181
</Combobox.Option>
161182
))
183+
) : submitting ? (
184+
<Loader className="spin h-3.5 w-3.5" />
185+
) : canCreateLabel ? (
186+
<p
187+
onClick={() => {
188+
handleAddLabel(query);
189+
}}
190+
className="text-left text-custom-text-200 cursor-pointer"
191+
>
192+
+ Add <span className="text-custom-text-100">&quot;{query}&quot;</span> to labels
193+
</p>
162194
) : (
163-
<span className="flex items-center gap-2 p-1">
164-
<p className="text-left text-custom-text-200 ">No matching results</p>
165-
</span>
195+
<p className="text-left text-custom-text-200 ">No matching results.</p>
166196
)}
167197
</div>
168198
</div>

web/core/components/issues/issue-detail/label/select/root.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const IssueLabelSelectRoot: FC<TIssueLabelSelectRoot> = (props) => {
2626
issueId={issueId}
2727
values={values}
2828
onSelect={handleLabel}
29+
onAddLabel={labelOperations.createLabel}
2930
/>
3031
);
3132
};

web/core/components/issues/issue-layouts/kanban/block.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-d
66
import { observer } from "mobx-react";
77
import { useParams } from "next/navigation";
88
// plane helpers
9+
import { EIssueServiceType } from "@plane/constants";
910
import { useOutsideClickDetector } from "@plane/hooks";
1011
// types
1112
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
@@ -26,7 +27,6 @@ import { IssueIdentifier } from "@/plane-web/components/issues";
2627
import { TRenderQuickActions } from "../list/list-view-types";
2728
import { IssueProperties } from "../properties/all-properties";
2829
import { getIssueBlockId } from "../utils";
29-
import { EIssueServiceType } from "@plane/constants";
3030

3131
interface IssueBlockProps {
3232
issueId: string;

web/core/components/issues/issue-layouts/properties/labels.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"use client";
22

3-
import { Fragment, useEffect, useRef, useState } from "react";
3+
import { useEffect, useRef, useState } from "react";
44
import { Placement } from "@popperjs/core";
55
import { observer } from "mobx-react";
66
import { useParams } from "next/navigation";
77
import { usePopper } from "react-popper";
8-
import { Check, ChevronDown, Search, Tags } from "lucide-react";
8+
import { Check, ChevronDown, Loader, Search, Tags } from "lucide-react";
99
import { Combobox } from "@headlessui/react";
1010
// plane helpers
1111
import { useOutsideClickDetector } from "@plane/hooks";
@@ -14,9 +14,12 @@ import { IIssueLabel } from "@plane/types";
1414
// ui
1515
import { ComboDropDown, Tooltip } from "@plane/ui";
1616
// hooks
17-
import { useLabel } from "@/hooks/store";
17+
import { getRandomLabelColor } from "@/constants/label";
18+
import { useLabel, useUserPermissions } from "@/hooks/store";
1819
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
1920
import { usePlatformOS } from "@/hooks/use-platform-os";
21+
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
22+
// constants
2023

2124
export interface IIssuePropertyLabels {
2225
projectId: string | null;
@@ -62,6 +65,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
6265
// states
6366
const [query, setQuery] = useState("");
6467
const [isOpen, setIsOpen] = useState(false);
68+
const [submitting, setSubmitting] = useState<boolean>(false);
6569
// refs
6670
const dropdownRef = useRef<HTMLDivElement | null>(null);
6771
const inputRef = useRef<HTMLInputElement | null>(null);
@@ -70,9 +74,12 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
7074
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
7175
const [isLoading, setIsLoading] = useState<boolean>(false);
7276
// store hooks
73-
const { fetchProjectLabels, getProjectLabels } = useLabel();
77+
const { fetchProjectLabels, getProjectLabels, createLabel } = useLabel();
7478
const { isMobile } = usePlatformOS();
7579
const storeLabels = getProjectLabels(projectId);
80+
const { allowPermissions } = useUserPermissions();
81+
82+
const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
7683

7784
const onOpen = () => {
7885
if (!storeLabels && workspaceSlug && projectId)
@@ -102,11 +109,17 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
102109

103110
useOutsideClickDetector(dropdownRef, handleClose);
104111

105-
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
112+
const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
106113
if (query !== "" && e.key === "Escape") {
107114
e.stopPropagation();
108115
setQuery("");
109116
}
117+
118+
if (query !== "" && e.key === "Enter") {
119+
e.stopPropagation();
120+
e.preventDefault();
121+
await handleAddLabel(query);
122+
}
110123
};
111124

112125
useEffect(() => {
@@ -249,6 +262,15 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
249262
</button>
250263
);
251264

265+
const handleAddLabel = async (labelName: string) => {
266+
if (!projectId) return;
267+
setSubmitting(true);
268+
const label = await createLabel(workspaceSlug, projectId, { name: labelName, color: getRandomLabelColor() });
269+
onChange([...value, label.id]);
270+
setQuery("");
271+
setSubmitting(false);
272+
};
273+
252274
return (
253275
<ComboDropDown
254276
as="div"
@@ -314,10 +336,19 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
314336
)}
315337
</Combobox.Option>
316338
))
339+
) : submitting ? (
340+
<Loader className="spin h-3.5 w-3.5" />
341+
) : canCreateLabel ? (
342+
<p
343+
onClick={() => {
344+
handleAddLabel(query);
345+
}}
346+
className="text-left text-custom-text-200 cursor-pointer"
347+
>
348+
+ Add <span className="text-custom-text-100">&quot;{query}&quot;</span> to labels
349+
</p>
317350
) : (
318-
<span className="flex items-center gap-2 p-1">
319-
<p className="text-left text-custom-text-200 ">No matching results</p>
320-
</span>
351+
<p className="text-left text-custom-text-200 ">No matching results.</p>
321352
)}
322353
</div>
323354
</div>

0 commit comments

Comments
 (0)