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 @@ -2,7 +2,7 @@ import type { ReactNode } from "react";
import { cn } from "@plane/utils";

type TSidebarPropertyListItemProps = {
icon: React.FC<{ className?: string }>;
icon: React.FC<{ className?: string }> | React.ReactNode;
label: string;
children: ReactNode;
appendElement?: ReactNode;
Expand All @@ -14,8 +14,8 @@ export function SidebarPropertyListItem(props: TSidebarPropertyListItemProps) {

return (
<div className="flex items-center gap-2">
<div className="flex shrink-0 items-center gap-1 w-30 text-body-xs-regular text-tertiary h-7.5">
<Icon className="h-4 w-4 shrink-0" />
<div className="flex shrink-0 items-center gap-1.5 w-30 text-body-xs-regular text-tertiary h-7.5">
{typeof Icon === "function" ? <Icon className="size-4 shrink-0" /> : Icon}
<span>{label}</span>
{appendElement}
</div>
Expand Down
7 changes: 1 addition & 6 deletions apps/web/core/components/dropdowns/priority.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ReactNode } from "react";
import { Fragment, useRef, useState } from "react";
import { useTheme } from "next-themes";
import { useRef, useState } from "react";
import { usePopper } from "react-popper";
import { Check, Search, SignalHigh } from "lucide-react";
import { Combobox } from "@headlessui/react";
Expand Down Expand Up @@ -341,10 +340,6 @@ export function PriorityDropdown(props: Props) {
],
});

// next-themes
// TODO: remove this after new theming implementation
const { resolvedTheme } = useTheme();

const options = ISSUE_PRIORITIES.map((priority) => ({
value: priority.key,
query: priority.key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ export const LabelListItem = observer(function LabelListItem(props: TLabelListIt
key={labelId}
type="button"
className={cn(
"h-full w-min flex items-center gap-1.5 rounded-lg px-2 py-0.5 bg-layer-transparent-active group text-body-xs-regular text-tertiary",
"h-full w-min flex items-center gap-1.5 rounded-sm px-2 py-0.5 bg-layer-transparent-active group text-body-xs-regular text-tertiary",
{
"cursor-pointer": !disabled,
}
)}
onClick={handleLabel}
disabled={disabled}
>
<LabelFilledIcon className="size-4" color={label.color ?? "#000000"} />
<LabelFilledIcon className="size-3" color={label.color ?? "#000000"} />
<div className="flex-shrink-0 text-body-xs-regular">{label.name}</div>
{!disabled && (
<div className="flex-shrink-0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const IssueLabel = observer(function IssueLabel(props: TIssueLabel) {
);

return (
<div className="relative flex flex-wrap items-center gap-1 px-2">
<div className="relative flex flex-wrap items-center gap-1 min-h-7.5 w-full">
<LabelList
workspaceSlug={workspaceSlug}
projectId={projectId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Combobox } from "@headlessui/react";
// plane imports
import { EUserPermissionsLevel, getRandomLabelColor } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { LabelFilledIcon, LabelPropertyIcon } from "@plane/propel/icons";
import type { IIssueLabel } from "@plane/types";
import { EUserProjectRoles } from "@plane/types";
// helpers
Expand Down Expand Up @@ -86,15 +85,9 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
const issueLabels = values ?? [];

const label = (
<button
type="button"
className="h-full w-full flex items-center gap-1.5 rounded-lg px-2 py-0.5 bg-layer-transparent-active hover:bg-layer-transparent-hover text-body-xs-regular text-tertiary"
>
<div className="flex-shrink-0">
<LabelFilledIcon className="size-3.5" />
</div>
<div className="flex-shrink-0">{t("label.select")}</div>
</button>
<span className="size-full flex items-center rounded-sm px-2 py-0.5 bg-layer-transparent hover:bg-layer-transparent-hover text-body-xs-regular text-tertiary">
{t("label.select")}
</span>
);

const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -121,101 +114,149 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
if (!issueId || !values) return <></>;

return (
<>
<Combobox
as="div"
className={`w-auto max-w-full flex-shrink-0 text-left`}
value={issueLabels}
onChange={(value) => onSelect(value)}
multiple
>
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className="cursor-pointer"
onClick={() => !projectLabels && fetchLabels()}
>
{label}
</button>
</Combobox.Button>

<Combobox.Options className="fixed z-10">
<div
className={`z-10 my-1 w-48 whitespace-nowrap rounded-sm border border-strong bg-surface-1 py-2.5 text-11 shadow-raised-200 focus:outline-none`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="px-2">
<div className="flex w-full items-center justify-start rounded-sm border border-subtle bg-surface-2 px-2">
<Search className="h-3.5 w-3.5 text-tertiary" />
<Combobox.Input
className="w-full bg-transparent px-2 py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("common.search.label")}
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
tabIndex={baseTabIndex}
/>
</div>
<Combobox
as="div"
className="size-full flex-shrink-0 text-left"
value={issueLabels}
onChange={(value) => onSelect(value)}
multiple
>
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className="cursor-pointer size-full"
onClick={() => !projectLabels && fetchLabels()}
>
{label}
</button>
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<div
className={`z-10 my-1 w-48 whitespace-nowrap rounded-sm border border-strong bg-surface-1 py-2.5 text-11 shadow-raised-200 focus:outline-none`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="px-2">
<div className="flex w-full items-center justify-start rounded-sm border border-subtle bg-surface-2 px-2">
<Search className="h-3.5 w-3.5 text-tertiary" />
<Combobox.Input
className="w-full bg-transparent px-2 py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("common.search.label")}
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
tabIndex={baseTabIndex}
/>
</div>
<div className={`vertical-scrollbar scrollbar-sm mt-2 max-h-48 space-y-1 overflow-y-scroll px-2 pr-0`}>
{isLoading ? (
<p className="text-center text-secondary">{t("common.loading")}</p>
) : filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 hover:bg-layer-1 ${
selected ? "text-primary" : "text-secondary"
}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && (
<div className="flex-shrink-0">
<Check className={`h-3.5 w-3.5`} />
</div>
)}
</>
)}
</Combobox.Option>
))
) : submitting ? (
<Loader className="spin h-3.5 w-3.5" />
) : canCreateLabel ? (
</div>
<div className={`vertical-scrollbar scrollbar-sm mt-2 max-h-48 space-y-1 overflow-y-scroll px-2 pr-0`}>
{isLoading ? (
<p className="text-center text-secondary">{t("common.loading")}</p>
) : filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
value={query}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!query.length) return;
handleAddLabel(query);
}}
className={`text-left text-secondary ${query.length ? "cursor-pointer" : "cursor-default"}`}
key={option.value}
value={option.value}
className={({ selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 hover:bg-layer-1 ${
selected ? "text-primary" : "text-secondary"
}`
}
>
{query.length ? (
{({ selected }) => (
<>
{/* TODO: Translate here */}+ Add <span className="text-primary">&quot;{query}&quot;</span> to
labels
{option.content}
{selected && (
<div className="flex-shrink-0">
<Check className={`h-3.5 w-3.5`} />
</div>
)}
</>
) : (
t("label.create.type")
)}
</Combobox.Option>
))
) : submitting ? (
<Loader className="spin h-3.5 w-3.5" />
) : canCreateLabel ? (
<Combobox.Option
value={query}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!query.length) return;
handleAddLabel(query);
}}
className={`text-left text-secondary ${query.length ? "cursor-pointer" : "cursor-default"}`}
>
{query.length ? (
<>
{/* TODO: Translate here */}+ Add <span className="text-primary">&quot;{query}&quot;</span> to
labels
</>
) : (
t("label.create.type")
)}
</Combobox.Option>
) : (
<p className="text-left text-secondary ">{t("common.search.no_matching_results")}</p>
)}
</div>
</div>
<div className={`vertical-scrollbar scrollbar-sm mt-2 max-h-48 space-y-1 overflow-y-scroll px-2 pr-0`}>
{isLoading ? (
<p className="text-center text-secondary">{t("common.loading")}</p>
) : filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 hover:bg-layer-1 ${
selected ? "text-primary" : "text-secondary"
}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && (
<div className="flex-shrink-0">
<Check className={`h-3.5 w-3.5`} />
</div>
)}
</>
)}
</Combobox.Option>
))
) : submitting ? (
<Loader className="spin h-3.5 w-3.5" />
) : canCreateLabel ? (
<Combobox.Option
value={query}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!query.length) return;
handleAddLabel(query);
}}
className={`text-left text-secondary ${query.length ? "cursor-pointer" : "cursor-default"}`}
>
{query.length ? (
<>
{/* TODO: Translate here */}+ Add <span className="text-primary">&quot;{query}&quot;</span> to labels
</>
) : (
<p className="text-left text-secondary ">{t("common.search.no_matching_results")}</p>
t("label.create.type")
)}
</div>
</div>
</Combobox.Options>
</Combobox>
</>
</Combobox.Option>
) : (
<p className="text-left text-secondary ">{t("common.search.no_matching_results")}</p>
)}
</div>
Comment on lines +208 to +258
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire dropdown options list (lines 208-258) is duplicated outside the dropdown container. This duplicate block should be removed as it will render the same list of options twice, once inside the proper dropdown container (lines 155-206) and once outside of it. Only the first instance (lines 155-206) inside the dropdown container should remain.

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +258
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Duplicate code block renders dropdown content twice.

Lines 208-258 duplicate the exact same content from lines 155-206. This renders the options list twice inside Combobox.Options. Remove this entire block.

         </div>
       </div>
-      <div className={`vertical-scrollbar scrollbar-sm mt-2 max-h-48 space-y-1 overflow-y-scroll px-2 pr-0`}>
-        {isLoading ? (
-          <p className="text-center text-secondary">{t("common.loading")}</p>
-        ) : filteredOptions.length > 0 ? (
-          filteredOptions.map((option) => (
-            <Combobox.Option
-              key={option.value}
-              value={option.value}
-              className={({ selected }) =>
-                `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 hover:bg-layer-1 ${
-                  selected ? "text-primary" : "text-secondary"
-                }`
-              }
-            >
-              {({ selected }) => (
-                <>
-                  {option.content}
-                  {selected && (
-                    <div className="flex-shrink-0">
-                      <Check className={`h-3.5 w-3.5`} />
-                    </div>
-                  )}
-                </>
-              )}
-            </Combobox.Option>
-          ))
-        ) : submitting ? (
-          <Loader className="spin  h-3.5 w-3.5" />
-        ) : canCreateLabel ? (
-          <Combobox.Option
-            value={query}
-            onClick={(e) => {
-              e.preventDefault();
-              e.stopPropagation();
-              if (!query.length) return;
-              handleAddLabel(query);
-            }}
-            className={`text-left text-secondary ${query.length ? "cursor-pointer" : "cursor-default"}`}
-          >
-            {query.length ? (
-              <>
-                {/* TODO: Translate here */}+ Add <span className="text-primary">&quot;{query}&quot;</span> to labels
-              </>
-            ) : (
-              t("label.create.type")
-            )}
-          </Combobox.Option>
-        ) : (
-          <p className="text-left text-secondary ">{t("common.search.no_matching_results")}</p>
-        )}
-      </div>
     </Combobox.Options>
   </Combobox>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/core/components/issues/issue-detail/label/select/label-select.tsx
around lines 208 to 258, remove the entire duplicated block that renders the
dropdown content (the conditional rendering that produces the options, loader,
create-item option, and no-results message) because it repeats the exact same
code from lines 155-206; keep the original single block (lines 155-206) inside
Combobox.Options so the list is rendered only once, ensure no other JSX
references this removed block, and run the app/tests to confirm no regressions.

</Combobox.Options>
</Combobox>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ export const IssueParentSelect = observer(function IssueParentSelect(props: TIss
"group flex items-center justify-between gap-2 px-2 py-0.5 rounded-sm outline-none",
{
"cursor-not-allowed": disabled,
"hover:bg-layer-1": !disabled,
"bg-layer-1": isParentIssueModalOpen,
"hover:bg-layer-transparent-hover": !disabled,
"bg-layer-transparent-selected": isParentIssueModalOpen,
},
className
)}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/core/components/issues/issue-detail/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
/>
) : (
<div className="flex h-full w-full overflow-hidden">
<div className="max-w-2/3 h-full w-full space-y-6 overflow-y-auto px-9 py-5">
<div className="h-full w-full space-y-6 overflow-y-auto px-9 py-5">
<IssueMainContent
workspaceSlug={workspaceSlug}
projectId={projectId}
Expand All @@ -312,7 +312,7 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
/>
</div>
<div
className="fixed right-0 z-[5] h-full w-full min-w-[300px] border-l border-subtle bg-surface-1 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
className="fixed right-0 z-[5] h-full w-full min-w-[300px] border-l border-subtle bg-surface-1 sm:w-1/2 md:relative md:w-1/4 lg:min-w-80 xl:min-w-96"
style={issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
>
<IssueDetailsSidebar
Expand Down
15 changes: 7 additions & 8 deletions apps/web/core/components/issues/issue-detail/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from "react";
import { observer } from "mobx-react";
// i18n
import { useTranslation } from "@plane/i18n";
Expand Down Expand Up @@ -81,8 +80,8 @@ export const IssueDetailsSidebar = observer(function IssueDetailsSidebar(props:
<>
<div className="flex items-center h-full w-full flex-col divide-y-2 divide-subtle-1 overflow-hidden">
<div className="h-full w-full overflow-y-auto px-6">
<h5 className="mt-6 text-body-xs-medium">{t("common.properties")}</h5>
<div className={`mb-2 mt-3 space-y-2.5 ${!isEditable ? "opacity-60" : ""}`}>
<h5 className="mt-5 text-body-xs-medium">{t("common.properties")}</h5>
<div className={`mb-2 mt-4 space-y-2.5 ${!isEditable ? "opacity-60" : ""}`}>
<SidebarPropertyListItem icon={StatePropertyIcon} label={t("common.state")}>
<StateDropdown
value={issue?.state_id}
Expand Down Expand Up @@ -121,10 +120,10 @@ export const IssueDetailsSidebar = observer(function IssueDetailsSidebar(props:
value={issue?.priority}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
disabled={!isEditable}
buttonVariant="border-with-text"
className="w-full grow rounded-lg"
buttonContainerClassName="w-full text-left px-2 h-7.5"
buttonClassName="w-min h-6 whitespace-nowrap"
buttonVariant="transparent-with-text"
className="w-full h-7.5 grow rounded-sm"
buttonContainerClassName="size-full text-left"
buttonClassName="size-full px-2 py-0.5 whitespace-nowrap [&_svg]:size-3.5"
/>
</SidebarPropertyListItem>

Expand Down Expand Up @@ -236,7 +235,7 @@ export const IssueDetailsSidebar = observer(function IssueDetailsSidebar(props:

<SidebarPropertyListItem icon={ParentPropertyIcon} label={t("common.parent")}>
<IssueParentSelectRoot
className="h-full w-full grow"
className="w-full h-7.5 grow"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
Expand Down
Loading
Loading