snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import * as React from "react";
import { Combobox as BaseCombobox } from "@base-ui-components/react/combobox";
import { SearchOutline } from "@makeplane/propel/icons";
import { cn } from "../utils/classname";
// Type definitions
type TMaxHeight = "lg" | "md" | "rg" | "sm";
export interface ComboboxProps {
value?: string | string[];
defaultValue?: string | string[];
onValueChange?: (value: string | string[]) => void;
multiSelect?: boolean;
maxSelections?: number;
disabled?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
}
export interface ComboboxButtonProps {
disabled?: boolean;
children?: React.ReactNode;
className?: string;
ref?: React.Ref<HTMLButtonElement>;
}
export interface ComboboxOptionsProps {
searchPlaceholder?: string;
emptyMessage?: string;
showSearch?: boolean;
className?: string;
children?: React.ReactNode;
maxHeight?: TMaxHeight;
inputClassName?: string;
optionsContainerClassName?: string;
positionerClassName?: string;
searchQuery?: string;
onSearchQueryChange?: (query: string) => void;
onSearchQueryKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
dataPreventOutsideClick?: boolean;
}
export interface ComboboxOptionProps {
value: string;
disabled?: boolean;
children?: React.ReactNode;
className?: string;
}
// Constants
const MAX_HEIGHT_CLASSES: Record<TMaxHeight, string> = {
lg: "max-h-60",
md: "max-h-48",
rg: "max-h-36",
sm: "max-h-28",
} as const;
// Root component
function ComboboxRoot({
value,
defaultValue,
onValueChange,
multiSelect = false,
disabled = false,
open,
onOpenChange,
children,
}: ComboboxProps) {
const handleValueChange = React.useCallback(
(newValue: string | string[]) => {
onValueChange?.(newValue);
},
[onValueChange]
);
return (
<BaseCombobox.Root
value={value}
defaultValue={defaultValue}
onValueChange={handleValueChange}
multiple={multiSelect}
disabled={disabled}
open={open}
onOpenChange={onOpenChange}
>
{children}
</BaseCombobox.Root>
);
}
// Trigger button component
const ComboboxButton = React.forwardRef(function ComboboxButton(
{ className, children, disabled = false }: ComboboxButtonProps,
ref: React.ForwardedRef<HTMLButtonElement>
) {
return (
<BaseCombobox.Trigger ref={ref} disabled={disabled} className={className}>
{children}
</BaseCombobox.Trigger>
);
});
// Options popup component
function ComboboxOptions({
children,
showSearch = false,
searchPlaceholder,
maxHeight = "lg",
className,
inputClassName,
optionsContainerClassName,
emptyMessage = "No results found",
positionerClassName,
searchQuery: controlledSearchQuery,
onSearchQueryChange,
onSearchQueryKeyDown,
dataPreventOutsideClick,
}: ComboboxOptionsProps) {
// const [searchQuery, setSearchQuery] = React.useState("");
const [internalSearchQuery, setInternalSearchQuery] = React.useState("");
const searchQuery = controlledSearchQuery !== undefined ? controlledSearchQuery : internalSearchQuery;
const setSearchQuery = React.useCallback(
(query: string) => {
if (onSearchQueryChange) {
onSearchQueryChange(query);
} else {
setInternalSearchQuery(query);
}
},
[onSearchQueryChange]
);
// Filter children based on search query
const filteredChildren = React.useMemo(() => {
if (!showSearch || !searchQuery) return children;
return React.Children.toArray(children).filter((child) => {
// React 19 defaults ReactElement's props to `unknown`; name the shape read below.
if (!React.isValidElement<{ children?: React.ReactNode; value?: string }>(child)) return true;
// Only filter ComboboxOption components, leave other elements (like additional content) unfiltered
if (child.type !== ComboboxOption) return true;
// Extract text content from child to search against
const getTextContent = (node: React.ReactNode): string => {
if (typeof node === "string") return node;
if (typeof node === "number") return String(node);
if (React.isValidElement<{ children?: React.ReactNode }>(node) && node.props.children) {
return getTextContent(node.props.children);
}
if (Array.isArray(node)) {
return node.map(getTextContent).join(" ");
}
return "";
};
const textContent = getTextContent(child.props.children);
const value = child.props.value || "";
const searchLower = searchQuery.toLowerCase();
return textContent.toLowerCase().includes(searchLower) || String(value).toLowerCase().includes(searchLower);
});
}, [children, searchQuery, showSearch]);
return (
<BaseCombobox.Portal>
<BaseCombobox.Positioner sideOffset={8} className={positionerClassName}>
<BaseCombobox.Popup
className={cn("shadow-lg rounded-md border border-subtle bg-surface-1 p-1", className)}
data-prevent-outside-click={dataPreventOutsideClick}
>
<div className="flex flex-col gap-1">
{showSearch && (
<div className="relative">
<SearchOutline className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-placeholder" />
<input
type="text"
placeholder={searchPlaceholder}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={onSearchQueryKeyDown}
className={cn(
"w-full rounded-sm border border-subtle bg-surface-2 py-1.5 pr-2 pl-8 text-13 outline-none placeholder:text-placeholder",
inputClassName
)}
/>
</div>
)}
<BaseCombobox.List
className={cn("overflow-auto outline-none", MAX_HEIGHT_CLASSES[maxHeight], optionsContainerClassName)}
>
{filteredChildren}
{showSearch &&
emptyMessage &&
React.Children.count(
React.Children.toArray(filteredChildren).filter(
(child) => React.isValidElement(child) && child.type === ComboboxOption
)
) === 0 && <div className="px-2 py-1.5 text-13 text-placeholder">{emptyMessage}</div>}
</BaseCombobox.List>
</div>
</BaseCombobox.Popup>
</BaseCombobox.Positioner>
</BaseCombobox.Portal>
);
}
// Individual option component
function ComboboxOption({ value, children, disabled, className }: ComboboxOptionProps) {
return (
<BaseCombobox.Item
value={value}
disabled={disabled}
className={cn("cursor-pointer rounded-sm px-2 py-1.5 text-13 transition-colors outline-none", className)}
>
{children}
</BaseCombobox.Item>
);
}
// Compound component export
const Combobox = Object.assign(ComboboxRoot, {
Button: ComboboxButton,
Options: ComboboxOptions,
Option: ComboboxOption,
});
export { Combobox };