UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: поиск из лупы, маршрутизация и статусные dropdown

This commit is contained in:
DCCONSTRUCTIONS 2026-04-19 21:38:11 +03:00
parent 21581373cd
commit 16f1552b22
8 changed files with 279 additions and 289 deletions

View File

@ -7,7 +7,6 @@
import type { ReactNode } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { Badge } from "@plane/propel/badge";
import type { TExternalContourRequest } from "@plane/types";
import { Avatar } from "@plane/ui";
import { renderFormattedDate } from "@plane/utils";
@ -54,7 +53,7 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.target_contour")}>
<Badge variant="neutral">{targetProjectName}</Badge>
{targetProjectName}
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.status")}>
@ -72,7 +71,7 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon
{assigneeDetails.length > 0 ? (
<div className="flex flex-wrap items-center gap-2">
{assigneeDetails.map((assignee) => (
<div key={assignee.id} className="flex items-center gap-2 rounded-sm border border-strong px-2 py-1">
<div key={assignee.id} className="flex items-center gap-2">
<Avatar src={assignee.avatar_url || ""} name={assignee.display_name || t("common.none")} size="sm" showTooltip />
<span className="text-12 font-medium text-secondary">{assignee.display_name}</span>
</div>
@ -91,13 +90,13 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon
{requestedAt ? renderFormattedDate(requestedAt) : t("common.none")}
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.due_date")}>
{dueDate}
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.last_updated")}>
{lastUpdatedAt ? renderFormattedDate(lastUpdatedAt) : t("common.none")}
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.due_date")}>
{dueDate}
</TraceabilityCell>
</div>
</div>
);

View File

@ -19,7 +19,7 @@ export function ExternalContourStatePill(props: Props) {
const state = request.issue.state_detail;
return (
<div className="inline-flex items-center gap-1 rounded-sm border border-subtle bg-layer-2 px-1.5 py-0.5 text-11 font-medium text-secondary">
<div className="inline-flex items-center gap-1.5 text-13 font-medium text-secondary">
<StateGroupIcon
stateGroup={state?.group ?? "backlog"}
color={state?.color}

View File

@ -11,7 +11,7 @@ import { usePopper } from "react-popper";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { SearchIcon, IntakeStateGroupIcon, ChevronDownIcon } from "@plane/propel/icons";
import { SearchIcon, IntakeStateGroupIcon, ChevronDownIcon, CheckIcon } from "@plane/propel/icons";
import type { IIntakeState } from "@plane/types";
import { ComboDropDown, Spinner } from "@plane/ui";
import { cn } from "@plane/utils";
@ -21,9 +21,6 @@ import { BUTTON_VARIANTS_WITH_TEXT } from "@/components/dropdowns/constants";
import type { TDropdownProps } from "@/components/dropdowns/types";
// hooks
import { useDropdown } from "@/hooks/use-dropdown";
// plane web imports
import { StateOption } from "@/plane-web/components/workflow";
export type TWorkItemStateDropdownBaseProps = TDropdownProps & {
alwaysAllowStateChange?: boolean;
button?: ReactNode;
@ -135,13 +132,14 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
handleClose();
};
const comboButton = (
<>
{button ? (
const comboButton = button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
className={cn(
"clickable block h-full w-full rounded-full border-0 bg-transparent shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0",
buttonContainerClassName
)}
onClick={handleOnClick}
disabled={disabled}
tabIndex={tabIndex}
@ -154,7 +152,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
"clickable block h-full max-w-full rounded-full border-0 bg-transparent shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0",
{
"cursor-not-allowed text-secondary": disabled,
"cursor-pointer": !disabled,
@ -197,8 +195,6 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
)}
</DropdownButton>
</button>
)}
</>
);
return (
@ -216,17 +212,17 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
className="nodedc-glass-modal nodedc-glass-surface my-1 w-52 rounded-[1.25rem] border-0 px-3 py-3 text-12 shadow-none outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-sm border border-subtle bg-surface-2 px-2">
<div className="flex items-center gap-1.5 rounded-[0.95rem] border-0 bg-white/5 px-3 py-2 outline-none">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder outline-none focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("common.search.label")}
@ -234,17 +230,28 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<StateOption
{...props}
<Combobox.Option
key={option.value}
option={option}
selectedValue={value}
className="flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none"
/>
value={option.value}
className={({ active, selected }) =>
cn(
`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-[0.9rem] px-2 py-2 select-none outline-none ${
active ? "bg-white/6" : ""
} ${selected ? "text-primary" : "text-secondary"}`
)
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>

View File

@ -102,12 +102,6 @@ function BorderButton(props: ButtonProps) {
>
{!hideIcon &&
(priority ? (
<div
className={cn({
// highlight just the icon if text is visible and priority is urgent
"rounded-sm border border-priority-urgent p-0.5": priority === "urgent" && !hideText && highlightUrgent,
})}
>
<PriorityIcon
priority={priority}
size={12}
@ -118,10 +112,8 @@ function BorderButton(props: ButtonProps) {
"translate-x-[0.0625rem]": hideText && priority === "high",
"translate-x-0.5": hideText && priority === "medium",
"translate-x-1": hideText && priority === "low",
// highlight the icon if priority is urgent
})}
/>
</div>
) : (
<SignalHigh className="size-3" />
))}
@ -132,7 +124,7 @@ function BorderButton(props: ButtonProps) {
"text-placeholder": !priority || priority === "none",
})}
>
{priorityDetails?.title ?? placeholder}
{priority ? t(priority) : placeholder}
</span>
)}
{dropdownArrow && (
@ -193,12 +185,6 @@ function BackgroundButton(props: ButtonProps) {
>
{!hideIcon &&
(priority ? (
<div
className={cn({
// highlight just the icon if text is visible and priority is urgent
"rounded-sm border border-priority-urgent p-0.5": priority === "urgent" && !hideText && highlightUrgent,
})}
>
<PriorityIcon
priority={priority}
size={12}
@ -209,10 +195,8 @@ function BackgroundButton(props: ButtonProps) {
"translate-x-[0.0625rem]": hideText && priority === "high",
"translate-x-0.5": hideText && priority === "medium",
"translate-x-1": hideText && priority === "low",
// highlight the icon if priority is urgent
})}
/>
</div>
) : (
<SignalHigh className="size-3" />
))}
@ -223,7 +207,7 @@ function BackgroundButton(props: ButtonProps) {
"text-placeholder": !priority || priority === "none",
})}
>
{priorityDetails?.title ?? t("common.priority") ?? placeholder}
{priority ? t(priority) : t("common.priority") ?? placeholder}
</span>
)}
{dropdownArrow && (
@ -277,12 +261,6 @@ function TransparentButton(props: ButtonProps) {
>
{!hideIcon &&
(priority ? (
<div
className={cn({
// highlight just the icon if text is visible and priority is urgent
"rounded-sm border border-priority-urgent p-0.5": priority === "urgent" && !hideText && highlightUrgent,
})}
>
<PriorityIcon
priority={priority}
size={12}
@ -293,10 +271,8 @@ function TransparentButton(props: ButtonProps) {
"translate-x-[0.0625rem]": hideText && priority === "high",
"translate-x-0.5": hideText && priority === "medium",
"translate-x-1": hideText && priority === "low",
// highlight the icon if priority is urgent
})}
/>
</div>
) : (
<SignalHigh className="size-3" />
))}
@ -307,7 +283,7 @@ function TransparentButton(props: ButtonProps) {
"text-placeholder": !priority || priority === "none",
})}
>
{priorityDetails?.title ?? t("common.priority") ?? placeholder}
{priority ? t(priority) : t("common.priority") ?? placeholder}
</span>
)}
{dropdownArrow && (
@ -368,8 +344,8 @@ export function PriorityDropdown(props: Props) {
query: priority.key,
content: (
<div className="flex items-center gap-2">
<PriorityIcon priority={priority.key} size={14} withContainer />
<span className="flex-grow truncate">{priority.title}</span>
<PriorityIcon priority={priority.key} size={14} />
<span className="flex-grow truncate">{t(priority.key)}</span>
</div>
),
}));
@ -398,9 +374,7 @@ export function PriorityDropdown(props: Props) {
? BackgroundButton
: TransparentButton;
const comboButton = (
<>
{button ? (
const comboButton = button ? (
<button
ref={setReferenceElement}
type="button"
@ -440,21 +414,13 @@ export function PriorityDropdown(props: Props) {
renderToolTipByDefault={renderByDefault}
/>
</button>
)}
</>
);
return (
<ComboDropDown
as="div"
ref={dropdownRef}
className={cn(
"h-full",
{
"bg-layer-1": isOpen,
},
className
)}
className={cn("h-full", className)}
value={value}
onChange={dropdownOnChange}
disabled={disabled}
@ -465,17 +431,17 @@ export function PriorityDropdown(props: Props) {
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
className="nodedc-glass-modal nodedc-glass-surface my-1 w-52 rounded-[1.25rem] border-0 px-3 py-3 text-12 shadow-none outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-sm border border-subtle bg-surface-2 px-2">
<div className="flex items-center gap-1.5 rounded-[0.95rem] border-0 bg-white/5 px-3 py-2 outline-none">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder outline-none focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("search")}
@ -483,7 +449,7 @@ export function PriorityDropdown(props: Props) {
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
@ -491,8 +457,8 @@ export function PriorityDropdown(props: Props) {
value={option.value}
className={({ active, selected }) =>
cn(
`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none ${
active ? "bg-layer-transparent-hover" : ""
`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-[0.9rem] px-2 py-2 select-none outline-none ${
active ? "bg-white/6" : ""
} ${selected ? "text-primary" : "text-secondary"}`
)
}

View File

@ -11,7 +11,7 @@ import { usePopper } from "react-popper";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { SearchIcon, StateGroupIcon, ChevronDownIcon } from "@plane/propel/icons";
import { SearchIcon, StateGroupIcon, ChevronDownIcon, CheckIcon } from "@plane/propel/icons";
import type { IState } from "@plane/types";
import { ComboDropDown, Spinner } from "@plane/ui";
import { cn } from "@plane/utils";
@ -21,9 +21,6 @@ import { BUTTON_VARIANTS_WITH_TEXT } from "@/components/dropdowns/constants";
import type { TDropdownProps } from "@/components/dropdowns/types";
// hooks
import { useDropdown } from "@/hooks/use-dropdown";
// plane web imports
import { StateOption } from "@/plane-web/components/workflow";
export type TWorkItemStateDropdownBaseProps = TDropdownProps & {
alwaysAllowStateChange?: boolean;
button?: ReactNode;
@ -136,13 +133,14 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
handleClose();
};
const comboButton = (
<>
{button ? (
const comboButton = button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
className={cn(
"clickable block h-full w-full rounded-full border-0 bg-transparent shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0",
buttonContainerClassName
)}
onClick={handleOnClick}
disabled={disabled}
tabIndex={tabIndex}
@ -155,7 +153,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
"clickable block h-full max-w-full rounded-full border-0 bg-transparent shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0",
{
"cursor-not-allowed text-secondary": disabled,
"cursor-pointer": !disabled,
@ -199,8 +197,6 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
)}
</DropdownButton>
</button>
)}
</>
);
return (
@ -218,17 +214,17 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
className="nodedc-glass-modal nodedc-glass-surface my-1 w-52 rounded-[1.25rem] border-0 px-3 py-3 text-12 shadow-none outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-sm border border-subtle bg-surface-2 px-2">
<div className="flex items-center gap-1.5 rounded-[0.95rem] border-0 bg-white/5 px-3 py-2 outline-none">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder outline-none focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("common.search.label")}
@ -236,17 +232,28 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<StateOption
{...props}
<Combobox.Option
key={option.value}
option={option}
selectedValue={value}
className="flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none"
/>
value={option.value}
className={({ active, selected }) =>
cn(
`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-[0.9rem] px-2 py-2 select-none outline-none ${
active ? "bg-white/6" : ""
} ${selected ? "text-primary" : "text-secondary"}`
)
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>

View File

@ -9,6 +9,7 @@ import { Command } from "cmdk";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { createPortal } from "react-dom";
import { useTranslation } from "@plane/i18n";
// hooks
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils";
@ -28,6 +29,7 @@ type TTopNavPowerKProps = {
export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
const { variant = "top-navigation" } = props;
const { t } = useTranslation();
// router
const router = useAppRouter();
const params = useParams();
@ -313,7 +315,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
placeholder="Search commands..."
placeholder={t("power_k.search_menu.quick_command_placeholder")}
className="placeholder-text-placeholder min-w-0 flex-1 bg-transparent text-13 text-primary outline-none"
/>
{searchTerm && (
@ -388,7 +390,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
placeholder="Search commands..."
placeholder={t("power_k.search_menu.quick_command_placeholder")}
className="placeholder-text-placeholder min-w-0 flex-1 bg-transparent text-13 text-primary outline-none"
autoFocus
/>
@ -398,7 +400,12 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
</button>
)}
</div>
<div className="nodedc-glass-modal nodedc-glass-surface mt-3 flex max-h-[70vh] w-full flex-col overflow-hidden rounded-[1.5rem] pt-3">
<div className="nodedc-glass-modal nodedc-glass-surface absolute bottom-full left-0 mb-3 flex max-h-[70vh] w-full flex-col overflow-hidden rounded-[1.5rem] pt-3">
<div className="px-4 pb-2">
<div className="text-[13px] font-medium text-secondary">
{t("power_k.search_menu.quick_access_title")}
</div>
</div>
{searchCommandContent}
</div>
</div>

View File

@ -2891,6 +2891,8 @@ export default {
search_menu: {
no_results: "No results found",
clear_search: "Clear search",
quick_access_title: "Quick access",
quick_command_placeholder: "Find a quick command",
},
footer: {
workspace_level: "Workspace level",

View File

@ -3044,6 +3044,8 @@ export default {
search_menu: {
no_results: "Ничего не найдено",
clear_search: "Очистить поиск",
quick_access_title: "Быстрый доступ",
quick_command_placeholder: "Найти быструю команду",
},
footer: {
workspace_level: "Уровень рабочего пространства",