Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
{/* labels */}
<div className="h-7">
<IssueLabelSelect
setIsOpen={() => {}}
value={data?.label_ids || []}
onChange={(labelIds) => handleData("label_ids", labelIds)}
projectId={projectId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export const LabelDropdown = (props: ILabelDropdownProps) => {
</Combobox.Option>
))
) : submitting ? (
<Loader className="spin h-3.5 w-3.5" />
<Loader className="animate-spin h-3.5 w-3.5" />
) : canCreateLabel ? (
<p
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ type TIssueDefaultPropertiesProps = {
parentId: string | null;
isDraft: boolean;
handleFormChange: () => void;
setLabelModal: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedParentIssue: (issue: ISearchIssueResponse) => void;
};

Expand All @@ -58,7 +57,6 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
parentId,
isDraft,
handleFormChange,
setLabelModal,
setSelectedParentIssue,
} = props;
// states
Expand All @@ -74,7 +72,8 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = 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());
Expand Down Expand Up @@ -147,15 +146,14 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
render={({ field: { value, onChange } }) => (
<div className="h-7">
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={(labelIds) => {
onChange(labelIds);
handleFormChange();
}}
projectId={projectId ?? undefined}
tabIndex={getIndex("label_ids")}
createLabelEnabled={canCreateLabel}
createLabelEnabled={!!canCreateLabel}
/>
</div>
)}
Expand Down
16 changes: 0 additions & 16 deletions apps/web/core/components/issues/issue-modal/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -97,7 +95,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
} = props;

// states
const [labelModal, setLabelModal] = useState(false);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [isMoving, setIsMoving] = useState<boolean>(false);

Expand Down Expand Up @@ -126,7 +123,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
} = useIssueModal();
const { isMobile } = usePlatformOS();
const { moveIssue } = useWorkspaceDraftIssues();
const { createLabel } = useLabel();

const {
issue: { getIssueById },
Expand Down Expand Up @@ -363,17 +359,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {

return (
<FormProvider {...methods}>
{projectId && (
<CreateLabelModal
createLabel={createLabel.bind(createLabel, workspaceSlug?.toString(), projectId)}
isOpen={labelModal}
handleClose={() => setLabelModal(false)}
onSuccess={(response) => {
setValue<"label_ids">("label_ids", [...watch("label_ids"), response.id]);
handleFormChange();
}}
/>
)}
<div className="flex gap-2 bg-transparent">
<div className="rounded-lg w-full">
<form
Expand Down Expand Up @@ -502,7 +487,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
parentId={watch("parent_id")}
isDraft={isDraft}
handleFormChange={handleFormChange}
setLabelModal={setLabelModal}
setSelectedParentIssue={setSelectedParentIssue}
/>
</div>
Expand Down
75 changes: 62 additions & 13 deletions apps/web/core/components/issues/select/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -26,7 +27,7 @@ export type TWorkItemLabelSelectBaseProps = {
onChange: (value: string[]) => void;
onDropdownOpen?: () => void;
placement?: Placement;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
createLabel?: (data: Partial<IIssueLabel>) => Promise<IIssueLabel>;
tabIndex?: number;
value: string[];
};
Expand All @@ -43,7 +44,7 @@ export const WorkItemLabelSelectBase: React.FC<TWorkItemLabelSelectBaseProps> =
onChange,
onDropdownOpen,
placement,
setIsOpen,
createLabel,
tabIndex,
value,
} = props;
Expand All @@ -55,6 +56,7 @@ export const WorkItemLabelSelectBase: React.FC<TWorkItemLabelSelectBaseProps> =
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [submitting, setSubmitting] = useState<boolean>(false);
// plane hooks
const { t } = useTranslation();
// store hooks
Expand Down Expand Up @@ -88,6 +90,26 @@ export const WorkItemLabelSelectBase: React.FC<TWorkItemLabelSelectBaseProps> =
onChange(val);
};

const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
const q = query.trim();
if (q !== "" && e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
setQuery("");
return;
}
if (
q !== "" &&
e.key === "Enter" &&
!e.nativeEvent.isComposing &&
createLabelEnabled &&
filteredOptions.length === 0 &&
!submitting
) {
e.preventDefault();
await handleAddLabel(q);
}
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);

const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
Expand All @@ -104,6 +126,23 @@ export const WorkItemLabelSelectBase: React.FC<TWorkItemLabelSelectBaseProps> =
}
}, [isDropdownOpen, isMobile]);

const handleAddLabel = async (labelName: string) => {
if (!createLabel || submitting) return;
const name = labelName.trim();
if (!name) return;
setSubmitting(true);
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 (
<Combobox
as="div"
Expand Down Expand Up @@ -165,6 +204,7 @@ export const WorkItemLabelSelectBase: React.FC<TWorkItemLabelSelectBaseProps> =
onChange={(event) => setQuery(event.target.value)}
placeholder={t("search")}
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
Expand Down Expand Up @@ -242,22 +282,31 @@ export const WorkItemLabelSelectBase: React.FC<TWorkItemLabelSelectBaseProps> =
</div>
);
})
) : submitting ? (
<Loader className="animate-spin h-3.5 w-3.5" />
) : createLabelEnabled ? (
<p
onClick={() => {
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 <span className="text-custom-text-100">&quot;{query}&quot;</span> to labels
</>
) : (
t("label.create.type")
)}
</p>
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">{t("no_matching_results")}</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">{t("loading")}</p>
)}
{createLabelEnabled && (
<button
type="button"
className="flex items-center gap-2 w-full select-none rounded px-1 py-2 hover:bg-custom-background-80"
onClick={() => setIsOpen(true)}
>
<Plus className="h-3 w-3" aria-hidden="true" />
<span className="whitespace-nowrap">{t("create_new_label")}</span>
</button>
)}
</div>
</div>
</Combobox.Options>
Expand Down
19 changes: 18 additions & 1 deletion apps/web/core/components/issues/select/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -15,21 +18,35 @@ export const IssueLabelSelect: React.FC<TWorkItemLabelSelectProps> = 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<IIssueLabel>) => {
if (!workspaceSlug || !projectId) {
throw new Error("Workspace slug or project ID is missing");
}
return createLabel(workspaceSlug.toString(), projectId, data);
};

return (
<WorkItemLabelSelectBase
{...props}
getLabelById={getLabelById}
labelIds={projectLabelIds ?? []}
onDropdownOpen={onDropdownOpen}
createLabel={handleCreateLabel}
createLabelEnabled={!!canCreateLabel}
/>
);
});
Loading
Loading