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 @@ -71,7 +71,7 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));

const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-end",
placement: "bottom-start",
modifiers: [
{
name: "preventOverflow",
Expand Down Expand Up @@ -114,149 +114,101 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
if (!issueId || !values) return <></>;

return (
<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}
/>
<>
<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>
</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) => (
<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
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"
}`
}
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"}`}
>
{({ selected }) => (
{query.length ? (
<>
{option.content}
{selected && (
<div className="flex-shrink-0">
<Check className={`h-3.5 w-3.5`} />
</div>
)}
{/* TODO: Translate here */}+ Add <span className="text-primary">&quot;{query}&quot;</span> to
labels
</>
) : (
t("label.create.type")
)}
</Combobox.Option>
Comment on lines 186 to 204
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 | 🟠 Major

Potential bug: value={query} may corrupt label selection.

When this "Add label" option is clicked, two things happen:

  1. onClick calls handleAddLabel(query) which creates the label and updates the selection with the new label ID
  2. Headless UI's Combobox also processes the selection and appends value (the query string) to the selected values array

This results in onSelect being called with the raw query string mixed into the label IDs array, potentially corrupting the issue's label data.

Consider using a sentinel value or handling the add-label flow outside of the Combobox selection mechanism.

               ) : 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"}`}
-                >
+                <div
+                  onClick={() => {
+                    if (!query.length) return;
+                    handleAddLabel(query);
+                  }}
+                  className={`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 hover:bg-layer-1 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>
+                </div>
               ) : (

))
) : 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
</>
) : (
t("label.create.type")
<p className="text-left text-secondary ">{t("common.search.no_matching_results")}</p>
)}
</Combobox.Option>
) : (
<p className="text-left text-secondary ">{t("common.search.no_matching_results")}</p>
)}
</div>
</Combobox.Options>
</Combobox>
</div>
</div>
</Combobox.Options>
</Combobox>
</>
);
});
2 changes: 1 addition & 1 deletion apps/web/core/components/issues/issue-detail/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ 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-5 text-body-xs-medium">{t("common.properties")}</h5>
<div className={`mb-2 mt-4 space-y-2.5 ${!isEditable ? "opacity-60" : ""}`}>
<div className={`mb-2 mt-4 space-y-2.5 truncate ${!isEditable ? "opacity-60" : ""}`}>
<SidebarPropertyListItem icon={StatePropertyIcon} label={t("common.state")}>
<StateDropdown
value={issue?.state_id}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/core/components/issues/peek-overview/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
{/* content */}
<div className="vertical-scrollbar scrollbar-md relative h-full w-full overflow-hidden overflow-y-auto">
{["side-peek", "modal"].includes(peekMode) ? (
<div className="relative flex flex-col gap-3 px-12 py-6 space-y-3">
<div className="relative flex flex-col gap-3 px-8 py-5 space-y-3">
<PeekOverviewIssueDetails
editorRef={editorRef}
workspaceSlug={workspaceSlug}
Expand Down
Loading