diff --git a/apps/api/plane/utils/filters/filterset.py b/apps/api/plane/utils/filters/filterset.py index 8fff8de37..0099b83d0 100644 --- a/apps/api/plane/utils/filters/filterset.py +++ b/apps/api/plane/utils/filters/filterset.py @@ -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"], } diff --git a/apps/web/ce/components/rich-filters/filter-value-input/root.tsx b/apps/web/ce/components/rich-filters/filter-value-input/root.tsx new file mode 100644 index 000000000..5a3842164 --- /dev/null +++ b/apps/web/ce/components/rich-filters/filter-value-input/root.tsx @@ -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( +

(_props: TFilterValueInputProps) => ( + // Fallback +

+ Filter type not supported +
+ ) +); diff --git a/apps/web/ce/helpers/work-item-filters/project-level.ts b/apps/web/ce/helpers/work-item-filters/project-level.ts index d6e069503..97724db44 100644 --- a/apps/web/ce/helpers/work-item-filters/project-level.ts +++ b/apps/web/ce/helpers/work-item-filters/project-level.ts @@ -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; }; diff --git a/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx b/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx index e56657e12..41fa9c2b5 100644 --- a/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx +++ b/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx @@ -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[]; configMap: { [key in TWorkItemFilterProperty]?: TFilterConfig; }; 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("created_at")({ + isEnabled: true, + filterIcon: Calendar, + ...operatorConfigs, + }), + [operatorConfigs] + ); + + // updated at filter config + const updatedAtFilterConfig = useMemo( + () => + getUpdatedAtFilterConfig("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 ?? [], }; }; diff --git a/apps/web/core/components/rich-filters/add-filters/button.tsx b/apps/web/core/components/rich-filters/add-filters/button.tsx new file mode 100644 index 000000000..a9f6581d9 --- /dev/null +++ b/apps/web/core/components/rich-filters/add-filters/button.tsx @@ -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

= { + buttonConfig?: { + label: string | null; + variant?: TButtonVariant; + className?: string; + defaultOpen?: boolean; + iconConfig?: { + shouldShowIcon: boolean; + iconComponent?: React.ElementType; + }; + isDisabled?: boolean; + }; + filter: IFilterInstance; + onFilterSelect?: (id: string) => void; +}; + +export const AddFilterButton = observer( +

(props: TAddFilterButtonProps) => { + 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 ( + + {iconConfig.shouldShowIcon && } + {label} + + } + /> + ); + } +); diff --git a/apps/web/core/components/rich-filters/add-filters-button.tsx b/apps/web/core/components/rich-filters/add-filters/dropdown.tsx similarity index 50% rename from apps/web/core/components/rich-filters/add-filters-button.tsx rename to apps/web/core/components/rich-filters/add-filters/dropdown.tsx index 305cb8ae2..25f138a00 100644 --- a/apps/web/core/components/rich-filters/add-filters-button.tsx +++ b/apps/web/core/components/rich-filters/add-filters/dropdown.tsx @@ -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

= { +export type TAddFilterDropdownProps

= { + customButton: React.ReactNode; buttonConfig?: { - label: string | null; - variant?: TButtonVariant; className?: string; defaultOpen?: boolean; - iconConfig?: { - shouldShowIcon: boolean; - iconComponent?: React.ElementType; - }; isDisabled?: boolean; }; filter: IFilterInstance; - onFilterSelect?: (id: string) => void; + handleFilterSelect: (property: P, operator: TSupportedOperators, isNegation: boolean) => void; }; -export const AddFilterButton = observer( -

(props: TAddFilterButtonProps) => { - 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( +

(props: TAddFilterDropdownProps) => { + 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: ( -

- {config.icon && ( - - )} - {config.label} +
+
+ {config.icon && ( + + )} + {config.label} +
+ {config.rightContent}
), 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 (
- {iconConfig.shouldShowIcon && } - {label} -
- } + customButtonClassName={className} + customButton={customButton} />
); diff --git a/apps/web/core/components/rich-filters/filter-item/close-button.tsx b/apps/web/core/components/rich-filters/filter-item/close-button.tsx new file mode 100644 index 000000000..e53026850 --- /dev/null +++ b/apps/web/core/components/rich-filters/filter-item/close-button.tsx @@ -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

{ + conditionId: string; + filter: IFilterInstance; +} + +export const FilterItemCloseButton = observer( +

(props: FilterItemCloseButtonProps) => { + const { conditionId, filter } = props; + + const handleRemoveFilter = () => { + filter.removeCondition(conditionId); + }; + + return ( + + ); + } +); diff --git a/apps/web/core/components/rich-filters/filter-item/container.tsx b/apps/web/core/components/rich-filters/filter-item/container.tsx new file mode 100644 index 000000000..f044c1b32 --- /dev/null +++ b/apps/web/core/components/rich-filters/filter-item/container.tsx @@ -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; + showTransition: boolean; + variant?: "default" | "error"; + tooltipContent?: React.ReactNode; +} + +export const FilterItemContainer: React.FC = (props) => { + const { children, conditionValue, showTransition, variant = "default", tooltipContent } = props; + // refs + const itemRef = useRef(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 ( + +

+ {children} +
+ + ); +}; diff --git a/apps/web/core/components/rich-filters/filter-item/invalid.tsx b/apps/web/core/components/rich-filters/filter-item/invalid.tsx new file mode 100644 index 000000000..18c9711a7 --- /dev/null +++ b/apps/web/core/components/rich-filters/filter-item/invalid.tsx @@ -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( +

(props: IFilterItemProps) => { + const { condition, filter, isDisabled = false, showTransition = true } = props; + + return ( + + {/* Property section */} + + {/* Remove button */} + {!isDisabled && } + + ); + } +); diff --git a/apps/web/core/components/rich-filters/filter-item/loader.tsx b/apps/web/core/components/rich-filters/filter-item/loader.tsx new file mode 100644 index 000000000..c2cbddefc --- /dev/null +++ b/apps/web/core/components/rich-filters/filter-item/loader.tsx @@ -0,0 +1,7 @@ +import { Loader } from "@plane/ui"; + +export const FilterItemLoader = () => ( + + + +); diff --git a/apps/web/core/components/rich-filters/filter-item/property.tsx b/apps/web/core/components/rich-filters/filter-item/property.tsx new file mode 100644 index 000000000..9999a4fb4 --- /dev/null +++ b/apps/web/core/components/rich-filters/filter-item/property.tsx @@ -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

{ + conditionId: string; + icon: React.FC> | undefined; + isDisabled?: boolean; + filter: IFilterInstance; + label: string; + tooltipContent?: React.ReactNode | undefined; +} + +export const FilterItemProperty = observer( +

(props: IFilterItemPropertyProps) => { + const { conditionId, filter, isDisabled } = props; + + if (isDisabled) { + return ; + } + + const handleFilterSelect = (property: P, operator: TSupportedOperators, isNegation: boolean) => { + filter.updateConditionProperty(conditionId, property, operator, isNegation); + }; + + return ( + } + /> + ); + } +); + +type TPropertyButtonProps

= IFilterItemPropertyProps & { + className?: string; +}; + +const PropertyButton =

(props: TPropertyButtonProps) => { + const { icon: Icon, label, tooltipContent, className } = props; + + return ( + +

+ {Icon && ( +
+ +
+ )} + {label} +
+ + ); +}; diff --git a/apps/web/core/components/rich-filters/filter-item.tsx b/apps/web/core/components/rich-filters/filter-item/root.tsx similarity index 56% rename from apps/web/core/components/rich-filters/filter-item.tsx rename to apps/web/core/components/rich-filters/filter-item/root.tsx index 9576c1c33..9fa373efd 100644 --- a/apps/web/core/components/rich-filters/filter-item.tsx +++ b/apps/web/core/components/rich-filters/filter-item/root.tsx @@ -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

{ +export interface IFilterItemProps

{ condition: TFilterConditionNodeForDisplay; filter: IFilterInstance; isDisabled?: boolean; @@ -25,10 +29,8 @@ interface FilterItemProps

} export const FilterItem = observer( -

(props: FilterItemProps) => { +

(props: IFilterItemProps) => { const { condition, filter, isDisabled = false, showTransition = true } = props; - // refs - const itemRef = useRef(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 ; + } + + if (!filterConfig) { + return ( + + ); + } - if (!filterConfig || !filterConfig.isEnabled) return null; return ( -

+ {/* Property section */} -
- {filterConfig.icon && ( -
- -
- )} - {filterConfig.label} -
+ {/* Operator section */} @@ -145,17 +119,8 @@ export const FilterItem = observer( )} {/* Remove button */} - {!isDisabled && ( - - )} -
+ {!isDisabled && } + ); } ); diff --git a/apps/web/core/components/rich-filters/filter-value-input/date/range.tsx b/apps/web/core/components/rich-filters/filter-value-input/date/range.tsx index df60d4d5e..10616b023 100644 --- a/apps/web/core/components/rich-filters/filter-value-input/date/range.tsx +++ b/apps/web/core/components/rich-filters/filter-value-input/date/range.tsx @@ -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

= { config: TDateRangeFilterFieldConfig; @@ -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, diff --git a/apps/web/core/components/rich-filters/filter-value-input/date/single.tsx b/apps/web/core/components/rich-filters/filter-value-input/date/single.tsx index c03256c03..5fd60d7a2 100644 --- a/apps/web/core/components/rich-filters/filter-value-input/date/single.tsx +++ b/apps/web/core/components/rich-filters/filter-value-input/date/single.tsx @@ -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

= { config: TDateFilterFieldConfig; @@ -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 diff --git a/apps/web/core/components/rich-filters/filter-value-input/root.tsx b/apps/web/core/components/rich-filters/filter-value-input/root.tsx index ce5777e95..a13640e88 100644 --- a/apps/web/core/components/rich-filters/filter-value-input/root.tsx +++ b/apps/web/core/components/rich-filters/filter-value-input/root.tsx @@ -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

= { - condition: TFilterConditionNodeForDisplay; - filterFieldConfig: TSupportedFilterFieldConfigs; - isDisabled?: boolean; - onChange: (values: SingleOrArray) => void; -}; - -// TODO: Prevent type assertion export const FilterValueInput = observer(

(props: TFilterValueInputProps) => { const { condition, filterFieldConfig, isDisabled = false, onChange } = props; @@ -80,11 +74,6 @@ export const FilterValueInput = observer( ); } - // Fallback - return ( -

- Filter type not supported -
- ); + return ; } ); diff --git a/apps/web/core/components/rich-filters/filter-value-input/select/selected-options-display.tsx b/apps/web/core/components/rich-filters/filter-value-input/select/selected-options-display.tsx index a2c35893a..348171ee9 100644 --- a/apps/web/core/components/rich-filters/filter-value-input/select/selected-options-display.tsx +++ b/apps/web/core/components/rich-filters/filter-value-input/select/selected-options-display.tsx @@ -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 = { selectedValue: SingleOrArray; @@ -13,7 +14,7 @@ type TSelectedOptionsDisplayProps = { }; export const SelectedOptionsDisplay = (props: TSelectedOptionsDisplayProps) => { - 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; diff --git a/apps/web/core/components/rich-filters/filters-row.tsx b/apps/web/core/components/rich-filters/filters-row.tsx index 0bac4a4c1..0d1c200c9 100644 --- a/apps/web/core/components/rich-filters/filters-row.tsx +++ b/apps/web/core/components/rich-filters/filters-row.tsx @@ -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 = { buttonConfig?: TAddFilterButtonProps["buttonConfig"]; @@ -25,10 +25,17 @@ export type TFiltersRowProps(props: TFiltersRowProps) => { - 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( ); - return ( - - {variant === "modal" ? ModalVariant : HeaderVariant} - - ); + if (!filter.configManager.areConfigsReady && !hasAnyConditions) { + return ( + + + + + + ); + } + + return {variant === "modal" ? ModalVariant : HeaderVariant}; } ); @@ -176,3 +181,22 @@ const ElementTransition = observer((props: TElementTransitionProps) => ( {props.children} )); + +type TRowTransitionProps = { + children: React.ReactNode; + show: boolean; +}; + +const RowTransition = observer((props: TRowTransitionProps) => ( + + {props.children} + +)); diff --git a/apps/web/core/components/rich-filters/filters-toggle.tsx b/apps/web/core/components/rich-filters/filters-toggle.tsx index 5c5f9d222..a1bea1882 100644 --- a/apps/web/core/components/rich-filters/filters-toggle.tsx +++ b/apps/web/core/components/rich-filters/filters-toggle.tsx @@ -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

= { filter: IFilterInstance | undefined; diff --git a/apps/web/core/components/rich-filters/shared.ts b/apps/web/core/components/rich-filters/shared.ts index 7361c81f2..9caf23643 100644 --- a/apps/web/core/components/rich-filters/shared.ts +++ b/apps/web/core/components/rich-filters/shared.ts @@ -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

= { + condition: TFilterConditionNodeForDisplay; + filterFieldConfig: TSupportedFilterFieldConfigs; + isDisabled?: boolean; + onChange: (values: SingleOrArray) => void; +}; diff --git a/apps/web/core/components/work-item-filters/filters-hoc/base.tsx b/apps/web/core/components/work-item-filters/filters-hoc/base.tsx index eff257d04..903e38f9b 100644 --- a/apps/web/core/components/work-item-filters/filters-hoc/base.tsx +++ b/apps/web/core/components/work-item-filters/filters-hoc/base.tsx @@ -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}; diff --git a/apps/web/core/components/work-item-filters/filters-hoc/project-level.tsx b/apps/web/core/components/work-item-filters/filters-hoc/project-level.tsx index 8a31f9053..6ef0ae8ff 100644 --- a/apps/web/core/components/work-item-filters/filters-hoc/project-level.tsx +++ b/apps/web/core/components/work-item-filters/filters-hoc/project-level.tsx @@ -167,6 +167,7 @@ export const ProjectLevelWorkItemFiltersHOC = observer((props: TProjectLevelWork { // observables filterConfigs: Map>; // filter property -> config configOptions: TConfigOptions; + areConfigsReady: boolean; // computed allAvailableConfigs: IFilterConfig[]; // computed functions @@ -32,6 +33,7 @@ export interface IFilterConfigManager

{ register: >(config: C) => void; registerAll: (configs: TFilterConfig[]) => void; updateConfigByProperty: (property: P, configUpdates: Partial>) => void; + setAreConfigsReady: (value: boolean) => void; } /** @@ -57,6 +59,7 @@ export class FilterConfigManager

["filterConfigs"]; configOptions: IFilterConfigManager

["configOptions"]; + areConfigsReady: IFilterConfigManager

["areConfigsReady"]; // parent filter instance private _filterInstance: IFilterInstance; @@ -69,18 +72,21 @@ export class FilterConfigManager

, params: TConfigManagerParams) { this.filterConfigs = new Map>(); 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

["setAreConfigsReady"] = action((value) => { + this.areConfigsReady = value; + }); + // ------------ private computed ------------ private get _allConfigs(): IFilterConfig[] { diff --git a/packages/shared-state/src/store/rich-filters/filter-helpers.ts b/packages/shared-state/src/store/rich-filters/filter-helpers.ts index c9483408b..9c57c3d71 100644 --- a/packages/shared-state/src/store/rich-filters/filter-helpers.ts +++ b/packages/shared-state/src/store/rich-filters/filter-helpers.ts @@ -6,6 +6,7 @@ import { IFilterAdapter, LOGICAL_OPERATOR, TSupportedOperators, + TFilterConditionNode, TFilterExpression, TFilterValue, TFilterProperty, @@ -43,9 +44,16 @@ export interface IFilterInstanceHelper

, isNegation: boolean ) => TFilterExpression

| null; + handleConditionPropertyUpdate: ( + expression: TFilterExpression

, + conditionId: string, + property: P, + operator: TSupportedOperators, + isNegation: boolean + ) => TFilterExpression

| null; // group operations restructureExpressionForOperatorChange: ( - expression: TFilterExpression

| null, + expression: TFilterExpression

, conditionId: string, newOperator: TSupportedOperators, isNegation: boolean, @@ -162,6 +170,28 @@ export class FilterInstanceHelper

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["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

{ - 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

, + conditionId: string, + payload: Partial>, + _isNegation: boolean + ): TFilterExpression

| null => { + // Update the condition with the payload + updateNodeInExpression(expression, conditionId, payload); + + return expression; + }; } diff --git a/packages/shared-state/src/store/rich-filters/filter.ts b/packages/shared-state/src/store/rich-filters/filter.ts index be13c7f88..94236036b 100644 --- a/packages/shared-state/src/store/rich-filters/filter.ts +++ b/packages/shared-state/src/store/rich-filters/filter.ts @@ -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

, isNegation: boolean ) => void; + updateConditionProperty: ( + conditionId: string, + property: P, + operator: TSupportedOperators, + isNegation: boolean + ) => void; updateConditionOperator: (conditionId: string, operator: TSupportedOperators, isNegation: boolean) => void; updateConditionValue: (conditionId: string, value: SingleOrArray) => void; removeCondition: (conditionId: string) => void; @@ -360,6 +366,33 @@ export class FilterInstance

["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

=> { + private _parseFilterValue = (value: TFilterValue): SingleOrArray => { + if (!value) return value; + if (typeof value !== "string") return value; // Handle empty string diff --git a/packages/types/src/rich-filters/config/filter-config.ts b/packages/types/src/rich-filters/config/filter-config.ts index 9ab7aeed2..de84a6596 100644 --- a/packages/types/src/rich-filters/config/filter-config.ts +++ b/packages/types/src/rich-filters/config/filter-config.ts @@ -15,4 +15,6 @@ export type TFilterConfig

; + 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 }; diff --git a/packages/types/src/rich-filters/operators/core.ts b/packages/types/src/rich-filters/operators/core.ts index 573b1a0a3..602fdab94 100644 --- a/packages/types/src/rich-filters/operators/core.ts +++ b/packages/types/src/rich-filters/operators/core.ts @@ -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 */ diff --git a/packages/types/src/rich-filters/operators/extended.ts b/packages/types/src/rich-filters/operators/extended.ts index db54ec91e..4616e40b6 100644 --- a/packages/types/src/rich-filters/operators/extended.ts +++ b/packages/types/src/rich-filters/operators/extended.ts @@ -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 */ diff --git a/packages/types/src/rich-filters/operators/index.ts b/packages/types/src/rich-filters/operators/index.ts index 458eff497..bcfd3cbba 100644 --- a/packages/types/src/rich-filters/operators/index.ts +++ b/packages/types/src/rich-filters/operators/index.ts @@ -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 = [ + ...CORE_MULTI_VALUE_OPERATORS, + ...EXTENDED_MULTI_VALUE_OPERATORS, +] as const; + // -------- COMPOSED TYPES -------- export type TLogicalOperator = (typeof LOGICAL_OPERATOR)[keyof typeof LOGICAL_OPERATOR]; diff --git a/packages/types/src/view-props.ts b/packages/types/src/view-props.ts index d073eafb9..04f4dcc85 100644 --- a/packages/types/src/view-props.ts +++ b/packages/types/src/view-props.ts @@ -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 = { diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index 8fdd0f664..37b7652ef 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -139,14 +139,14 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {

-
+
{ />
{filteredOptions ? ( diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 8a3fc0e3b..5ce0ba82b 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -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; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ddd80a886..7cc4c58e9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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"; diff --git a/packages/utils/src/loader.ts b/packages/utils/src/loader.ts new file mode 100644 index 000000000..d876f267a --- /dev/null +++ b/packages/utils/src/loader.ts @@ -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"; diff --git a/packages/utils/src/rich-filters/factories/configs/index.ts b/packages/utils/src/rich-filters/factories/configs/index.ts index 102ec949b..6a75f4dce 100644 --- a/packages/utils/src/rich-filters/factories/configs/index.ts +++ b/packages/utils/src/rich-filters/factories/configs/index.ts @@ -1,2 +1,3 @@ export * from "./core"; export * from "./shared"; +export * from "./properties"; diff --git a/packages/utils/src/rich-filters/factories/configs/properties/date.ts b/packages/utils/src/rich-filters/factories/configs/properties/date.ts new file mode 100644 index 000000000..e22abd5ad --- /dev/null +++ b/packages/utils/src/rich-filters/factories/configs/properties/date.ts @@ -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 & TCreateDateFilterParams; + +/** + * Get the date property filter config + * @param params - The filter params + * @returns The date property filter config + */ +export const getDatePropertyFilterConfig = +

(key: P): TCreateFilterConfig => + (params: TCreateDatePropertyFilterParams) => + createFilterConfig({ + id: key, + ...params, + label: params.propertyDisplayName, + icon: params.filterIcon, + allowMultipleFilters: true, + supportedOperatorConfigsMap: getSupportedDateOperators(params), + }); diff --git a/packages/utils/src/rich-filters/factories/configs/properties/index.ts b/packages/utils/src/rich-filters/factories/configs/properties/index.ts new file mode 100644 index 000000000..96edf472a --- /dev/null +++ b/packages/utils/src/rich-filters/factories/configs/properties/index.ts @@ -0,0 +1,3 @@ +export * from "./date"; +export * from "./member-picker"; +export * from "./shared"; diff --git a/packages/utils/src/rich-filters/factories/configs/properties/member-picker.ts b/packages/utils/src/rich-filters/factories/configs/properties/member-picker.ts new file mode 100644 index 000000000..d94ad4aba --- /dev/null +++ b/packages/utils/src/rich-filters/factories/configs/properties/member-picker.ts @@ -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 & TCreateUserFilterParams; + +/** + * Get the member picker property filter config + * @param params - The filter params + * @returns The member picker property filter config + */ +export const getMemberPickerPropertyFilterConfig = +

(key: P): TCreateFilterConfig => + (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) + ), + ]), + }); diff --git a/packages/utils/src/rich-filters/factories/configs/properties/shared.ts b/packages/utils/src/rich-filters/factories/configs/properties/shared.ts new file mode 100644 index 000000000..1fa1267b2 --- /dev/null +++ b/packages/utils/src/rich-filters/factories/configs/properties/shared.ts @@ -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 & { + 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( + { + 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 => + 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 & { + 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( + { + items: params.projects, + getId: (project) => project.id, + getLabel: (project) => project.name, + getValue: (project) => project.id, + getIconData: (project) => project, + }, + { + singleValueOperator, + ...params, + }, + { + ...params, + } + ); diff --git a/packages/utils/src/rich-filters/factories/configs/shared.ts b/packages/utils/src/rich-filters/factories/configs/shared.ts index 1c43e0a9d..3a24f3896 100644 --- a/packages/utils/src/rich-filters/factories/configs/shared.ts +++ b/packages/utils/src/rich-filters/factories/configs/shared.ts @@ -29,14 +29,21 @@ export const createFilterConfig =

& { isEnabled: boolean; allowedOperators: Set; + 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 { +export interface IFilterIconConfig { filterIcon?: React.FC>; getOptionIcon?: (value: T) => React.ReactNode; } diff --git a/packages/utils/src/rich-filters/factories/index.ts b/packages/utils/src/rich-filters/factories/index.ts index 9518a7e35..976c1ab63 100644 --- a/packages/utils/src/rich-filters/factories/index.ts +++ b/packages/utils/src/rich-filters/factories/index.ts @@ -1,3 +1,4 @@ export * from "./configs/core"; export * from "./configs/shared"; +export * from "./configs/properties"; export * from "./nodes/core"; diff --git a/packages/utils/src/work-item-filters/configs/filters/cycle.ts b/packages/utils/src/work-item-filters/configs/filters/cycle.ts index 08b8aff6b..9228c7f49 100644 --- a/packages/utils/src/work-item-filters/configs/filters/cycle.ts +++ b/packages/utils/src/work-item-filters/configs/filters/cycle.ts @@ -60,8 +60,8 @@ export const getCycleFilterConfig = createFilterConfig({ 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) diff --git a/packages/utils/src/work-item-filters/configs/filters/date.ts b/packages/utils/src/work-item-filters/configs/filters/date.ts index 1de8f9728..1492c4518 100644 --- a/packages/utils/src/work-item-filters/configs/filters/date.ts +++ b/packages/utils/src/work-item-filters/configs/filters/date.ts @@ -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({ 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({ 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 = +

(key: P): TCreateFilterConfig => + (params: TCreateDateFilterParams) => + createFilterConfig({ + 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 = +

(key: P): TCreateFilterConfig => + (params: TCreateDateFilterParams) => + createFilterConfig({ + id: key, + label: "Updated at", + ...params, icon: params.filterIcon, - isEnabled: params.isEnabled, allowMultipleFilters: true, supportedOperatorConfigsMap: getSupportedDateOperators(params), }); diff --git a/packages/utils/src/work-item-filters/configs/filters/label.ts b/packages/utils/src/work-item-filters/configs/filters/label.ts index 41fa84bf3..dcabd9db3 100644 --- a/packages/utils/src/work-item-filters/configs/filters/label.ts +++ b/packages/utils/src/work-item-filters/configs/filters/label.ts @@ -59,8 +59,8 @@ export const getLabelFilterConfig = createFilterConfig({ 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) diff --git a/packages/utils/src/work-item-filters/configs/filters/module.ts b/packages/utils/src/work-item-filters/configs/filters/module.ts index 0c595eb25..8c36409c1 100644 --- a/packages/utils/src/work-item-filters/configs/filters/module.ts +++ b/packages/utils/src/work-item-filters/configs/filters/module.ts @@ -53,8 +53,8 @@ export const getModuleFilterConfig = createFilterConfig({ id: key, label: "Module", + ...params, icon: params.filterIcon, - isEnabled: params.isEnabled, supportedOperatorConfigsMap: new Map([ createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) => getModuleMultiSelectConfig(updatedParams) diff --git a/packages/utils/src/work-item-filters/configs/filters/priority.ts b/packages/utils/src/work-item-filters/configs/filters/priority.ts index b04a3b786..0d5834c5e 100644 --- a/packages/utils/src/work-item-filters/configs/filters/priority.ts +++ b/packages/utils/src/work-item-filters/configs/filters/priority.ts @@ -56,8 +56,8 @@ export const getPriorityFilterConfig = createFilterConfig({ 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) diff --git a/packages/utils/src/work-item-filters/configs/filters/project.ts b/packages/utils/src/work-item-filters/configs/filters/project.ts index b5c123ed4..fcefa309c 100644 --- a/packages/utils/src/work-item-filters/configs/filters/project.ts +++ b/packages/utils/src/work-item-filters/configs/filters/project.ts @@ -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({ 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) diff --git a/packages/utils/src/work-item-filters/configs/filters/state.ts b/packages/utils/src/work-item-filters/configs/filters/state.ts index 2281171e1..b3a3d6daf 100644 --- a/packages/utils/src/work-item-filters/configs/filters/state.ts +++ b/packages/utils/src/work-item-filters/configs/filters/state.ts @@ -63,8 +63,8 @@ export const getStateGroupFilterConfig = createFilterConfig({ 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( { 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({ 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) ), ]), }); diff --git a/packages/utils/src/work-item-filters/configs/filters/user.ts b/packages/utils/src/work-item-filters/configs/filters/user.ts index cae90b871..f5c1cb7e2 100644 --- a/packages/utils/src/work-item-filters/configs/filters/user.ts +++ b/packages/utils/src/work-item-filters/configs/filters/user.ts @@ -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 & { - 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( - { - 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({ 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({ 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({ 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({ 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) ), ]), });