[WEB-5099] improvement: enhance rich filters with new components and configurations (#7916)
* feat: enhance rich filters with new components and configurations - Added `AdditionalFilterValueInput` for unsupported filter types. - Introduced `FilterItem` and related components for better filter item management. - Updated filter configurations to include new properties and support for multiple values. - Improved loading states and error handling in filter components. - Refactored existing filter logic to streamline operations and enhance performance. * Refactor rich filters component structure and enhance filter item functionality - Moved AddFilterButton and AddFilterDropdown to a new directory structure for better organization. - Updated FilterItemProperty to handle filter selection and condition updates more effectively. - Enhanced the FilterInstance class with methods to update condition properties and operators, improving filter management. - Added new functionality to handle invalid filter states and improve user feedback. * [WEB-5111] feat: add 'created_at' and 'updated_at' filters to work item configuration - Introduced new filter configurations for 'created_at' and 'updated_at' in the work item filters. - Updated relevant components to utilize these new filters, enhancing filtering capabilities. - Added corresponding filter configuration functions in the utils for better date handling. * fix: build
This commit is contained in:
parent
9f41e92d21
commit
cfb4a8212c
49 changed files with 854 additions and 247 deletions
|
|
@ -159,6 +159,7 @@ class IssueFilterSet(BaseFilterSet):
|
|||
"start_date": ["exact", "range"],
|
||||
"target_date": ["exact", "range"],
|
||||
"created_at": ["exact", "range"],
|
||||
"updated_at": ["exact", "range"],
|
||||
"is_draft": ["exact"],
|
||||
"priority": ["exact", "in"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { TFilterValue, TFilterProperty } from "@plane/types";
|
||||
// local imports
|
||||
import { TFilterValueInputProps } from "@/components/rich-filters/shared";
|
||||
|
||||
export const AdditionalFilterValueInput = observer(
|
||||
<P extends TFilterProperty, V extends TFilterValue>(_props: TFilterValueInputProps<P, V>) => (
|
||||
// Fallback
|
||||
<div className="h-full flex items-center px-4 text-xs text-custom-text-400 transition-opacity duration-200 cursor-not-allowed">
|
||||
Filter type not supported
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
// plane imports
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// plane web imports
|
||||
import { TWorkItemFiltersEntityProps } from "@/plane-web/hooks/work-item-filters/use-work-item-filters-config";
|
||||
|
||||
export type TGetAdditionalPropsForProjectLevelFiltersHOCParams = {
|
||||
entityType: EIssuesStoreType;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useCallback, useMemo } from "react";
|
|||
import {
|
||||
AtSign,
|
||||
Briefcase,
|
||||
Calendar,
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
CircleUserRound,
|
||||
|
|
@ -32,6 +33,7 @@ import {
|
|||
import { Avatar, Logo } from "@plane/ui";
|
||||
import {
|
||||
getAssigneeFilterConfig,
|
||||
getCreatedAtFilterConfig,
|
||||
getCreatedByFilterConfig,
|
||||
getCycleFilterConfig,
|
||||
getFileURL,
|
||||
|
|
@ -45,6 +47,8 @@ import {
|
|||
getStateGroupFilterConfig,
|
||||
getSubscriberFilterConfig,
|
||||
getTargetDateFilterConfig,
|
||||
getUpdatedAtFilterConfig,
|
||||
isLoaderReady,
|
||||
} from "@plane/utils";
|
||||
// store hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
|
|
@ -72,18 +76,20 @@ export type TUseWorkItemFiltersConfigProps = {
|
|||
} & TWorkItemFiltersEntityProps;
|
||||
|
||||
export type TWorkItemFiltersConfig = {
|
||||
areAllConfigsInitialized: boolean;
|
||||
configs: TFilterConfig<TWorkItemFilterProperty, TFilterValue>[];
|
||||
configMap: {
|
||||
[key in TWorkItemFilterProperty]?: TFilterConfig<TWorkItemFilterProperty, TFilterValue>;
|
||||
};
|
||||
isFilterEnabled: (key: TWorkItemFilterProperty) => boolean;
|
||||
members: IUserLite[];
|
||||
};
|
||||
|
||||
export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): TWorkItemFiltersConfig => {
|
||||
const { allowedFilters, cycleIds, labelIds, memberIds, moduleIds, projectId, projectIds, stateIds, workspaceSlug } =
|
||||
props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { loader: projectLoader, getProjectById } = useProject();
|
||||
const { getCycleById } = useCycle();
|
||||
const { getLabelById } = useLabel();
|
||||
const { getModuleById } = useModule();
|
||||
|
|
@ -128,6 +134,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
|||
: [],
|
||||
[projectIds, getProjectById]
|
||||
);
|
||||
const areAllConfigsInitialized = useMemo(() => isLoaderReady(projectLoader), [projectLoader]);
|
||||
|
||||
/**
|
||||
* Checks if a filter is enabled based on the filters to show.
|
||||
|
|
@ -317,6 +324,28 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
|||
[operatorConfigs]
|
||||
);
|
||||
|
||||
// created at filter config
|
||||
const createdAtFilterConfig = useMemo(
|
||||
() =>
|
||||
getCreatedAtFilterConfig<TWorkItemFilterProperty>("created_at")({
|
||||
isEnabled: true,
|
||||
filterIcon: Calendar,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[operatorConfigs]
|
||||
);
|
||||
|
||||
// updated at filter config
|
||||
const updatedAtFilterConfig = useMemo(
|
||||
() =>
|
||||
getUpdatedAtFilterConfig<TWorkItemFilterProperty>("updated_at")({
|
||||
isEnabled: true,
|
||||
filterIcon: Calendar,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[operatorConfigs]
|
||||
);
|
||||
|
||||
// project filter config
|
||||
const projectFilterConfig = useMemo(
|
||||
() =>
|
||||
|
|
@ -331,6 +360,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
|||
);
|
||||
|
||||
return {
|
||||
areAllConfigsInitialized,
|
||||
configs: [
|
||||
stateFilterConfig,
|
||||
stateGroupFilterConfig,
|
||||
|
|
@ -343,6 +373,8 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
|||
moduleFilterConfig,
|
||||
startDateFilterConfig,
|
||||
targetDateFilterConfig,
|
||||
createdAtFilterConfig,
|
||||
updatedAtFilterConfig,
|
||||
createdByFilterConfig,
|
||||
subscriberFilterConfig,
|
||||
],
|
||||
|
|
@ -360,7 +392,10 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
|||
priority: priorityFilterConfig,
|
||||
start_date: startDateFilterConfig,
|
||||
target_date: targetDateFilterConfig,
|
||||
created_at: createdAtFilterConfig,
|
||||
updated_at: updatedAtFilterConfig,
|
||||
},
|
||||
isFilterEnabled,
|
||||
members: members ?? [],
|
||||
};
|
||||
};
|
||||
|
|
|
|||
71
apps/web/core/components/rich-filters/add-filters/button.tsx
Normal file
71
apps/web/core/components/rich-filters/add-filters/button.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { LOGICAL_OPERATOR, TExternalFilter, TFilterProperty, TSupportedOperators } from "@plane/types";
|
||||
import { cn, getButtonStyling, TButtonVariant } from "@plane/ui";
|
||||
// local imports
|
||||
import { AddFilterDropdown } from "./dropdown";
|
||||
|
||||
export type TAddFilterButtonProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
buttonConfig?: {
|
||||
label: string | null;
|
||||
variant?: TButtonVariant;
|
||||
className?: string;
|
||||
defaultOpen?: boolean;
|
||||
iconConfig?: {
|
||||
shouldShowIcon: boolean;
|
||||
iconComponent?: React.ElementType;
|
||||
};
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
filter: IFilterInstance<P, E>;
|
||||
onFilterSelect?: (id: string) => void;
|
||||
};
|
||||
|
||||
export const AddFilterButton = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: TAddFilterButtonProps<P, E>) => {
|
||||
const { filter, buttonConfig, onFilterSelect } = props;
|
||||
const {
|
||||
variant = "link-neutral",
|
||||
className,
|
||||
label,
|
||||
iconConfig = { shouldShowIcon: true },
|
||||
isDisabled = false,
|
||||
} = buttonConfig || {};
|
||||
// derived values
|
||||
const FilterIcon = iconConfig.iconComponent || ListFilter;
|
||||
|
||||
const handleFilterSelect = (property: P, operator: TSupportedOperators, isNegation: boolean) => {
|
||||
filter.addCondition(
|
||||
LOGICAL_OPERATOR.AND,
|
||||
{
|
||||
property,
|
||||
operator,
|
||||
value: undefined,
|
||||
},
|
||||
isNegation
|
||||
);
|
||||
onFilterSelect?.(property);
|
||||
};
|
||||
|
||||
if (isDisabled) return null;
|
||||
return (
|
||||
<AddFilterDropdown
|
||||
{...props}
|
||||
buttonConfig={{
|
||||
...buttonConfig,
|
||||
className: cn(getButtonStyling(variant, "sm"), "py-[5px]", className),
|
||||
}}
|
||||
handleFilterSelect={handleFilterSelect}
|
||||
customButton={
|
||||
<div className="flex items-center gap-1">
|
||||
{iconConfig.shouldShowIcon && <FilterIcon className="size-4 text-custom-text-200" />}
|
||||
{label}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -1,53 +1,40 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
// plane imports
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { LOGICAL_OPERATOR, TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { CustomSearchSelect, TButtonVariant } from "@plane/ui";
|
||||
import { cn, getOperatorForPayload } from "@plane/utils";
|
||||
import { TExternalFilter, TFilterProperty, TSupportedOperators } from "@plane/types";
|
||||
import { CustomSearchSelect } from "@plane/ui";
|
||||
import { getOperatorForPayload } from "@plane/utils";
|
||||
|
||||
export type TAddFilterButtonProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
export type TAddFilterDropdownProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
customButton: React.ReactNode;
|
||||
buttonConfig?: {
|
||||
label: string | null;
|
||||
variant?: TButtonVariant;
|
||||
className?: string;
|
||||
defaultOpen?: boolean;
|
||||
iconConfig?: {
|
||||
shouldShowIcon: boolean;
|
||||
iconComponent?: React.ElementType;
|
||||
};
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
filter: IFilterInstance<P, E>;
|
||||
onFilterSelect?: (id: string) => void;
|
||||
handleFilterSelect: (property: P, operator: TSupportedOperators, isNegation: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddFilterButton = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: TAddFilterButtonProps<P, E>) => {
|
||||
const { filter, buttonConfig, onFilterSelect } = props;
|
||||
const {
|
||||
label,
|
||||
variant = "link-neutral",
|
||||
className,
|
||||
defaultOpen = false,
|
||||
iconConfig = { shouldShowIcon: true },
|
||||
isDisabled = false,
|
||||
} = buttonConfig || {};
|
||||
// derived values
|
||||
const FilterIcon = iconConfig.iconComponent || ListFilter;
|
||||
export const AddFilterDropdown = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: TAddFilterDropdownProps<P, E>) => {
|
||||
const { filter, customButton, buttonConfig } = props;
|
||||
const { className, defaultOpen = false, isDisabled = false } = buttonConfig || {};
|
||||
|
||||
// Transform available filter configs to CustomSearchSelect options format
|
||||
const filterOptions = filter.configManager.allAvailableConfigs.map((config) => ({
|
||||
value: config.id,
|
||||
content: (
|
||||
<div className="flex items-center gap-2 text-custom-text-200 transition-all duration-200 ease-in-out">
|
||||
{config.icon && (
|
||||
<config.icon className="size-4 text-custom-text-300 transition-transform duration-200 ease-in-out" />
|
||||
)}
|
||||
<span>{config.label}</span>
|
||||
<div className="flex items-center justify-between gap-2 text-custom-text-200 transition-all duration-200 ease-in-out">
|
||||
<div className="flex items-center gap-2">
|
||||
{config.icon && (
|
||||
<config.icon className="size-4 text-custom-text-300 transition-transform duration-200 ease-in-out" />
|
||||
)}
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
{config.rightContent}
|
||||
</div>
|
||||
),
|
||||
query: config.label.toLowerCase(),
|
||||
|
|
@ -70,16 +57,7 @@ export const AddFilterButton = observer(
|
|||
const config = filter.configManager.getConfigByProperty(property);
|
||||
if (config?.firstOperator) {
|
||||
const { operator, isNegation } = getOperatorForPayload(config.firstOperator);
|
||||
filter.addCondition(
|
||||
LOGICAL_OPERATOR.AND,
|
||||
{
|
||||
property: config.id,
|
||||
operator,
|
||||
value: undefined,
|
||||
},
|
||||
isNegation
|
||||
);
|
||||
onFilterSelect?.(property);
|
||||
props.handleFilterSelect(property, operator, isNegation);
|
||||
} else {
|
||||
setToast({
|
||||
title: "Filter configuration error",
|
||||
|
|
@ -89,7 +67,6 @@ export const AddFilterButton = observer(
|
|||
}
|
||||
};
|
||||
|
||||
if (isDisabled) return null;
|
||||
return (
|
||||
<div className="relative transition-all duration-200 ease-in-out">
|
||||
<CustomSearchSelect
|
||||
|
|
@ -98,16 +75,11 @@ export const AddFilterButton = observer(
|
|||
onChange={handleFilterSelect}
|
||||
options={displayOptions}
|
||||
optionsClassName="w-56"
|
||||
maxHeight="full"
|
||||
maxHeight="2xl"
|
||||
placement="bottom-start"
|
||||
disabled={isDisabled}
|
||||
customButtonClassName={cn(getButtonStyling(variant, "sm"), "py-[5px]", className)}
|
||||
customButton={
|
||||
<div className="flex items-center gap-1">
|
||||
{iconConfig.shouldShowIcon && <FilterIcon className="size-4 text-custom-text-200" />}
|
||||
{label}
|
||||
</div>
|
||||
}
|
||||
customButtonClassName={className}
|
||||
customButton={customButton}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
|
||||
interface FilterItemCloseButtonProps<P extends TFilterProperty, E extends TExternalFilter> {
|
||||
conditionId: string;
|
||||
filter: IFilterInstance<P, E>;
|
||||
}
|
||||
|
||||
export const FilterItemCloseButton = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: FilterItemCloseButtonProps<P, E>) => {
|
||||
const { conditionId, filter } = props;
|
||||
|
||||
const handleRemoveFilter = () => {
|
||||
filter.removeCondition(conditionId);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleRemoveFilter}
|
||||
className="px-1.5 text-custom-text-400 hover:text-custom-text-300 focus:outline-none hover:bg-custom-background-90"
|
||||
type="button"
|
||||
aria-label="Remove filter"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/propel/utils";
|
||||
import { SingleOrArray, TFilterValue } from "@plane/types";
|
||||
import { hasValidValue } from "@plane/utils";
|
||||
|
||||
interface FilterItemContainerProps {
|
||||
children: React.ReactNode;
|
||||
conditionValue: SingleOrArray<TFilterValue>;
|
||||
showTransition: boolean;
|
||||
variant?: "default" | "error";
|
||||
tooltipContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FilterItemContainer: React.FC<FilterItemContainerProps> = (props) => {
|
||||
const { children, conditionValue, showTransition, variant = "default", tooltipContent } = props;
|
||||
// refs
|
||||
const itemRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// effects
|
||||
useEffect(() => {
|
||||
if (!showTransition) return;
|
||||
|
||||
const element = itemRef.current;
|
||||
if (!element) return;
|
||||
|
||||
if (hasValidValue(conditionValue)) return;
|
||||
|
||||
const applyInitialStyles = () => {
|
||||
element.style.opacity = "0";
|
||||
element.style.transform = "scale(0.95)";
|
||||
};
|
||||
|
||||
const applyFinalStyles = () => {
|
||||
// Force a reflow to ensure the initial state is applied
|
||||
void element.offsetWidth;
|
||||
element.style.opacity = "1";
|
||||
element.style.transform = "scale(1)";
|
||||
};
|
||||
|
||||
applyInitialStyles();
|
||||
applyFinalStyles();
|
||||
|
||||
return () => {
|
||||
applyInitialStyles();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tooltip tooltipContent={tooltipContent} position="bottom" disabled={!tooltipContent}>
|
||||
<div
|
||||
ref={itemRef}
|
||||
className={cn("flex h-7 items-stretch rounded overflow-hidden border transition-all duration-200", {
|
||||
"border-custom-border-200 bg-custom-background-100": variant === "default",
|
||||
"border-red-500 bg-custom-background-90": variant === "error",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
// plane imports
|
||||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
// local imports
|
||||
import { FilterItemCloseButton } from "./close-button";
|
||||
import { FilterItemContainer } from "./container";
|
||||
import { FilterItemProperty } from "./property";
|
||||
import { IFilterItemProps } from "./root";
|
||||
|
||||
export const InvalidFilterItem = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: IFilterItemProps<P, E>) => {
|
||||
const { condition, filter, isDisabled = false, showTransition = true } = props;
|
||||
|
||||
return (
|
||||
<FilterItemContainer
|
||||
conditionValue={condition.value}
|
||||
showTransition={showTransition}
|
||||
variant="error"
|
||||
tooltipContent="This filter condition is no longer valid. The property may have been deleted or your access to it may have changed."
|
||||
>
|
||||
{/* Property section */}
|
||||
<FilterItemProperty
|
||||
conditionId={condition.id}
|
||||
icon={CircleAlert}
|
||||
label="Invalid filter"
|
||||
filter={filter}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
{/* Remove button */}
|
||||
{!isDisabled && <FilterItemCloseButton conditionId={condition.id} filter={filter} />}
|
||||
</FilterItemContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { Loader } from "@plane/ui";
|
||||
|
||||
export const FilterItemLoader = () => (
|
||||
<Loader>
|
||||
<Loader.Item height="28px" width="180px" />
|
||||
</Loader>
|
||||
);
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/propel/utils";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { TExternalFilter, TFilterProperty, TSupportedOperators } from "@plane/types";
|
||||
// local imports
|
||||
import { AddFilterDropdown } from "../add-filters/dropdown";
|
||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../shared";
|
||||
|
||||
interface IFilterItemPropertyProps<P extends TFilterProperty, E extends TExternalFilter> {
|
||||
conditionId: string;
|
||||
icon: React.FC<React.SVGAttributes<SVGElement>> | undefined;
|
||||
isDisabled?: boolean;
|
||||
filter: IFilterInstance<P, E>;
|
||||
label: string;
|
||||
tooltipContent?: React.ReactNode | undefined;
|
||||
}
|
||||
|
||||
export const FilterItemProperty = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: IFilterItemPropertyProps<P, E>) => {
|
||||
const { conditionId, filter, isDisabled } = props;
|
||||
|
||||
if (isDisabled) {
|
||||
return <PropertyButton {...props} />;
|
||||
}
|
||||
|
||||
const handleFilterSelect = (property: P, operator: TSupportedOperators, isNegation: boolean) => {
|
||||
filter.updateConditionProperty(conditionId, property, operator, isNegation);
|
||||
};
|
||||
|
||||
return (
|
||||
<AddFilterDropdown
|
||||
{...props}
|
||||
handleFilterSelect={handleFilterSelect}
|
||||
customButton={<PropertyButton {...props} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TPropertyButtonProps<P extends TFilterProperty, E extends TExternalFilter> = IFilterItemPropertyProps<P, E> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const PropertyButton = <P extends TFilterProperty, E extends TExternalFilter>(props: TPropertyButtonProps<P, E>) => {
|
||||
const { icon: Icon, label, tooltipContent, className } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipContent={tooltipContent} position="bottom-start" disabled={!tooltipContent}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-[5px] text-xs text-custom-text-300 min-w-0 h-full",
|
||||
COMMON_FILTER_ITEM_BORDER_CLASSNAME,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon && (
|
||||
<div className="transition-transform duration-200 ease-in-out flex-shrink-0">
|
||||
<Icon className="size-3.5" />
|
||||
</div>
|
||||
)}
|
||||
<span className="truncate">{label}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useRef, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import {
|
||||
|
|
@ -12,12 +11,17 @@ import {
|
|||
TAllAvailableOperatorsForDisplay,
|
||||
} from "@plane/types";
|
||||
import { CustomSearchSelect } from "@plane/ui";
|
||||
import { cn, hasValidValue, getOperatorForPayload } from "@plane/utils";
|
||||
import { cn, getOperatorForPayload } from "@plane/utils";
|
||||
// local imports
|
||||
import { FilterValueInput } from "./filter-value-input/root";
|
||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "./shared";
|
||||
import { FilterValueInput } from "../filter-value-input/root";
|
||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../shared";
|
||||
import { FilterItemCloseButton } from "./close-button";
|
||||
import { FilterItemContainer } from "./container";
|
||||
import { InvalidFilterItem } from "./invalid";
|
||||
import { FilterItemLoader } from "./loader";
|
||||
import { FilterItemProperty } from "./property";
|
||||
|
||||
interface FilterItemProps<P extends TFilterProperty, E extends TExternalFilter> {
|
||||
export interface IFilterItemProps<P extends TFilterProperty, E extends TExternalFilter> {
|
||||
condition: TFilterConditionNodeForDisplay<P, TFilterValue>;
|
||||
filter: IFilterInstance<P, E>;
|
||||
isDisabled?: boolean;
|
||||
|
|
@ -25,10 +29,8 @@ interface FilterItemProps<P extends TFilterProperty, E extends TExternalFilter>
|
|||
}
|
||||
|
||||
export const FilterItem = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: FilterItemProps<P, E>) => {
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: IFilterItemProps<P, E>) => {
|
||||
const { condition, filter, isDisabled = false, showTransition = true } = props;
|
||||
// refs
|
||||
const itemRef = useRef<HTMLDivElement>(null);
|
||||
// derived values
|
||||
const filterConfig = condition?.property ? filter.configManager.getConfigByProperty(condition.property) : undefined;
|
||||
const operatorOptions = filterConfig
|
||||
|
|
@ -48,36 +50,6 @@ export const FilterItem = observer(
|
|||
isDisabled ||
|
||||
(condition.operator && operatorOptions?.length === 1 && operatorOptions[0]?.value === condition.operator);
|
||||
|
||||
// effects
|
||||
useEffect(() => {
|
||||
if (!showTransition) return;
|
||||
|
||||
const element = itemRef.current;
|
||||
if (!element) return;
|
||||
|
||||
if (hasValidValue(condition.value)) return;
|
||||
|
||||
const applyInitialStyles = () => {
|
||||
element.style.opacity = "0";
|
||||
element.style.transform = "scale(0.95)";
|
||||
};
|
||||
|
||||
const applyFinalStyles = () => {
|
||||
// Force a reflow to ensure the initial state is applied
|
||||
void element.offsetWidth;
|
||||
element.style.opacity = "1";
|
||||
element.style.transform = "scale(1)";
|
||||
};
|
||||
|
||||
applyInitialStyles();
|
||||
applyFinalStyles();
|
||||
|
||||
return () => {
|
||||
applyInitialStyles();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleOperatorChange = (operator: TAllAvailableOperatorsForDisplay) => {
|
||||
if (operator) {
|
||||
const { operator: positiveOperator, isNegation } = getOperatorForPayload(operator);
|
||||
|
|
@ -89,30 +61,32 @@ export const FilterItem = observer(
|
|||
filter.updateConditionValue(condition.id, values);
|
||||
};
|
||||
|
||||
const handleRemoveFilter = () => {
|
||||
filter.removeCondition(condition.id);
|
||||
};
|
||||
if (!filter.configManager.areConfigsReady) {
|
||||
return <FilterItemLoader />;
|
||||
}
|
||||
|
||||
if (!filterConfig) {
|
||||
return (
|
||||
<InvalidFilterItem
|
||||
condition={condition}
|
||||
filter={filter}
|
||||
isDisabled={isDisabled}
|
||||
showTransition={showTransition}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterConfig || !filterConfig.isEnabled) return null;
|
||||
return (
|
||||
<div
|
||||
ref={itemRef}
|
||||
className="flex h-7 items-stretch rounded overflow-hidden border border-custom-border-200 bg-custom-background-100 transition-all duration-200"
|
||||
>
|
||||
<FilterItemContainer conditionValue={condition.value} showTransition={showTransition}>
|
||||
{/* Property section */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-0.5 text-xs text-custom-text-300 min-w-0",
|
||||
COMMON_FILTER_ITEM_BORDER_CLASSNAME
|
||||
)}
|
||||
>
|
||||
{filterConfig.icon && (
|
||||
<div className="transition-transform duration-200 ease-in-out flex-shrink-0">
|
||||
<filterConfig.icon className="size-3.5" />
|
||||
</div>
|
||||
)}
|
||||
<span className="truncate">{filterConfig.label}</span>
|
||||
</div>
|
||||
<FilterItemProperty
|
||||
conditionId={condition.id}
|
||||
filter={filter}
|
||||
icon={filterConfig.icon}
|
||||
isDisabled={isDisabled}
|
||||
label={filterConfig.label}
|
||||
tooltipContent={filterConfig.tooltipContent}
|
||||
/>
|
||||
|
||||
{/* Operator section */}
|
||||
<CustomSearchSelect
|
||||
|
|
@ -125,7 +99,7 @@ export const FilterItem = observer(
|
|||
isOperatorSelectionDisabled && "hover:bg-custom-background-100"
|
||||
)}
|
||||
optionsClassName="w-48"
|
||||
maxHeight="full"
|
||||
maxHeight="2xl"
|
||||
disabled={isOperatorSelectionDisabled}
|
||||
customButton={
|
||||
<div className="flex items-center h-full" aria-disabled={isOperatorSelectionDisabled}>
|
||||
|
|
@ -145,17 +119,8 @@ export const FilterItem = observer(
|
|||
)}
|
||||
|
||||
{/* Remove button */}
|
||||
{!isDisabled && (
|
||||
<button
|
||||
onClick={handleRemoveFilter}
|
||||
className="px-1.5 text-custom-text-400 hover:text-custom-text-300 focus:outline-none hover:bg-custom-background-90"
|
||||
type="button"
|
||||
aria-label="Remove filter"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!isDisabled && <FilterItemCloseButton conditionId={condition.id} filter={filter} />}
|
||||
</FilterItemContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -6,7 +6,7 @@ import { cn, isValidDate, renderFormattedPayloadDate, toFilterArray } from "@pla
|
|||
// components
|
||||
import { DateRangeDropdown } from "@/components/dropdowns/date-range";
|
||||
// local imports
|
||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../../shared";
|
||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME, EMPTY_FILTER_PLACEHOLDER_TEXT } from "../../shared";
|
||||
|
||||
type TDateRangeFilterValueInputProps<P extends TFilterProperty> = {
|
||||
config: TDateRangeFilterFieldConfig<string>;
|
||||
|
|
@ -42,7 +42,7 @@ export const DateRangeFilterValueInput = observer(
|
|||
minDate={config.min}
|
||||
maxDate={config.max}
|
||||
mergeDates
|
||||
placeholder={{ from: "--" }}
|
||||
placeholder={{ from: EMPTY_FILTER_PLACEHOLDER_TEXT }}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName={cn("rounded-none", {
|
||||
[COMMON_FILTER_ITEM_BORDER_CLASSNAME]: !isDisabled,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
|||
import { TDateFilterFieldConfig, TFilterConditionNodeForDisplay, TFilterProperty } from "@plane/types";
|
||||
import { cn, renderFormattedPayloadDate } from "@plane/utils";
|
||||
import { DateDropdown } from "@/components/dropdowns/date";
|
||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME } from "../../shared";
|
||||
import { COMMON_FILTER_ITEM_BORDER_CLASSNAME, EMPTY_FILTER_PLACEHOLDER_TEXT } from "../../shared";
|
||||
|
||||
type TSingleDateFilterValueInputProps<P extends TFilterProperty> = {
|
||||
config: TDateFilterFieldConfig<string>;
|
||||
|
|
@ -34,7 +34,7 @@ export const SingleDateFilterValueInput = observer(
|
|||
minDate={config.min}
|
||||
maxDate={config.max}
|
||||
icon={null}
|
||||
placeholder="--"
|
||||
placeholder={EMPTY_FILTER_PLACEHOLDER_TEXT}
|
||||
buttonVariant="transparent-with-text"
|
||||
isClearable={false}
|
||||
closeOnSelect
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import {
|
||||
|
|
@ -11,23 +12,16 @@ import {
|
|||
TMultiSelectFilterFieldConfig,
|
||||
TDateFilterFieldConfig,
|
||||
TDateRangeFilterFieldConfig,
|
||||
TSupportedFilterFieldConfigs,
|
||||
TFilterConditionNodeForDisplay,
|
||||
} from "@plane/types";
|
||||
// local imports
|
||||
import { AdditionalFilterValueInput } from "@/plane-web/components/rich-filters/filter-value-input/root";
|
||||
import { TFilterValueInputProps } from "../shared";
|
||||
import { DateRangeFilterValueInput } from "./date/range";
|
||||
import { SingleDateFilterValueInput } from "./date/single";
|
||||
import { MultiSelectFilterValueInput } from "./select/multi";
|
||||
import { SingleSelectFilterValueInput } from "./select/single";
|
||||
|
||||
type TFilterValueInputProps<P extends TFilterProperty, V extends TFilterValue> = {
|
||||
condition: TFilterConditionNodeForDisplay<P, V>;
|
||||
filterFieldConfig: TSupportedFilterFieldConfigs<V>;
|
||||
isDisabled?: boolean;
|
||||
onChange: (values: SingleOrArray<V>) => void;
|
||||
};
|
||||
|
||||
// TODO: Prevent type assertion
|
||||
export const FilterValueInput = observer(
|
||||
<P extends TFilterProperty, V extends TFilterValue>(props: TFilterValueInputProps<P, V>) => {
|
||||
const { condition, filterFieldConfig, isDisabled = false, onChange } = props;
|
||||
|
|
@ -80,11 +74,6 @@ export const FilterValueInput = observer(
|
|||
);
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return (
|
||||
<div className="h-full flex items-center px-4 text-xs text-custom-text-400 transition-opacity duration-200 cursor-not-allowed">
|
||||
Filter type not supported
|
||||
</div>
|
||||
);
|
||||
return <AdditionalFilterValueInput {...props} />;
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Transition } from "@headlessui/react";
|
|||
// plane imports
|
||||
import { SingleOrArray, IFilterOption, TFilterValue } from "@plane/types";
|
||||
import { cn, toFilterArray } from "@plane/utils";
|
||||
import { EMPTY_FILTER_PLACEHOLDER_TEXT } from "../../shared";
|
||||
|
||||
type TSelectedOptionsDisplayProps<V extends TFilterValue> = {
|
||||
selectedValue: SingleOrArray<V>;
|
||||
|
|
@ -13,7 +14,7 @@ type TSelectedOptionsDisplayProps<V extends TFilterValue> = {
|
|||
};
|
||||
|
||||
export const SelectedOptionsDisplay = <V extends TFilterValue>(props: TSelectedOptionsDisplayProps<V>) => {
|
||||
const { selectedValue, options, displayCount = 2, emptyValue = "--", fallbackText } = props;
|
||||
const { selectedValue, options, displayCount = 2, emptyValue = EMPTY_FILTER_PLACEHOLDER_TEXT, fallbackText } = props;
|
||||
// derived values
|
||||
const selectedArray = toFilterArray(selectedValue);
|
||||
const remainingCount = selectedArray.length - displayCount;
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import { Transition } from "@headlessui/react";
|
|||
import { Button } from "@plane/propel/button";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { cn, EHeaderVariant, Header } from "@plane/ui";
|
||||
import { cn, EHeaderVariant, Header, Loader } from "@plane/ui";
|
||||
// local imports
|
||||
import { AddFilterButton, TAddFilterButtonProps } from "./add-filters-button";
|
||||
import { FilterItem } from "./filter-item";
|
||||
import { AddFilterButton, TAddFilterButtonProps } from "./add-filters/button";
|
||||
import { FilterItem } from "./filter-item/root";
|
||||
|
||||
export type TFiltersRowProps<K extends TFilterProperty, E extends TExternalFilter> = {
|
||||
buttonConfig?: TAddFilterButtonProps<K, E>["buttonConfig"];
|
||||
|
|
@ -25,10 +25,17 @@ export type TFiltersRowProps<K extends TFilterProperty, E extends TExternalFilte
|
|||
|
||||
export const FiltersRow = observer(
|
||||
<K extends TFilterProperty, E extends TExternalFilter>(props: TFiltersRowProps<K, E>) => {
|
||||
const { buttonConfig, disabledAllOperations = false, filter, variant = "header", trackerElements } = props;
|
||||
const {
|
||||
buttonConfig,
|
||||
disabledAllOperations: disabledAllOperationsProp = false,
|
||||
filter,
|
||||
variant = "header",
|
||||
trackerElements,
|
||||
} = props;
|
||||
// states
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
// derived values
|
||||
const disabledAllOperations = disabledAllOperationsProp || !filter.configManager.areConfigsReady;
|
||||
const hasAnyConditions = filter.allConditionsForDisplay.length > 0;
|
||||
const hasAvailableOperations =
|
||||
!disabledAllOperations && (filter.canClearFilters || filter.canSaveView || filter.canUpdateView);
|
||||
|
|
@ -140,19 +147,17 @@ export const FiltersRow = observer(
|
|||
</Header>
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
show={filter.isVisible}
|
||||
enter="transition-all duration-150 ease-out"
|
||||
enterFrom="opacity-0 -translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition-all duration-100 ease-in"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-1"
|
||||
>
|
||||
{variant === "modal" ? ModalVariant : HeaderVariant}
|
||||
</Transition>
|
||||
);
|
||||
if (!filter.configManager.areConfigsReady && !hasAnyConditions) {
|
||||
return (
|
||||
<RowTransition show={filter.isVisible}>
|
||||
<Loader>
|
||||
<Loader.Item height="44px" width="100%" className={cn({ "rounded-none": variant === "header" })} />
|
||||
</Loader>
|
||||
</RowTransition>
|
||||
);
|
||||
}
|
||||
|
||||
return <RowTransition show={filter.isVisible}>{variant === "modal" ? ModalVariant : HeaderVariant}</RowTransition>;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -176,3 +181,22 @@ const ElementTransition = observer((props: TElementTransitionProps) => (
|
|||
{props.children}
|
||||
</Transition>
|
||||
));
|
||||
|
||||
type TRowTransitionProps = {
|
||||
children: React.ReactNode;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
const RowTransition = observer((props: TRowTransitionProps) => (
|
||||
<Transition
|
||||
show={props.show}
|
||||
enter="transition-all duration-150 ease-out"
|
||||
enterFrom="opacity-0 -translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition-all duration-100 ease-in"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-1"
|
||||
>
|
||||
{props.children}
|
||||
</Transition>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { IFilterInstance } from "@plane/shared-state";
|
|||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { cn } from "@plane/ui";
|
||||
// components
|
||||
import { AddFilterButton } from "@/components/rich-filters/add-filters-button";
|
||||
import { AddFilterButton } from "@/components/rich-filters/add-filters/button";
|
||||
|
||||
type TFiltersToggleProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
filter: IFilterInstance<P, E> | undefined;
|
||||
|
|
|
|||
|
|
@ -1 +1,18 @@
|
|||
import {
|
||||
SingleOrArray,
|
||||
TFilterConditionNodeForDisplay,
|
||||
TFilterProperty,
|
||||
TFilterValue,
|
||||
TSupportedFilterFieldConfigs,
|
||||
} from "@plane/types";
|
||||
|
||||
export const COMMON_FILTER_ITEM_BORDER_CLASSNAME = "border-r border-custom-border-200";
|
||||
|
||||
export const EMPTY_FILTER_PLACEHOLDER_TEXT = "--";
|
||||
|
||||
export type TFilterValueInputProps<P extends TFilterProperty, V extends TFilterValue> = {
|
||||
condition: TFilterConditionNodeForDisplay<P, V>;
|
||||
filterFieldConfig: TSupportedFilterFieldConfigs<V>;
|
||||
isDisabled?: boolean;
|
||||
onChange: (values: SingleOrArray<V>) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
|||
[deleteFilter, entityType, workItemEntityID]
|
||||
);
|
||||
|
||||
workItemLayoutFilter.configManager.setAreConfigsReady(workItemFiltersConfig.areAllConfigsInitialized);
|
||||
workItemLayoutFilter.configManager.registerAll(workItemFiltersConfig.configs);
|
||||
|
||||
return <>{typeof children === "function" ? children({ filter: workItemLayoutFilter }) : children}</>;
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export const ProjectLevelWorkItemFiltersHOC = observer((props: TProjectLevelWork
|
|||
<WorkItemFiltersHOC
|
||||
{...props}
|
||||
{...getAdditionalProjectLevelFiltersHOCProps({
|
||||
entityType: props.entityType,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface IFilterConfigManager<P extends TFilterProperty> {
|
|||
// observables
|
||||
filterConfigs: Map<P, IFilterConfig<P, TFilterValue>>; // filter property -> config
|
||||
configOptions: TConfigOptions;
|
||||
areConfigsReady: boolean;
|
||||
// computed
|
||||
allAvailableConfigs: IFilterConfig<P, TFilterValue>[];
|
||||
// computed functions
|
||||
|
|
@ -32,6 +33,7 @@ export interface IFilterConfigManager<P extends TFilterProperty> {
|
|||
register: <C extends TFilterConfig<P, TFilterValue>>(config: C) => void;
|
||||
registerAll: (configs: TFilterConfig<P, TFilterValue>[]) => void;
|
||||
updateConfigByProperty: (property: P, configUpdates: Partial<TFilterConfig<P, TFilterValue>>) => void;
|
||||
setAreConfigsReady: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -57,6 +59,7 @@ export class FilterConfigManager<P extends TFilterProperty, E extends TExternalF
|
|||
// observables
|
||||
filterConfigs: IFilterConfigManager<P>["filterConfigs"];
|
||||
configOptions: IFilterConfigManager<P>["configOptions"];
|
||||
areConfigsReady: IFilterConfigManager<P>["areConfigsReady"];
|
||||
// parent filter instance
|
||||
private _filterInstance: IFilterInstance<P, E>;
|
||||
|
||||
|
|
@ -69,18 +72,21 @@ export class FilterConfigManager<P extends TFilterProperty, E extends TExternalF
|
|||
constructor(filterInstance: IFilterInstance<P, E>, params: TConfigManagerParams) {
|
||||
this.filterConfigs = new Map<P, IFilterConfig<P>>();
|
||||
this.configOptions = this._initializeConfigOptions(params.options);
|
||||
this.areConfigsReady = true;
|
||||
// parent filter instance
|
||||
this._filterInstance = filterInstance;
|
||||
|
||||
makeObservable(this, {
|
||||
filterConfigs: observable,
|
||||
configOptions: observable,
|
||||
areConfigsReady: observable,
|
||||
// computed
|
||||
allAvailableConfigs: computed,
|
||||
// helpers
|
||||
register: action,
|
||||
registerAll: action,
|
||||
updateConfigByProperty: action,
|
||||
setAreConfigsReady: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +152,14 @@ export class FilterConfigManager<P extends TFilterProperty, E extends TExternalF
|
|||
prevConfig?.mutate(configUpdates);
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the configs ready state.
|
||||
* @param value - The new configs ready state.
|
||||
*/
|
||||
setAreConfigsReady: IFilterConfigManager<P>["setAreConfigsReady"] = action((value) => {
|
||||
this.areConfigsReady = value;
|
||||
});
|
||||
|
||||
// ------------ private computed ------------
|
||||
|
||||
private get _allConfigs(): IFilterConfig<P, TFilterValue>[] {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
IFilterAdapter,
|
||||
LOGICAL_OPERATOR,
|
||||
TSupportedOperators,
|
||||
TFilterConditionNode,
|
||||
TFilterExpression,
|
||||
TFilterValue,
|
||||
TFilterProperty,
|
||||
|
|
@ -43,9 +44,16 @@ export interface IFilterInstanceHelper<P extends TFilterProperty, E extends TExt
|
|||
condition: TFilterConditionPayload<P, V>,
|
||||
isNegation: boolean
|
||||
) => TFilterExpression<P> | null;
|
||||
handleConditionPropertyUpdate: (
|
||||
expression: TFilterExpression<P>,
|
||||
conditionId: string,
|
||||
property: P,
|
||||
operator: TSupportedOperators,
|
||||
isNegation: boolean
|
||||
) => TFilterExpression<P> | null;
|
||||
// group operations
|
||||
restructureExpressionForOperatorChange: (
|
||||
expression: TFilterExpression<P> | null,
|
||||
expression: TFilterExpression<P>,
|
||||
conditionId: string,
|
||||
newOperator: TSupportedOperators,
|
||||
isNegation: boolean,
|
||||
|
|
@ -162,6 +170,28 @@ export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternal
|
|||
isNegation
|
||||
) => this._addConditionByOperator(expression, groupOperator, this._getConditionPayloadToAdd(condition, isNegation));
|
||||
|
||||
/**
|
||||
* Updates the property and operator of a condition in the filter expression.
|
||||
* This method updates the property, operator, resets the value, and handles negation properly.
|
||||
* @param expression - The filter expression to operate on
|
||||
* @param conditionId - The ID of the condition being updated
|
||||
* @param property - The new property for the condition
|
||||
* @param operator - The new operator for the condition
|
||||
* @param isNegation - Whether the condition should be negated
|
||||
* @returns The updated expression
|
||||
*/
|
||||
handleConditionPropertyUpdate: IFilterInstanceHelper<P, E>["handleConditionPropertyUpdate"] = (
|
||||
expression,
|
||||
conditionId,
|
||||
property,
|
||||
operator,
|
||||
isNegation
|
||||
) => {
|
||||
const payload = { property, operator, value: undefined };
|
||||
|
||||
return this._updateCondition(expression, conditionId, payload, isNegation);
|
||||
};
|
||||
|
||||
// ------------ group operations ------------
|
||||
|
||||
/**
|
||||
|
|
@ -177,17 +207,12 @@ export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternal
|
|||
expression,
|
||||
conditionId,
|
||||
newOperator,
|
||||
_isNegation,
|
||||
isNegation,
|
||||
shouldResetValue
|
||||
) => {
|
||||
if (!expression) return null;
|
||||
|
||||
const payload = shouldResetValue ? { operator: newOperator, value: undefined } : { operator: newOperator };
|
||||
|
||||
// Update the condition with the new operator
|
||||
updateNodeInExpression(expression, conditionId, payload);
|
||||
|
||||
return expression;
|
||||
return this._updateCondition(expression, conditionId, payload, isNegation);
|
||||
};
|
||||
|
||||
// ------------ private helpers ------------
|
||||
|
|
@ -227,4 +252,24 @@ export class FilterInstanceHelper<P extends TFilterProperty, E extends TExternal
|
|||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a condition with the given payload and handles negation wrapping/unwrapping.
|
||||
* @param expression - The filter expression to operate on
|
||||
* @param conditionId - The ID of the condition being updated
|
||||
* @param payload - The payload to update the condition with
|
||||
* @param isNegation - Whether the condition should be negated
|
||||
* @returns The updated expression with proper negation handling
|
||||
*/
|
||||
private _updateCondition = (
|
||||
expression: TFilterExpression<P>,
|
||||
conditionId: string,
|
||||
payload: Partial<TFilterConditionNode<P, TFilterValue>>,
|
||||
_isNegation: boolean
|
||||
): TFilterExpression<P> | null => {
|
||||
// Update the condition with the payload
|
||||
updateNodeInExpression(expression, conditionId, payload);
|
||||
|
||||
return expression;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cloneDeep } from "lodash-es";
|
||||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, toJS } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
|
@ -101,6 +101,12 @@ export interface IFilterInstance<P extends TFilterProperty, E extends TExternalF
|
|||
condition: TFilterConditionPayload<P, V>,
|
||||
isNegation: boolean
|
||||
) => void;
|
||||
updateConditionProperty: (
|
||||
conditionId: string,
|
||||
property: P,
|
||||
operator: TSupportedOperators,
|
||||
isNegation: boolean
|
||||
) => void;
|
||||
updateConditionOperator: (conditionId: string, operator: TSupportedOperators, isNegation: boolean) => void;
|
||||
updateConditionValue: <V extends TFilterValue>(conditionId: string, value: SingleOrArray<V>) => void;
|
||||
removeCondition: (conditionId: string) => void;
|
||||
|
|
@ -360,6 +366,33 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the property of a condition in the filter expression.
|
||||
* @param conditionId - The id of the condition to update.
|
||||
* @param property - The new property for the condition.
|
||||
*/
|
||||
updateConditionProperty: IFilterInstance<P, E>["updateConditionProperty"] = action(
|
||||
(conditionId: string, property: P, operator: TSupportedOperators, isNegation: boolean) => {
|
||||
if (!this.expression) return;
|
||||
const conditionBeforeUpdate = cloneDeep(findNodeById(this.expression, conditionId));
|
||||
if (!conditionBeforeUpdate || conditionBeforeUpdate.type !== FILTER_NODE_TYPE.CONDITION) return;
|
||||
|
||||
// Update the condition property
|
||||
const updatedExpression = this.helper.handleConditionPropertyUpdate(
|
||||
this.expression,
|
||||
conditionId,
|
||||
property,
|
||||
operator,
|
||||
isNegation
|
||||
);
|
||||
|
||||
if (updatedExpression) {
|
||||
this.expression = updatedExpression;
|
||||
this._notifyExpressionChange();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the operator of a condition in the filter expression.
|
||||
* @param conditionId - The id of the condition to update.
|
||||
|
|
@ -410,12 +443,23 @@ export class FilterInstance<P extends TFilterProperty, E extends TExternalFilter
|
|||
// If the expression is not valid, return
|
||||
if (!this.expression) return;
|
||||
|
||||
// Get the condition before update
|
||||
const conditionBeforeUpdate = cloneDeep(findNodeById(this.expression, conditionId));
|
||||
|
||||
// If the condition is not valid, return
|
||||
if (!conditionBeforeUpdate || conditionBeforeUpdate.type !== FILTER_NODE_TYPE.CONDITION) return;
|
||||
|
||||
// If the value is not valid, remove the condition
|
||||
if (!hasValidValue(value)) {
|
||||
this.removeCondition(conditionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the value is the same as the condition before update, return
|
||||
if (isEqual(conditionBeforeUpdate.value, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the condition value
|
||||
updateNodeInExpression(this.expression, conditionId, {
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { isEmpty } from "lodash-es";
|
||||
import {
|
||||
LOGICAL_OPERATOR,
|
||||
MULTI_VALUE_OPERATORS,
|
||||
SingleOrArray,
|
||||
TFilterExpression,
|
||||
TFilterValue,
|
||||
|
|
@ -161,7 +162,8 @@ class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWor
|
|||
const operator = key.substring(lastDoubleUnderscoreIndex + 2);
|
||||
|
||||
// Validate property is in allowed list
|
||||
if (!WORK_ITEM_FILTER_PROPERTY_KEYS.includes(property as TWorkItemFilterProperty)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!WORK_ITEM_FILTER_PROPERTY_KEYS.includes(property as any) && !property.startsWith("customproperty_")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -192,17 +194,12 @@ class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWor
|
|||
// Find the last occurrence of '__' to separate property from operator
|
||||
const lastDoubleUnderscoreIndex = key.lastIndexOf("__");
|
||||
const property = key.substring(0, lastDoubleUnderscoreIndex);
|
||||
const operator = key.substring(lastDoubleUnderscoreIndex + 2);
|
||||
const operator = key.substring(lastDoubleUnderscoreIndex + 2) as TSupportedOperators;
|
||||
|
||||
const rawValue = data[key as TWorkItemFilterConditionKey];
|
||||
|
||||
if (typeof rawValue !== "string") {
|
||||
console.error(`Filter value must be a string, got: ${typeof rawValue}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse comma-separated values
|
||||
const parsedValue = this._parseFilterValue(rawValue);
|
||||
const parsedValue = MULTI_VALUE_OPERATORS.includes(operator) ? this._parseFilterValue(rawValue) : rawValue;
|
||||
|
||||
return [property as TWorkItemFilterProperty, operator as TSupportedOperators, parsedValue];
|
||||
};
|
||||
|
|
@ -212,7 +209,9 @@ class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWor
|
|||
* @param value - The string value to parse
|
||||
* @returns Parsed value as string or array of strings
|
||||
*/
|
||||
private _parseFilterValue = (value: string): SingleOrArray<TFilterValue> => {
|
||||
private _parseFilterValue = (value: TFilterValue): SingleOrArray<TFilterValue> => {
|
||||
if (!value) return value;
|
||||
|
||||
if (typeof value !== "string") return value;
|
||||
|
||||
// Handle empty string
|
||||
|
|
|
|||
|
|
@ -15,4 +15,6 @@ export type TFilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
|
|||
isEnabled: boolean;
|
||||
allowMultipleFilters?: boolean;
|
||||
supportedOperatorConfigsMap: TOperatorConfigMap<V>;
|
||||
rightContent?: React.ReactNode; // content to display on the right side of the filter option in the dropdown
|
||||
tooltipContent?: React.ReactNode; // content to display when hovering over the applied filter item in the filter list
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ export const CORE_COMPARISON_OPERATOR = {
|
|||
RANGE: "range",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Core operators that support multiple values
|
||||
*/
|
||||
export const CORE_MULTI_VALUE_OPERATORS = [CORE_COLLECTION_OPERATOR.IN, CORE_COMPARISON_OPERATOR.RANGE] as const;
|
||||
|
||||
/**
|
||||
* All core operators
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ export const EXTENDED_COLLECTION_OPERATOR = {} as const;
|
|||
*/
|
||||
export const EXTENDED_COMPARISON_OPERATOR = {} as const;
|
||||
|
||||
/**
|
||||
* Extended operators that support multiple values
|
||||
*/
|
||||
export const EXTENDED_MULTI_VALUE_OPERATORS = [] as const;
|
||||
|
||||
/**
|
||||
* All extended operators
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
CORE_COLLECTION_OPERATOR,
|
||||
CORE_COMPARISON_OPERATOR,
|
||||
TCoreSupportedOperators,
|
||||
CORE_MULTI_VALUE_OPERATORS,
|
||||
} from "./core";
|
||||
import {
|
||||
EXTENDED_LOGICAL_OPERATOR,
|
||||
|
|
@ -11,6 +12,7 @@ import {
|
|||
EXTENDED_COLLECTION_OPERATOR,
|
||||
EXTENDED_COMPARISON_OPERATOR,
|
||||
TExtendedSupportedOperators,
|
||||
EXTENDED_MULTI_VALUE_OPERATORS,
|
||||
} from "./extended";
|
||||
|
||||
// -------- COMPOSED OPERATORS --------
|
||||
|
|
@ -35,6 +37,11 @@ export const COMPARISON_OPERATOR = {
|
|||
...EXTENDED_COMPARISON_OPERATOR,
|
||||
} as const;
|
||||
|
||||
export const MULTI_VALUE_OPERATORS: ReadonlyArray<TSupportedOperators> = [
|
||||
...CORE_MULTI_VALUE_OPERATORS,
|
||||
...EXTENDED_MULTI_VALUE_OPERATORS,
|
||||
] as const;
|
||||
|
||||
// -------- COMPOSED TYPES --------
|
||||
|
||||
export type TLogicalOperator = (typeof LOGICAL_OPERATOR)[keyof typeof LOGICAL_OPERATOR];
|
||||
|
|
|
|||
|
|
@ -100,13 +100,15 @@ export const WORK_ITEM_FILTER_PROPERTY_KEYS = [
|
|||
"cycle_id",
|
||||
"module_id",
|
||||
"project_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
] as const;
|
||||
export type TWorkItemFilterProperty = (typeof WORK_ITEM_FILTER_PROPERTY_KEYS)[number];
|
||||
|
||||
export type TWorkItemFilterConditionKey = `${TWorkItemFilterProperty}__${TSupportedOperators}`;
|
||||
|
||||
export type TWorkItemFilterConditionData = Partial<{
|
||||
[K in TWorkItemFilterConditionKey]: string;
|
||||
[K in TWorkItemFilterConditionKey]: string | boolean | number;
|
||||
}>;
|
||||
|
||||
export type TWorkItemFilterAndGroup = {
|
||||
|
|
|
|||
|
|
@ -139,14 +139,14 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
|||
<Combobox.Options data-prevent-outside-click static>
|
||||
<div
|
||||
className={cn(
|
||||
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-48 whitespace-nowrap z-30",
|
||||
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-48 whitespace-nowrap z-30",
|
||||
optionsClassName
|
||||
)}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2 mx-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
|
|
@ -157,12 +157,13 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn("mt-2 space-y-1 overflow-y-scroll", {
|
||||
className={cn("mt-2 px-2 space-y-1 overflow-y-scroll vertical-scrollbar scrollbar-xs", {
|
||||
"max-h-96": maxHeight === "2xl",
|
||||
"max-h-80": maxHeight === "xl",
|
||||
"max-h-60": maxHeight === "lg",
|
||||
"max-h-48": maxHeight === "md",
|
||||
"max-h-36": maxHeight === "rg",
|
||||
"max-h-28": maxHeight === "sm",
|
||||
"max-h-full": maxHeight === "full",
|
||||
})}
|
||||
>
|
||||
{filteredOptions ? (
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export interface IDropdownProps {
|
|||
disabled?: boolean;
|
||||
input?: boolean;
|
||||
label?: string | React.ReactNode;
|
||||
maxHeight?: "sm" | "rg" | "md" | "lg" | "full";
|
||||
maxHeight?: "sm" | "rg" | "md" | "lg" | "xl" | "2xl";
|
||||
noChevron?: boolean;
|
||||
chevronClassName?: string;
|
||||
onOpen?: () => void;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export * from "./file";
|
|||
export * from "./filter";
|
||||
export * from "./get-icon-for-link";
|
||||
export * from "./intake";
|
||||
export * from "./loader";
|
||||
export * from "./math";
|
||||
export * from "./module";
|
||||
export * from "./notification";
|
||||
|
|
|
|||
4
packages/utils/src/loader.ts
Normal file
4
packages/utils/src/loader.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { TLoader } from "@plane/types";
|
||||
|
||||
// checks if a loader has finished initialization
|
||||
export const isLoaderReady = (loader: TLoader | undefined) => loader !== "init-loader";
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./core";
|
||||
export * from "./shared";
|
||||
export * from "./properties";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
// plane imports
|
||||
import { TFilterProperty } from "@plane/types";
|
||||
// local imports
|
||||
import { createFilterConfig, TCreateDateFilterParams, TCreateFilterConfig } from "../shared";
|
||||
import { getSupportedDateOperators, TCustomPropertyFilterParams } from "./shared";
|
||||
|
||||
/**
|
||||
* Date property filter specific params
|
||||
*/
|
||||
export type TCreateDatePropertyFilterParams = TCustomPropertyFilterParams<Date> & TCreateDateFilterParams;
|
||||
|
||||
/**
|
||||
* Get the date property filter config
|
||||
* @param params - The filter params
|
||||
* @returns The date property filter config
|
||||
*/
|
||||
export const getDatePropertyFilterConfig =
|
||||
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateDatePropertyFilterParams> =>
|
||||
(params: TCreateDatePropertyFilterParams) =>
|
||||
createFilterConfig({
|
||||
id: key,
|
||||
...params,
|
||||
label: params.propertyDisplayName,
|
||||
icon: params.filterIcon,
|
||||
allowMultipleFilters: true,
|
||||
supportedOperatorConfigsMap: getSupportedDateOperators(params),
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./date";
|
||||
export * from "./member-picker";
|
||||
export * from "./shared";
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// plane imports
|
||||
import { EQUALITY_OPERATOR, IUserLite, TFilterProperty } from "@plane/types";
|
||||
// local imports
|
||||
import { createFilterConfig, createOperatorConfigEntry, TCreateFilterConfig } from "../shared";
|
||||
import { getMemberMultiSelectConfig, TCreateUserFilterParams, TCustomPropertyFilterParams } from "./shared";
|
||||
|
||||
/**
|
||||
* Member picker property filter specific params
|
||||
*/
|
||||
type TCreateMemberPickerPropertyFilterParams = TCustomPropertyFilterParams<IUserLite> & TCreateUserFilterParams;
|
||||
|
||||
/**
|
||||
* Get the member picker property filter config
|
||||
* @param params - The filter params
|
||||
* @returns The member picker property filter config
|
||||
*/
|
||||
export const getMemberPickerPropertyFilterConfig =
|
||||
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateMemberPickerPropertyFilterParams> =>
|
||||
(params: TCreateMemberPickerPropertyFilterParams) =>
|
||||
createFilterConfig({
|
||||
id: key,
|
||||
...params,
|
||||
label: params.propertyDisplayName,
|
||||
icon: params.filterIcon,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(EQUALITY_OPERATOR.EXACT, params, (updatedParams) =>
|
||||
getMemberMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// plane imports
|
||||
import {
|
||||
COMPARISON_OPERATOR,
|
||||
EQUALITY_OPERATOR,
|
||||
IProject,
|
||||
IUserLite,
|
||||
TOperatorConfigMap,
|
||||
TSupportedOperators,
|
||||
} from "@plane/types";
|
||||
// local imports
|
||||
import { getDatePickerConfig, getDateRangePickerConfig, getMultiSelectConfig } from "../core";
|
||||
import {
|
||||
createOperatorConfigEntry,
|
||||
IFilterIconConfig,
|
||||
TCreateDateFilterParams,
|
||||
TCreateFilterConfigParams,
|
||||
} from "../shared";
|
||||
|
||||
// ------------ Base User Filter Types ------------
|
||||
|
||||
/**
|
||||
* User filter specific params
|
||||
*/
|
||||
export type TCreateUserFilterParams = TCreateFilterConfigParams &
|
||||
IFilterIconConfig<IUserLite> & {
|
||||
members: IUserLite[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get the member multi select config
|
||||
* @param params - The filter params
|
||||
* @returns The member multi select config
|
||||
*/
|
||||
export const getMemberMultiSelectConfig = (params: TCreateUserFilterParams, singleValueOperator: TSupportedOperators) =>
|
||||
getMultiSelectConfig<IUserLite, string, IUserLite>(
|
||||
{
|
||||
items: params.members,
|
||||
getId: (member) => member.id,
|
||||
getLabel: (member) => member.display_name,
|
||||
getValue: (member) => member.id,
|
||||
getIconData: (member) => member,
|
||||
},
|
||||
{
|
||||
singleValueOperator,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
...params,
|
||||
}
|
||||
);
|
||||
|
||||
// ------------ Date Operators ------------
|
||||
|
||||
export const getSupportedDateOperators = (params: TCreateDateFilterParams): TOperatorConfigMap<Date> =>
|
||||
new Map([
|
||||
createOperatorConfigEntry(EQUALITY_OPERATOR.EXACT, params, (updatedParams) => getDatePickerConfig(updatedParams)),
|
||||
createOperatorConfigEntry(COMPARISON_OPERATOR.RANGE, params, (updatedParams) =>
|
||||
getDateRangePickerConfig(updatedParams)
|
||||
),
|
||||
]);
|
||||
|
||||
// ------------ Project filter ------------
|
||||
|
||||
/**
|
||||
* Project filter specific params
|
||||
*/
|
||||
export type TCreateProjectFilterParams = TCreateFilterConfigParams &
|
||||
IFilterIconConfig<IProject> & {
|
||||
projects: IProject[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get the project multi select config
|
||||
* @param params - The filter params
|
||||
* @returns The member multi select config
|
||||
*/
|
||||
export const getProjectMultiSelectConfig = (
|
||||
params: TCreateProjectFilterParams,
|
||||
singleValueOperator: TSupportedOperators
|
||||
) =>
|
||||
getMultiSelectConfig<IProject, string, IProject>(
|
||||
{
|
||||
items: params.projects,
|
||||
getId: (project) => project.id,
|
||||
getLabel: (project) => project.name,
|
||||
getValue: (project) => project.id,
|
||||
getIconData: (project) => project,
|
||||
},
|
||||
{
|
||||
singleValueOperator,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
...params,
|
||||
}
|
||||
);
|
||||
|
|
@ -29,14 +29,21 @@ export const createFilterConfig = <P extends TFilterProperty, V extends TFilterV
|
|||
export type TCreateFilterConfigParams = Omit<TBaseFilterFieldConfig, "isOperatorEnabled"> & {
|
||||
isEnabled: boolean;
|
||||
allowedOperators: Set<TSupportedOperators>;
|
||||
rightContent?: React.ReactNode; // content to display on the right side of the filter option in the dropdown
|
||||
tooltipContent?: React.ReactNode; // content to display when hovering over the applied filter item in the filter list
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for filter icon type
|
||||
*/
|
||||
export type TFilterIconType = string | number | boolean | object | undefined;
|
||||
|
||||
/**
|
||||
* Icon configuration for filters and their options.
|
||||
* - filterIcon: Optional icon for the filter
|
||||
* - getOptionIcon: Function to get icon for specific option values
|
||||
*/
|
||||
export interface IFilterIconConfig<T extends string | number | boolean | object | undefined = undefined> {
|
||||
export interface IFilterIconConfig<T extends TFilterIconType = undefined> {
|
||||
filterIcon?: React.FC<React.SVGAttributes<SVGElement>>;
|
||||
getOptionIcon?: (value: T) => React.ReactNode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./configs/core";
|
||||
export * from "./configs/shared";
|
||||
export * from "./configs/properties";
|
||||
export * from "./nodes/core";
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ export const getCycleFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Cycle",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getCycleMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
// plane imports
|
||||
import { TFilterProperty } from "@plane/types";
|
||||
// local imports
|
||||
import { createFilterConfig, TCreateFilterConfig, TCreateDateFilterParams } from "../../../rich-filters";
|
||||
import { getSupportedDateOperators } from "./shared";
|
||||
import {
|
||||
createFilterConfig,
|
||||
TCreateFilterConfig,
|
||||
TCreateDateFilterParams,
|
||||
getSupportedDateOperators,
|
||||
} from "../../../rich-filters";
|
||||
|
||||
// ------------ Date filters ------------
|
||||
|
||||
|
|
@ -18,8 +22,8 @@ export const getStartDateFilterConfig =
|
|||
createFilterConfig<P, Date>({
|
||||
id: key,
|
||||
label: "Start date",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
allowMultipleFilters: true,
|
||||
supportedOperatorConfigsMap: getSupportedDateOperators(params),
|
||||
});
|
||||
|
|
@ -36,8 +40,44 @@ export const getTargetDateFilterConfig =
|
|||
createFilterConfig<P, Date>({
|
||||
id: key,
|
||||
label: "Target date",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
allowMultipleFilters: true,
|
||||
supportedOperatorConfigsMap: getSupportedDateOperators(params),
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the created at filter config
|
||||
* @template K - The filter key
|
||||
* @param key - The filter key to use
|
||||
* @returns A function that takes parameters and returns the created at filter config
|
||||
*/
|
||||
export const getCreatedAtFilterConfig =
|
||||
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateDateFilterParams> =>
|
||||
(params: TCreateDateFilterParams) =>
|
||||
createFilterConfig<P, Date>({
|
||||
id: key,
|
||||
label: "Created at",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
allowMultipleFilters: true,
|
||||
supportedOperatorConfigsMap: getSupportedDateOperators(params),
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the updated at filter config
|
||||
* @template K - The filter key
|
||||
* @param key - The filter key to use
|
||||
* @returns A function that takes parameters and returns the updated at filter config
|
||||
*/
|
||||
export const getUpdatedAtFilterConfig =
|
||||
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateDateFilterParams> =>
|
||||
(params: TCreateDateFilterParams) =>
|
||||
createFilterConfig<P, Date>({
|
||||
id: key,
|
||||
label: "Updated at",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
allowMultipleFilters: true,
|
||||
supportedOperatorConfigsMap: getSupportedDateOperators(params),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ export const getLabelFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Label",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getLabelMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ export const getModuleFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Module",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getModuleMultiSelectConfig(updatedParams)
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ export const getPriorityFilterConfig =
|
|||
createFilterConfig<P, TIssuePriorities>({
|
||||
id: key,
|
||||
label: "Priority",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getPriorityMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
// plane imports
|
||||
import { EQUALITY_OPERATOR, TFilterProperty, COLLECTION_OPERATOR } from "@plane/types";
|
||||
// local imports
|
||||
import { createFilterConfig, createOperatorConfigEntry, TCreateFilterConfig } from "../../../rich-filters";
|
||||
import { getProjectMultiSelectConfig, TCreateProjectFilterParams } from "./shared";
|
||||
import {
|
||||
createFilterConfig,
|
||||
createOperatorConfigEntry,
|
||||
getProjectMultiSelectConfig,
|
||||
TCreateFilterConfig,
|
||||
TCreateProjectFilterParams,
|
||||
} from "../../../rich-filters";
|
||||
|
||||
// ------------ Project filter ------------
|
||||
|
||||
|
|
@ -18,8 +23,8 @@ export const getProjectFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Projects",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getProjectMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ export const getStateGroupFilterConfig =
|
|||
createFilterConfig<P, TStateGroups>({
|
||||
id: key,
|
||||
label: "State Group",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getStateGroupMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
|
|
@ -87,7 +87,7 @@ export type TCreateStateFilterParams = TCreateFilterConfigParams &
|
|||
* @param params - The filter params
|
||||
* @returns The state multi select config
|
||||
*/
|
||||
export const getStateMultiSelectConfig = (params: TCreateStateFilterParams) =>
|
||||
export const getStateMultiSelectConfig = (params: TCreateStateFilterParams, singleValueOperator: TSupportedOperators) =>
|
||||
getMultiSelectConfig<IState, string, IState>(
|
||||
{
|
||||
items: params.states,
|
||||
|
|
@ -97,7 +97,7 @@ export const getStateMultiSelectConfig = (params: TCreateStateFilterParams) =>
|
|||
getIconData: (state) => state,
|
||||
},
|
||||
{
|
||||
singleValueOperator: EQUALITY_OPERATOR.EXACT,
|
||||
singleValueOperator,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
|
|
@ -117,11 +117,11 @@ export const getStateFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "State",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getStateMultiSelectConfig(updatedParams)
|
||||
getStateMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,48 +1,14 @@
|
|||
// plane imports
|
||||
import { EQUALITY_OPERATOR, IUserLite, TFilterProperty, COLLECTION_OPERATOR } from "@plane/types";
|
||||
import { EQUALITY_OPERATOR, TFilterProperty, COLLECTION_OPERATOR } from "@plane/types";
|
||||
// local imports
|
||||
import {
|
||||
createFilterConfig,
|
||||
TCreateFilterConfigParams,
|
||||
IFilterIconConfig,
|
||||
TCreateFilterConfig,
|
||||
getMultiSelectConfig,
|
||||
createOperatorConfigEntry,
|
||||
getMemberMultiSelectConfig,
|
||||
TCreateUserFilterParams,
|
||||
} from "../../../rich-filters";
|
||||
|
||||
// ------------ Base User Filter Types ------------
|
||||
|
||||
/**
|
||||
* User filter specific params
|
||||
*/
|
||||
export type TCreateUserFilterParams = TCreateFilterConfigParams &
|
||||
IFilterIconConfig<IUserLite> & {
|
||||
members: IUserLite[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get the member multi select config
|
||||
* @param params - The filter params
|
||||
* @returns The member multi select config
|
||||
*/
|
||||
export const getMemberMultiSelectConfig = (params: TCreateUserFilterParams) =>
|
||||
getMultiSelectConfig<IUserLite, string, IUserLite>(
|
||||
{
|
||||
items: params.members,
|
||||
getId: (member) => member.id,
|
||||
getLabel: (member) => member.display_name,
|
||||
getValue: (member) => member.id,
|
||||
getIconData: (member) => member,
|
||||
},
|
||||
{
|
||||
singleValueOperator: EQUALITY_OPERATOR.EXACT,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
...params,
|
||||
}
|
||||
);
|
||||
|
||||
// ------------ Assignee filter ------------
|
||||
|
||||
/**
|
||||
|
|
@ -62,11 +28,11 @@ export const getAssigneeFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Assignees",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getMemberMultiSelectConfig(updatedParams)
|
||||
getMemberMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
|
@ -90,11 +56,11 @@ export const getMentionFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Mentions",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getMemberMultiSelectConfig(updatedParams)
|
||||
getMemberMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
|
@ -118,11 +84,11 @@ export const getCreatedByFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Created by",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getMemberMultiSelectConfig(updatedParams)
|
||||
getMemberMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
|
@ -146,11 +112,11 @@ export const getSubscriberFilterConfig =
|
|||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Subscriber",
|
||||
...params,
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getMemberMultiSelectConfig(updatedParams)
|
||||
getMemberMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue