Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/plane/utils/filters/filterset.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class Meta:
"start_date": ["exact", "range"],
"target_date": ["exact", "range"],
"created_at": ["exact", "range"],
"updated_at": ["exact", "range"],
"is_draft": ["exact"],
"priority": ["exact", "in"],
}
Expand Down
15 changes: 15 additions & 0 deletions apps/web/ce/components/rich-filters/filter-value-input/root.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
);
4 changes: 4 additions & 0 deletions apps/web/ce/helpers/work-item-filters/project-level.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useMemo } from "react";
import {
AtSign,
Briefcase,
Calendar,
CalendarCheck2,
CalendarClock,
CircleUserRound,
Expand Down Expand Up @@ -32,6 +33,7 @@ import {
import { Avatar, Logo } from "@plane/ui";
import {
getAssigneeFilterConfig,
getCreatedAtFilterConfig,
getCreatedByFilterConfig,
getCycleFilterConfig,
getFileURL,
Expand All @@ -45,6 +47,8 @@ import {
getStateGroupFilterConfig,
getSubscriberFilterConfig,
getTargetDateFilterConfig,
getUpdatedAtFilterConfig,
isLoaderReady,
} from "@plane/utils";
// store hooks
import { useCycle } from "@/hooks/store/use-cycle";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
() =>
Expand All @@ -331,6 +360,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
);

return {
areAllConfigsInitialized,
configs: [
stateFilterConfig,
stateGroupFilterConfig,
Expand All @@ -343,6 +373,8 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
moduleFilterConfig,
startDateFilterConfig,
targetDateFilterConfig,
createdAtFilterConfig,
updatedAtFilterConfig,
createdByFilterConfig,
subscriberFilterConfig,
],
Expand All @@ -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 changes: 71 additions & 0 deletions apps/web/core/components/rich-filters/add-filters/button.tsx
Original file line number Diff line number Diff line change
@@ -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>
}
/>
);
}
);
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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",
Expand All @@ -89,7 +67,6 @@ export const AddFilterButton = observer(
}
};

if (isDisabled) return null;
return (
<div className="relative transition-all duration-200 ease-in-out">
<CustomSearchSelect
Expand All @@ -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>
);
Expand Down
32 changes: 32 additions & 0 deletions apps/web/core/components/rich-filters/filter-item/close-button.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
);
Loading
Loading