UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: верхняя панель, rich filters и runtime-fix DEBUG=release
This commit is contained in:
parent
b1912c64e9
commit
ae124e50e5
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"nodedc": {
|
||||||
|
"accent_rgb": [195, 255, 102],
|
||||||
|
"passive_card_rgb": [42, 43, 46],
|
||||||
|
"active_card_rgb": [195, 255, 102]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,11 +23,25 @@ from plane.utils.url import is_valid_url
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
def env_flag(value, default=False):
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
normalized = str(value).strip().lower()
|
||||||
|
if normalized in {"1", "true", "yes", "on", "debug"}:
|
||||||
|
return True
|
||||||
|
if normalized in {"0", "false", "no", "off", "release", "prod", "production"}:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
# Secret Key
|
# Secret Key
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
|
SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = int(os.environ.get("DEBUG", "0"))
|
DEBUG = env_flag(os.environ.get("DEBUG", "0"))
|
||||||
|
|
||||||
# Self-hosted mode
|
# Self-hosted mode
|
||||||
IS_SELF_MANAGED = True
|
IS_SELF_MANAGED = True
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import os
|
||||||
from .common import * # noqa
|
from .common import * # noqa
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
DEBUG = env_flag(os.environ.get("DEBUG", 0), default=False)
|
||||||
|
|
||||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export const IssuesHeader = observer(function IssuesHeader() {
|
||||||
)}
|
)}
|
||||||
</Header.LeftItem>
|
</Header.LeftItem>
|
||||||
<Header.RightItem>
|
<Header.RightItem>
|
||||||
<div className="hidden gap-2 md:flex">
|
<div className="hidden items-center gap-2 md:flex">
|
||||||
<HeaderFilters
|
<HeaderFilters
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
currentProjectDetails={currentProjectDetails}
|
currentProjectDetails={currentProjectDetails}
|
||||||
|
|
@ -120,6 +120,7 @@ export const IssuesHeader = observer(function IssuesHeader() {
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
className="nodedc-toolbar-primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -94,47 +94,54 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
|
||||||
projectDetails={currentProjectDetails ?? undefined}
|
projectDetails={currentProjectDetails ?? undefined}
|
||||||
isEpic={storeType === EIssuesStoreType.EPIC}
|
isEpic={storeType === EIssuesStoreType.EPIC}
|
||||||
/>
|
/>
|
||||||
<div className="hidden @4xl:flex">
|
<div className="nodedc-top-toolbar-cluster flex items-center gap-2">
|
||||||
<LayoutSelection
|
<div className="hidden @4xl:flex">
|
||||||
layouts={LAYOUTS}
|
<LayoutSelection
|
||||||
onChange={(layout) => handleLayoutChange(layout)}
|
layouts={LAYOUTS}
|
||||||
selectedLayout={activeLayout}
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
/>
|
selectedLayout={activeLayout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex @4xl:hidden">
|
||||||
|
<MobileLayoutSelection
|
||||||
|
layouts={LAYOUTS}
|
||||||
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
|
activeLayout={activeLayout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<WorkItemFiltersToggle entityType={storeType} entityId={projectId} />
|
||||||
|
<FiltersDropdown
|
||||||
|
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
||||||
|
title={t("common.display")}
|
||||||
|
placement="bottom-end"
|
||||||
|
>
|
||||||
|
<DisplayFiltersSelection
|
||||||
|
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
||||||
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
|
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||||
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
|
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||||
|
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||||
|
isEpic={storeType === EIssuesStoreType.EPIC}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
|
{canUserCreateIssue ? (
|
||||||
|
<Button
|
||||||
|
className="nodedc-toolbar-pill hidden md:inline-flex"
|
||||||
|
onClick={() => setAnalyticsModal(true)}
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="hidden @4xl:flex">{t("common.analytics")}</div>
|
||||||
|
<div className="flex @4xl:hidden">
|
||||||
|
<ChartNoAxesColumn className="size-3.5" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex @4xl:hidden">
|
|
||||||
<MobileLayoutSelection
|
|
||||||
layouts={LAYOUTS}
|
|
||||||
onChange={(layout) => handleLayoutChange(layout)}
|
|
||||||
activeLayout={activeLayout}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<WorkItemFiltersToggle entityType={storeType} entityId={projectId} />
|
|
||||||
<FiltersDropdown
|
|
||||||
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
|
||||||
title={t("common.display")}
|
|
||||||
placement="bottom-end"
|
|
||||||
>
|
|
||||||
<DisplayFiltersSelection
|
|
||||||
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
|
||||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
|
||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
|
||||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
|
||||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
|
||||||
isEpic={storeType === EIssuesStoreType.EPIC}
|
|
||||||
/>
|
|
||||||
</FiltersDropdown>
|
|
||||||
{canUserCreateIssue ? (
|
|
||||||
<Button className="hidden px-2 md:block" onClick={() => setAnalyticsModal(true)} variant="secondary" size="lg">
|
|
||||||
<div className="hidden @4xl:flex">{t("common.analytics")}</div>
|
|
||||||
<div className="flex @4xl:hidden">
|
|
||||||
<ChartNoAxesColumn className="size-3.5" />
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,10 @@ export const FilterDisplayProperties = observer(function FilterDisplayProperties
|
||||||
<button
|
<button
|
||||||
key={displayProperty.key}
|
key={displayProperty.key}
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded-sm border px-2 py-0.5 text-11 transition-all ${
|
className={`rounded-full border-0 px-3 py-1.5 text-12 transition-all ${
|
||||||
displayProperties?.[displayProperty.key]
|
displayProperties?.[displayProperty.key]
|
||||||
? "border-accent-strong bg-accent-primary text-on-color"
|
? "bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]"
|
||||||
: "border-subtle hover:bg-layer-1"
|
: "bg-white/5 text-secondary hover:bg-white/8"
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleUpdate({
|
handleUpdate({
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,12 @@ export function FiltersDropdown(props: Props) {
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
prependIcon={icon}
|
prependIcon={icon}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className="relative"
|
className="nodedc-toolbar-pill relative"
|
||||||
|
data-active={open}
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className={`${open ? "text-primary" : "text-secondary"}`}>
|
<div className={`${open ? "text-[rgb(var(--nodedc-accent-rgb))]" : "text-secondary"}`}>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
{isFiltersApplied && (
|
{isFiltersApplied && (
|
||||||
|
|
@ -80,6 +81,7 @@ export function FiltersDropdown(props: Props) {
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
className="nodedc-toolbar-pill"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
{miniIcon || title}
|
{miniIcon || title}
|
||||||
|
|
@ -100,7 +102,7 @@ export function FiltersDropdown(props: Props) {
|
||||||
{/** translate-y-0 is a hack to create new stacking context. Required for safari */}
|
{/** translate-y-0 is a hack to create new stacking context. Required for safari */}
|
||||||
<Popover.Panel className="fixed z-10 translate-y-0">
|
<Popover.Panel className="fixed z-10 translate-y-0">
|
||||||
<div
|
<div
|
||||||
className="my-1 overflow-hidden rounded-sm border border-subtle bg-surface-1 shadow-raised-100"
|
className="nodedc-dropdown-surface my-1 overflow-hidden"
|
||||||
ref={setPopperElement}
|
ref={setPopperElement}
|
||||||
style={styles.popper}
|
style={styles.popper}
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ type Props = {
|
||||||
|
|
||||||
export function FilterHeader({ title, isPreviewEnabled, handleIsPreviewEnabled }: Props) {
|
export function FilterHeader({ title, isPreviewEnabled, handleIsPreviewEnabled }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-0 flex items-center justify-between gap-2 bg-surface-1">
|
<div className="sticky top-0 flex items-center justify-between gap-2 bg-transparent pb-1">
|
||||||
<div className="flex-grow truncate text-caption-sm-medium text-placeholder">{title}</div>
|
<div className="flex-grow truncate text-caption-sm-medium text-placeholder">{title}</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-5 w-5 flex-shrink-0 place-items-center rounded-sm hover:bg-layer-transparent-hover"
|
className="nodedc-toolbar-icon-button grid h-7 w-7 flex-shrink-0 place-items-center"
|
||||||
onClick={handleIsPreviewEnabled}
|
onClick={handleIsPreviewEnabled}
|
||||||
>
|
>
|
||||||
{isPreviewEnabled ? <ChevronUpIcon height={14} width={14} /> : <ChevronDownIcon height={14} width={14} />}
|
{isPreviewEnabled ? <ChevronUpIcon height={14} width={14} /> : <ChevronDownIcon height={14} width={14} />}
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,20 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FilterOption(props: Props) {
|
export function FilterOption(props: Props) {
|
||||||
const { icon, isChecked, multiple = true, onClick, title, activePulse = false } = props;
|
const { icon, isChecked, onClick, title, activePulse = false } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center gap-2 rounded-sm p-1.5 hover:bg-layer-transparent-hover"
|
className="nodedc-dropdown-option"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`grid h-3 w-3 flex-shrink-0 place-items-center border ${
|
className={`grid h-4 w-4 flex-shrink-0 place-items-center border-0 ${
|
||||||
isChecked ? "border-accent-strong bg-accent-primary text-on-color" : "border-strong"
|
isChecked
|
||||||
} ${multiple ? "rounded-xs" : "rounded-full"}`}
|
? "bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]"
|
||||||
|
: "bg-white/6 text-transparent"
|
||||||
|
} rounded-full`}
|
||||||
>
|
>
|
||||||
{isChecked && <CheckIcon width={10} height={10} strokeWidth={3} />}
|
{isChecked && <CheckIcon width={10} height={10} strokeWidth={3} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,25 +32,25 @@ export function LayoutSelection(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 rounded-md bg-layer-3 p-1">
|
<div className="nodedc-toolbar-group flex items-center gap-1">
|
||||||
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
|
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
|
||||||
<Tooltip key={layout.key} tooltipContent={t(layout.i18n_title)} isMobile={isMobile}>
|
<Tooltip key={layout.key} tooltipContent={t(layout.i18n_title)} isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group grid h-5.5 w-7 place-items-center overflow-hidden rounded-sm transition-all hover:bg-layer-transparent-hover",
|
"nodedc-toolbar-icon-button group grid h-8 w-8 place-items-center overflow-hidden"
|
||||||
{
|
|
||||||
"bg-layer-transparent-active hover:bg-layer-transparent-active": selectedLayout === layout.key,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
|
data-active={selectedLayout === layout.key}
|
||||||
onClick={() => handleOnChange(layout.key)}
|
onClick={() => handleOnChange(layout.key)}
|
||||||
>
|
>
|
||||||
<IssueLayoutIcon
|
<span className="nodedc-toolbar-icon-active-dot">
|
||||||
layout={layout.key}
|
<IssueLayoutIcon
|
||||||
size={14}
|
layout={layout.key}
|
||||||
strokeWidth={2}
|
size={14}
|
||||||
className={`size-3.5 ${selectedLayout == layout.key ? "text-primary" : "text-secondary"}`}
|
strokeWidth={2}
|
||||||
/>
|
className={`size-3.5 ${selectedLayout == layout.key ? "text-[#0b1117]" : "text-secondary"}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ export function MobileLayoutSelection({
|
||||||
className="flex flex-grow justify-center text-13 text-secondary"
|
className="flex flex-grow justify-center text-13 text-secondary"
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
customButton={
|
customButton={
|
||||||
<Button variant="secondary" className="relative px-2">
|
<Button variant="secondary" className="nodedc-toolbar-pill relative gap-2 px-3">
|
||||||
{activeLayout && (
|
<span className="nodedc-toolbar-icon-active-dot bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]">
|
||||||
<IssueLayoutIcon layout={activeLayout} size={14} strokeWidth={2} className={`h-3.5 w-3.5`} />
|
{activeLayout && (
|
||||||
)}
|
<IssueLayoutIcon layout={activeLayout} size={14} strokeWidth={2} className={`h-3.5 w-3.5`} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
<ChevronDownIcon className="my-auto size-3 text-secondary" strokeWidth={2} />
|
<ChevronDownIcon className="my-auto size-3 text-secondary" strokeWidth={2} />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -312,12 +312,14 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
: "rounded-lg border border-subtle bg-layer-2 p-3 shadow-raised-100 outline-[0.5px] outline-transparent hover:border-strong hover:shadow-raised-200",
|
: "rounded-lg border border-subtle bg-layer-2 p-3 shadow-raised-100 outline-[0.5px] outline-transparent hover:border-strong hover:shadow-raised-200",
|
||||||
{ "hover:cursor-pointer": isDragAllowed },
|
{ "hover:cursor-pointer": isDragAllowed },
|
||||||
{
|
{
|
||||||
"bg-[#C3FF66] text-[#111111]": cardVariant === "internal-contour" && isPeeked,
|
"bg-[rgb(var(--nodedc-card-active-rgb))] text-[#111111]":
|
||||||
"bg-[#2A2B2E] text-white": cardVariant === "internal-contour" && !isPeeked,
|
cardVariant === "internal-contour" && isPeeked,
|
||||||
|
"bg-[rgb(var(--nodedc-card-passive-rgb))] text-white":
|
||||||
|
cardVariant === "internal-contour" && !isPeeked,
|
||||||
"border border-accent-strong hover:border-accent-strong": cardVariant !== "internal-contour" && isPeeked,
|
"border border-accent-strong hover:border-accent-strong": cardVariant !== "internal-contour" && isPeeked,
|
||||||
},
|
},
|
||||||
{ "z-[100] bg-layer-1": isCurrentBlockDragging && cardVariant !== "internal-contour" },
|
{ "z-[100] bg-layer-1": isCurrentBlockDragging && cardVariant !== "internal-contour" },
|
||||||
{ "z-[100] bg-[#C3FF66]": isCurrentBlockDragging && cardVariant === "internal-contour" }
|
{ "z-[100] bg-[rgb(var(--nodedc-card-active-rgb))]": isCurrentBlockDragging && cardVariant === "internal-contour" }
|
||||||
)}
|
)}
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
disabled={!!issue?.tempId}
|
disabled={!!issue?.tempId}
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,9 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||||
const foregroundClasses = isActive ? "text-[#111111]" : "text-white";
|
const foregroundClasses = isActive ? "text-[#111111]" : "text-white";
|
||||||
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
|
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
|
||||||
const pillBackgroundClasses = isActive ? "bg-black/10 text-[#111111]" : "bg-[#1B1B1F] text-white";
|
const pillBackgroundClasses =
|
||||||
const iconBubbleClasses = isActive ? "bg-black text-[#C3FF66]" : "bg-[#111214] text-white";
|
isActive ? "bg-black/10 text-[#111111]" : "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
|
||||||
|
const iconBubbleClasses = isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white";
|
||||||
const statusIconColor = selectedState?.color ?? (isActive ? "#111111" : "var(--text-color-primary)");
|
const statusIconColor = selectedState?.color ?? (isActive ? "#111111" : "var(--text-color-primary)");
|
||||||
|
|
||||||
const handleEventPropagation = (e: React.MouseEvent) => {
|
const handleEventPropagation = (e: React.MouseEvent) => {
|
||||||
|
|
@ -95,7 +96,9 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
ref={menuActionRef}
|
ref={menuActionRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-8 w-8 cursor-pointer items-center justify-center rounded-full p-1 transition-colors",
|
"flex h-8 w-8 cursor-pointer items-center justify-center rounded-full p-1 transition-colors",
|
||||||
isActive ? "bg-black text-[#C3FF66] hover:bg-black/90" : "bg-[#111214] text-white hover:bg-[#0A0B0C]",
|
isActive
|
||||||
|
? "bg-black text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90"
|
||||||
|
: "bg-[#111214] text-white hover:bg-[#0A0B0C]",
|
||||||
isMenuActive && (isActive ? "bg-black/90" : "bg-[#0A0B0C]")
|
isMenuActive && (isActive ? "bg-black/90" : "bg-[#0A0B0C]")
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,14 @@ export const AddFilterButton = observer(function AddFilterButton<P extends TFilt
|
||||||
{...props}
|
{...props}
|
||||||
buttonConfig={{
|
buttonConfig={{
|
||||||
...buttonConfig,
|
...buttonConfig,
|
||||||
className: cn(getButtonStyling(variant, size), "py-[5px]", className),
|
className: cn(getButtonStyling(variant, size), "nodedc-toolbar-filter-toggle", className),
|
||||||
}}
|
}}
|
||||||
handleFilterSelect={handleFilterSelect}
|
handleFilterSelect={handleFilterSelect}
|
||||||
customButton={
|
customButton={
|
||||||
<div className="flex items-center gap-1">
|
<div className={cn("flex items-center", label ? "gap-1" : "justify-center")}>
|
||||||
{iconConfig.shouldShowIcon && <FilterIcon className="size-4 text-secondary" />}
|
{iconConfig.shouldShowIcon && (
|
||||||
|
<FilterIcon className={cn("size-4 text-secondary", !label && "translate-y-[3px]")} />
|
||||||
|
)}
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type { IFilterInstance } from "@plane/shared-state";
|
||||||
import type { TExternalFilter, TFilterProperty, TSupportedOperators } from "@plane/types";
|
import type { TExternalFilter, TFilterProperty, TSupportedOperators } from "@plane/types";
|
||||||
import { CustomSearchSelect } from "@plane/ui";
|
import { CustomSearchSelect } from "@plane/ui";
|
||||||
import { getOperatorForPayload } from "@plane/utils";
|
import { getOperatorForPayload } from "@plane/utils";
|
||||||
|
import { localizeRichFilterLabel } from "../i18n";
|
||||||
|
|
||||||
export type TAddFilterDropdownProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
export type TAddFilterDropdownProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||||
customButton: React.ReactNode;
|
customButton: React.ReactNode;
|
||||||
|
|
@ -40,12 +41,12 @@ export const AddFilterDropdown = observer(function AddFilterDropdown<
|
||||||
{config.icon && (
|
{config.icon && (
|
||||||
<config.icon className="size-4 text-tertiary transition-transform duration-200 ease-in-out" />
|
<config.icon className="size-4 text-tertiary transition-transform duration-200 ease-in-out" />
|
||||||
)}
|
)}
|
||||||
<span>{config.label}</span>
|
<span>{localizeRichFilterLabel(config.label)}</span>
|
||||||
</div>
|
</div>
|
||||||
{config.rightContent}
|
{config.rightContent}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
query: config.label.toLowerCase(),
|
query: localizeRichFilterLabel(config.label).toLowerCase(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// If all filters are applied, show disabled options
|
// If all filters are applied, show disabled options
|
||||||
|
|
@ -54,8 +55,8 @@ export const AddFilterDropdown = observer(function AddFilterDropdown<
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
value: "all_filters_applied",
|
value: "all_filters_applied",
|
||||||
content: <div className="text-placeholder italic">All filters applied</div>,
|
content: <div className="text-placeholder italic">Все фильтры уже применены</div>,
|
||||||
query: "all filters applied",
|
query: "все фильтры уже применены",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const FilterItemCloseButton = observer(function FilterItemCloseButton<
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleRemoveFilter}
|
onClick={handleRemoveFilter}
|
||||||
className="bg-layer-transparent px-1.5 text-placeholder hover:bg-layer-transparent-hover hover:text-tertiary focus:outline-none"
|
className="rounded-r-full bg-transparent px-2 text-placeholder hover:bg-white/6 hover:text-tertiary focus:outline-none"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Remove filter"
|
aria-label="Remove filter"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,9 @@ export function FilterItemContainer(props: FilterItemContainerProps) {
|
||||||
<Tooltip tooltipContent={tooltipContent} position="bottom" disabled={!tooltipContent}>
|
<Tooltip tooltipContent={tooltipContent} position="bottom" disabled={!tooltipContent}>
|
||||||
<div
|
<div
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
className={cn("flex h-7 items-stretch overflow-hidden rounded-sm border transition-all duration-200", {
|
className={cn("flex min-h-9 items-stretch overflow-hidden rounded-full transition-all duration-200", {
|
||||||
"border-subtle bg-surface-1": variant === "default",
|
"nodedc-filter-chip": variant === "default",
|
||||||
"border-danger-strong bg-surface-2": variant === "error",
|
"bg-[#3b1212]/80 text-[#ffd6d6] rounded-full": variant === "error",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type { IFilterInstance } from "@plane/shared-state";
|
||||||
import type { TExternalFilter, TFilterProperty, TSupportedOperators } from "@plane/types";
|
import type { TExternalFilter, TFilterProperty, TSupportedOperators } from "@plane/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { AddFilterDropdown } from "../add-filters/dropdown";
|
import { AddFilterDropdown } from "../add-filters/dropdown";
|
||||||
|
import { localizeRichFilterLabel } from "../i18n";
|
||||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../shared";
|
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../shared";
|
||||||
|
|
||||||
interface IFilterItemPropertyProps<P extends TFilterProperty, E extends TExternalFilter> {
|
interface IFilterItemPropertyProps<P extends TFilterProperty, E extends TExternalFilter> {
|
||||||
|
|
@ -52,12 +53,13 @@ type TPropertyButtonProps<P extends TFilterProperty, E extends TExternalFilter>
|
||||||
|
|
||||||
function PropertyButton<P extends TFilterProperty, E extends TExternalFilter>(props: TPropertyButtonProps<P, E>) {
|
function PropertyButton<P extends TFilterProperty, E extends TExternalFilter>(props: TPropertyButtonProps<P, E>) {
|
||||||
const { icon: Icon, label, tooltipContent, className } = props;
|
const { icon: Icon, label, tooltipContent, className } = props;
|
||||||
|
const localizedLabel = localizeRichFilterLabel(label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipContent={tooltipContent} position="bottom-start" disabled={!tooltipContent}>
|
<Tooltip tooltipContent={tooltipContent} position="bottom-start" disabled={!tooltipContent}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full min-w-0 items-center gap-1 px-2 py-[5px] text-11 text-tertiary",
|
"flex h-full min-w-0 items-center gap-1.5 px-3 py-[7px] text-13 text-tertiary",
|
||||||
COMMON_FILTER_ITEM_BORDER_CLASSNAME,
|
COMMON_FILTER_ITEM_BORDER_CLASSNAME,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
@ -67,7 +69,7 @@ function PropertyButton<P extends TFilterProperty, E extends TExternalFilter>(pr
|
||||||
<Icon className="size-3.5" />
|
<Icon className="size-3.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="truncate">{label}</span>
|
<span className="truncate">{localizedLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { CustomSearchSelect } from "@plane/ui";
|
||||||
import { cn, getOperatorForPayload } from "@plane/utils";
|
import { cn, getOperatorForPayload } from "@plane/utils";
|
||||||
// local imports
|
// local imports
|
||||||
import { FilterValueInput } from "../filter-value-input/root";
|
import { FilterValueInput } from "../filter-value-input/root";
|
||||||
|
import { localizeRichFilterOperator } from "../i18n";
|
||||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../shared";
|
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../shared";
|
||||||
import { FilterItemCloseButton } from "./close-button";
|
import { FilterItemCloseButton } from "./close-button";
|
||||||
import { FilterItemContainer } from "./container";
|
import { FilterItemContainer } from "./container";
|
||||||
|
|
@ -44,8 +45,8 @@ export const FilterItem = observer(function FilterItem<P extends TFilterProperty
|
||||||
?.getAllDisplayOperatorOptionsByValue(condition.value as TFilterValue)
|
?.getAllDisplayOperatorOptionsByValue(condition.value as TFilterValue)
|
||||||
.map((option) => ({
|
.map((option) => ({
|
||||||
value: option.value,
|
value: option.value,
|
||||||
content: option.label,
|
content: localizeRichFilterOperator(option.label),
|
||||||
query: option.label.toLowerCase(),
|
query: localizeRichFilterOperator(option.label).toLowerCase(),
|
||||||
}));
|
}));
|
||||||
const selectedOperatorFieldConfig = filterConfig?.getOperatorConfig(condition.operator);
|
const selectedOperatorFieldConfig = filterConfig?.getOperatorConfig(condition.operator);
|
||||||
const selectedOperatorOption = filterConfig?.getDisplayOperatorByValue(
|
const selectedOperatorOption = filterConfig?.getDisplayOperatorByValue(
|
||||||
|
|
@ -102,15 +103,15 @@ export const FilterItem = observer(function FilterItem<P extends TFilterProperty
|
||||||
options={operatorOptions}
|
options={operatorOptions}
|
||||||
className={COMMON_FILTER_ITEM_BORDER_CLASSNAME}
|
className={COMMON_FILTER_ITEM_BORDER_CLASSNAME}
|
||||||
customButtonClassName={cn(
|
customButtonClassName={cn(
|
||||||
"h-full px-2 text-13 font-regular",
|
"h-full px-3 text-13 font-regular",
|
||||||
isOperatorSelectionDisabled && "hover:bg-layer-2-hover"
|
isOperatorSelectionDisabled && "hover:bg-transparent"
|
||||||
)}
|
)}
|
||||||
optionsClassName="w-48"
|
optionsClassName="w-48"
|
||||||
maxHeight="2xl"
|
maxHeight="2xl"
|
||||||
disabled={isOperatorSelectionDisabled}
|
disabled={isOperatorSelectionDisabled}
|
||||||
customButton={
|
customButton={
|
||||||
<div className="flex h-full items-center" aria-disabled={isOperatorSelectionDisabled}>
|
<div className="flex h-full items-center" aria-disabled={isOperatorSelectionDisabled}>
|
||||||
{filterConfig.getLabelForOperator(selectedOperatorOption)}
|
{localizeRichFilterOperator(filterConfig.getLabelForOperator(selectedOperatorOption))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function SelectedOptionsDisplay<V extends TFilterValue>(props: TSelectedO
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
className="ml-1 whitespace-nowrap text-tertiary"
|
className="ml-1 whitespace-nowrap text-tertiary"
|
||||||
>
|
>
|
||||||
+{remainingCount} more
|
+{remainingCount} еще
|
||||||
</Transition>
|
</Transition>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export const getFormattedOptions = <V extends TFilterValue>(options: IFilterOpti
|
||||||
|
|
||||||
export const getCommonCustomSearchSelectProps = (isDisabled?: boolean) => ({
|
export const getCommonCustomSearchSelectProps = (isDisabled?: boolean) => ({
|
||||||
customButtonClassName: cn(
|
customButtonClassName: cn(
|
||||||
"h-full w-full px-2 text-13 font-regular transition-all duration-300 ease-in-out",
|
"h-full w-full px-3 text-13 font-regular transition-all duration-300 ease-in-out",
|
||||||
!isDisabled && COMMON_FILTER_ITEM_BORDER_CLASSNAME,
|
!isDisabled && COMMON_FILTER_ITEM_BORDER_CLASSNAME,
|
||||||
isDisabled && "hover:bg-surface-1"
|
isDisabled && "hover:bg-surface-1"
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { observer } from "mobx-react";
|
||||||
import { ListFilterPlus } from "lucide-react";
|
import { ListFilterPlus } from "lucide-react";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import type { IFilterInstance } from "@plane/shared-state";
|
import type { IFilterInstance } from "@plane/shared-state";
|
||||||
import type { TExternalFilter, TFilterProperty } from "@plane/types";
|
import type { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||||
|
|
@ -40,6 +41,7 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
|
||||||
variant = "header",
|
variant = "header",
|
||||||
trackerElements,
|
trackerElements,
|
||||||
} = props;
|
} = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
// states
|
// states
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
// derived values
|
// derived values
|
||||||
|
|
@ -53,7 +55,7 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalButtonConfig: Partial<TAddFilterButtonProps<K, E>["buttonConfig"]> = {
|
const modalButtonConfig: Partial<TAddFilterButtonProps<K, E>["buttonConfig"]> = {
|
||||||
label: !hasAnyConditions ? "Filters" : null,
|
label: !hasAnyConditions ? t("common.filters") : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = useCallback(async () => {
|
const handleUpdate = useCallback(async () => {
|
||||||
|
|
@ -92,44 +94,44 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
|
||||||
<ElementTransition show={filter.canClearFilters}>
|
<ElementTransition show={filter.canClearFilters}>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={COMMON_OPERATION_BUTTON_CLASSNAME}
|
className={cn(COMMON_OPERATION_BUTTON_CLASSNAME, "nodedc-filter-clear-button")}
|
||||||
onClick={filter.clearFilters}
|
onClick={filter.clearFilters}
|
||||||
data-ph-element={trackerElements?.clearFilter}
|
data-ph-element={trackerElements?.clearFilter}
|
||||||
>
|
>
|
||||||
{filter.clearFilterOptions?.label ?? "Clear all"}
|
{t("common.clear")}
|
||||||
</Button>
|
</Button>
|
||||||
</ElementTransition>
|
</ElementTransition>
|
||||||
<ElementTransition show={filter.canSaveView}>
|
<ElementTransition show={filter.canSaveView}>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={COMMON_OPERATION_BUTTON_CLASSNAME}
|
className={cn(COMMON_OPERATION_BUTTON_CLASSNAME, "nodedc-filter-clear-button")}
|
||||||
onClick={filter.saveView}
|
onClick={filter.saveView}
|
||||||
data-ph-element={trackerElements?.saveView}
|
data-ph-element={trackerElements?.saveView}
|
||||||
>
|
>
|
||||||
{filter.saveViewOptions?.label ?? "Save view"}
|
{filter.saveViewOptions?.label ?? "Сохранить вид"}
|
||||||
</Button>
|
</Button>
|
||||||
</ElementTransition>
|
</ElementTransition>
|
||||||
<ElementTransition show={filter.canUpdateView}>
|
<ElementTransition show={filter.canUpdateView}>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={COMMON_OPERATION_BUTTON_CLASSNAME}
|
className={cn(COMMON_OPERATION_BUTTON_CLASSNAME, "nodedc-filter-clear-button")}
|
||||||
onClick={handleUpdate}
|
onClick={handleUpdate}
|
||||||
loading={isUpdating}
|
loading={isUpdating}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
data-ph-element={trackerElements?.updateView}
|
data-ph-element={trackerElements?.updateView}
|
||||||
>
|
>
|
||||||
{isUpdating ? "Confirming" : (filter.updateViewOptions?.label ?? "Update view")}
|
{isUpdating ? "Подтверждение..." : (filter.updateViewOptions?.label ?? "Обновить вид")}
|
||||||
</Button>
|
</Button>
|
||||||
</ElementTransition>
|
</ElementTransition>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const mainContent = (
|
const mainContent = (
|
||||||
<div className="flex w-full items-start gap-2 rounded-lg bg-layer-1 px-4 py-2">
|
<div className="nodedc-filter-row-shell flex w-full items-start gap-2 px-3 py-2">
|
||||||
<div className="flex w-full flex-wrap items-center gap-2">{leftContent}</div>
|
<div className="flex w-full flex-wrap items-center gap-2">{leftContent}</div>
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center gap-2 border-l border-subtle pl-4", {
|
className={cn("flex items-center gap-2", {
|
||||||
"border-l-transparent pl-0": !hasAvailableOperations,
|
"hidden": !hasAvailableOperations,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{rightContent}
|
{rightContent}
|
||||||
|
|
@ -137,12 +139,10 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ModalVariant = (
|
const ModalVariant = <div className="flex min-h-11 w-full flex-wrap items-center gap-2 p-2">{mainContent}</div>;
|
||||||
<div className="flex min-h-11 w-full flex-wrap items-center gap-2 rounded-lg bg-layer-1 p-2">{mainContent}</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const HeaderVariant = (
|
const HeaderVariant = (
|
||||||
<Header variant={EHeaderVariant.TERNARY} className="min-h-11 bg-surface-1 !px-3">
|
<Header variant={EHeaderVariant.TERNARY} className="min-h-11 bg-transparent !px-0">
|
||||||
{mainContent}
|
{mainContent}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
|
|
@ -160,7 +160,7 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
|
||||||
return <RowTransition show={filter.isVisible}>{variant === "modal" ? ModalVariant : HeaderVariant}</RowTransition>;
|
return <RowTransition show={filter.isVisible}>{variant === "modal" ? ModalVariant : HeaderVariant}</RowTransition>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const COMMON_OPERATION_BUTTON_CLASSNAME = "py-1";
|
const COMMON_OPERATION_BUTTON_CLASSNAME = "min-h-9 px-4 py-1";
|
||||||
|
|
||||||
type TElementTransitionProps = {
|
type TElementTransitionProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ type TFiltersToggleProps<P extends TFilterProperty, E extends TExternalFilter> =
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMMON_CLASSNAME =
|
const COMMON_CLASSNAME =
|
||||||
"grid place-items-center h-7 w-full py-0.5 px-2 rounded-md border border-subtle-1 transition-all duration-200 cursor-pointer";
|
"nodedc-toolbar-filter-toggle grid h-8 w-8 place-items-center px-0 py-0 transition-all duration-200 cursor-pointer";
|
||||||
|
|
||||||
export const FiltersToggle = observer(function FiltersToggle<P extends TFilterProperty, E extends TExternalFilter>(
|
export const FiltersToggle = observer(function FiltersToggle<P extends TFilterProperty, E extends TExternalFilter>(
|
||||||
props: TFiltersToggleProps<P, E>
|
props: TFiltersToggleProps<P, E>
|
||||||
|
|
@ -40,26 +40,13 @@ export const FiltersToggle = observer(function FiltersToggle<P extends TFilterPr
|
||||||
filter.toggleVisibility();
|
filter.toggleVisibility();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Base classes when filter is active
|
|
||||||
const activeFilterBaseClasses =
|
|
||||||
"text-accent-primary border border-accent-subtle-1 hover:border-accent-subtle-1 active:border-accent-subtle-1 focus:border-accent-subtle-1";
|
|
||||||
|
|
||||||
// State classes that prevent hover/active/focus color changes
|
|
||||||
const noHoverStateClasses = "hover:text-accent-primary active:text-accent-primary focus:text-accent-primary";
|
|
||||||
|
|
||||||
// Background classes based on toggle state (darker when open, lighter when closed)
|
|
||||||
const backgroundClasses = isFilterRowVisible
|
|
||||||
? "bg-accent-subtle-hover hover:bg-accent-subtle-hover active:bg-accent-subtle-hover focus:bg-accent-subtle-hover"
|
|
||||||
: "bg-accent-subtle hover:bg-accent-subtle active:bg-accent-subtle focus:bg-accent-subtle";
|
|
||||||
|
|
||||||
const buttonClassName = cn({
|
const buttonClassName = cn({
|
||||||
[activeFilterBaseClasses]: showFilterRowChangesPill,
|
"nodedc-toolbar-filter-toggle": true,
|
||||||
[backgroundClasses]: showFilterRowChangesPill,
|
"text-[rgb(var(--nodedc-accent-rgb))]": showFilterRowChangesPill,
|
||||||
[noHoverStateClasses]: showFilterRowChangesPill,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconClassName = cn({
|
const iconClassName = cn({
|
||||||
"text-accent-primary [&_path]:fill-current": showFilterRowChangesPill,
|
"text-[rgb(var(--nodedc-accent-rgb))] [&_path]:fill-current": showFilterRowChangesPill,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show the add filter button when there are no active conditions, the filter row is hidden, and no unsaved changes exist
|
// Show the add filter button when there are no active conditions, the filter row is hidden, and no unsaved changes exist
|
||||||
|
|
@ -84,7 +71,8 @@ export const FiltersToggle = observer(function FiltersToggle<P extends TFilterPr
|
||||||
icon={showFilterRowChangesPill ? FilterAppliedIcon : FilterIcon}
|
icon={showFilterRowChangesPill ? FilterAppliedIcon : FilterIcon}
|
||||||
onClick={handleToggleFilter}
|
onClick={handleToggleFilter}
|
||||||
className={buttonClassName}
|
className={buttonClassName}
|
||||||
iconClassName={iconClassName}
|
data-active={showFilterRowChangesPill}
|
||||||
|
iconClassName={cn("translate-y-[3px]", iconClassName)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
const FILTER_LABEL_MAP: Record<string, string> = {
|
||||||
|
Assignees: "Назначенные",
|
||||||
|
Assignee: "Назначенный",
|
||||||
|
Priority: "Приоритет",
|
||||||
|
State: "Статус",
|
||||||
|
"State Group": "Группа статусов",
|
||||||
|
"State Groups": "Группы статусов",
|
||||||
|
Mentions: "Упоминания",
|
||||||
|
Label: "Метка",
|
||||||
|
Labels: "Метки",
|
||||||
|
Project: "Проект",
|
||||||
|
"Created by": "Автор",
|
||||||
|
"Created at": "Дата создания",
|
||||||
|
"Updated at": "Дата обновления",
|
||||||
|
"Start date": "Дата начала",
|
||||||
|
"Target date": "Срок выполнения",
|
||||||
|
"Due date": "Срок выполнения",
|
||||||
|
Parent: "Родительский",
|
||||||
|
};
|
||||||
|
|
||||||
|
const FILTER_OPERATOR_MAP: Record<string, string> = {
|
||||||
|
is: "равно",
|
||||||
|
"is any of": "содержит одно из",
|
||||||
|
between: "между",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const localizeRichFilterLabel = (label: string | undefined) => {
|
||||||
|
if (!label) return "";
|
||||||
|
return FILTER_LABEL_MAP[label] ?? label;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const localizeRichFilterOperator = (label: string | undefined) => {
|
||||||
|
if (!label) return "";
|
||||||
|
return FILTER_OPERATOR_MAP[label] ?? label;
|
||||||
|
};
|
||||||
|
|
@ -12,7 +12,7 @@ import type {
|
||||||
TSupportedFilterFieldConfigs,
|
TSupportedFilterFieldConfigs,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
|
||||||
export const COMMON_FILTER_ITEM_BORDER_CLASSNAME = "border-r border-subtle-1";
|
export const COMMON_FILTER_ITEM_BORDER_CLASSNAME = "nodedc-filter-chip-separator";
|
||||||
|
|
||||||
export const EMPTY_FILTER_PLACEHOLDER_TEXT = "--";
|
export const EMPTY_FILTER_PLACEHOLDER_TEXT = "--";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@
|
||||||
--editor-colors-light-blue-background: #c5eff9;
|
--editor-colors-light-blue-background: #c5eff9;
|
||||||
--editor-colors-dark-blue-background: #c9dafb;
|
--editor-colors-dark-blue-background: #c9dafb;
|
||||||
--editor-colors-purple-background: #e3d8fd;
|
--editor-colors-purple-background: #e3d8fd;
|
||||||
|
--nodedc-accent-rgb: 51 163 255;
|
||||||
|
--nodedc-card-passive-rgb: 42 43 46;
|
||||||
|
--nodedc-card-active-rgb: 195 255 102;
|
||||||
/* end background colors */
|
/* end background colors */
|
||||||
}
|
}
|
||||||
/* background colors */
|
/* background colors */
|
||||||
|
|
@ -354,6 +357,169 @@
|
||||||
@apply bg-white/6;
|
@apply bg-white/6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-group {
|
||||||
|
background: rgba(8, 8, 11, 0.92);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-icon-button {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: transparent !important;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
transition:
|
||||||
|
color 160ms ease,
|
||||||
|
background-color 160ms ease,
|
||||||
|
transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-icon-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-icon-button[data-active="true"] {
|
||||||
|
background: transparent !important;
|
||||||
|
color: rgba(255, 255, 255, 0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-icon-active-dot {
|
||||||
|
display: grid;
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition:
|
||||||
|
background-color 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-icon-button[data-active="true"] .nodedc-toolbar-icon-active-dot {
|
||||||
|
background: rgb(var(--nodedc-accent-rgb));
|
||||||
|
color: #0b1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-pill {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
background: rgba(18, 18, 22, 0.94) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
transition:
|
||||||
|
background-color 160ms ease,
|
||||||
|
color 160ms ease,
|
||||||
|
transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-pill:hover {
|
||||||
|
background: rgba(24, 24, 29, 0.96) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-pill[data-active="true"] {
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-primary {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
background: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
color: #0b1117 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-primary:hover {
|
||||||
|
background: rgba(var(--nodedc-accent-rgb), 0.92) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-filter-toggle {
|
||||||
|
display: grid;
|
||||||
|
height: 2.5rem !important;
|
||||||
|
width: 2.5rem !important;
|
||||||
|
place-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: transparent !important;
|
||||||
|
color: rgba(255, 255, 255, 0.72) !important;
|
||||||
|
transition: color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-filter-toggle:hover,
|
||||||
|
.nodedc-toolbar-filter-toggle:focus-visible {
|
||||||
|
background: transparent !important;
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-toolbar-filter-toggle[data-active="true"] {
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-filter-row-shell {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%),
|
||||||
|
rgba(8, 8, 11, 0.84);
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.35rem !important;
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-top-toolbar-cluster > * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-filter-chip {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.045) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-filter-chip-separator {
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-filter-clear-button {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-filter-clear-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-calendar-shell {
|
.nodedc-calendar-shell {
|
||||||
@apply rounded-[1.1rem] border-0 bg-transparent p-1 shadow-none outline-none;
|
@apply rounded-[1.1rem] border-0 bg-transparent p-1 shadow-none outline-none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
|
||||||
optionsClassName = "",
|
optionsClassName = "",
|
||||||
value,
|
value,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
noResultsMessage = "No matches found",
|
noResultsMessage = "Совпадений не найдено",
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
} = props;
|
} = props;
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
@ -104,14 +104,14 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
|
||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between gap-1 text-11",
|
"flex w-full items-center justify-between gap-1 text-11",
|
||||||
{
|
{
|
||||||
"cursor-not-allowed text-secondary": disabled,
|
"cursor-not-allowed text-secondary": disabled,
|
||||||
"cursor-pointer hover:bg-layer-transparent-hover": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
},
|
},
|
||||||
customButtonClassName
|
customButtonClassName
|
||||||
)}
|
)}
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
>
|
>
|
||||||
{customButton}
|
{customButton}
|
||||||
|
|
@ -122,13 +122,13 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
|
||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between gap-1 rounded-sm border-[0.5px] border-strong",
|
"flex w-full items-center justify-between gap-1 rounded-full border-0 outline-none",
|
||||||
{
|
{
|
||||||
"px-3 py-2 text-13": input,
|
"px-3 py-2 text-13": input,
|
||||||
"px-2 py-1 text-11": !input,
|
"px-2 py-1 text-11": !input,
|
||||||
"cursor-not-allowed text-secondary": disabled,
|
"cursor-not-allowed text-secondary": disabled,
|
||||||
"cursor-pointer hover:bg-layer-transparent-hover": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
},
|
},
|
||||||
buttonClassName
|
buttonClassName
|
||||||
)}
|
)}
|
||||||
|
|
@ -146,25 +146,25 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
|
||||||
<Combobox.Options data-prevent-outside-click static>
|
<Combobox.Options data-prevent-outside-click static>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-30 my-1 min-w-48 overflow-y-scroll rounded-md border-[0.5px] border-subtle-1 bg-surface-1 py-2.5 text-11 whitespace-nowrap focus:outline-none",
|
"nodedc-dropdown-surface z-30 my-1 min-w-48 overflow-y-scroll whitespace-nowrap focus:outline-none",
|
||||||
optionsClassName
|
optionsClassName
|
||||||
)}
|
)}
|
||||||
ref={setPopperElement}
|
ref={setPopperElement}
|
||||||
style={styles.popper}
|
style={styles.popper}
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
>
|
>
|
||||||
<div className="mx-2 flex items-center gap-1.5 rounded-sm border border-subtle px-2">
|
<div className="nodedc-dropdown-search mx-1">
|
||||||
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
|
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
|
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Поиск"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn("vertical-scrollbar mt-2 scrollbar-xs space-y-1 overflow-y-scroll px-2", {
|
className={cn("vertical-scrollbar mt-2 scrollbar-xs space-y-1 overflow-y-scroll px-1", {
|
||||||
"max-h-96": maxHeight === "2xl",
|
"max-h-96": maxHeight === "2xl",
|
||||||
"max-h-80": maxHeight === "xl",
|
"max-h-80": maxHeight === "xl",
|
||||||
"max-h-60": maxHeight === "lg",
|
"max-h-60": maxHeight === "lg",
|
||||||
|
|
@ -181,9 +181,9 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
cn(
|
cn(
|
||||||
"flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none",
|
"nodedc-dropdown-option",
|
||||||
{
|
{
|
||||||
"bg-layer-transparent-hover": active,
|
"bg-white/6": active,
|
||||||
"cursor-not-allowed text-placeholder opacity-60": option.disabled,
|
"cursor-not-allowed text-placeholder opacity-60": option.disabled,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue