mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: date range selector (#8991)
This commit is contained in:
		
							parent
							
								
									45035102f4
								
							
						
					
					
						commit
						da16b316aa
					
				@ -0,0 +1,81 @@
 | 
			
		||||
import {
 | 
			
		||||
    Box,
 | 
			
		||||
    List,
 | 
			
		||||
    ListItem,
 | 
			
		||||
    ListItemButton,
 | 
			
		||||
    styled,
 | 
			
		||||
    Typography,
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
import type { FilterItemParams } from '../../filter/FilterItem/FilterItem';
 | 
			
		||||
import type { FC } from 'react';
 | 
			
		||||
import { calculateDateRange, type RangeType } from './calculateDateRange';
 | 
			
		||||
 | 
			
		||||
export const PresetsHeader = styled(Typography)(({ theme }) => ({
 | 
			
		||||
    paddingLeft: theme.spacing(2),
 | 
			
		||||
    paddingBottom: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const DateRangePresets: FC<{
 | 
			
		||||
    onRangeChange: (value: {
 | 
			
		||||
        from: FilterItemParams;
 | 
			
		||||
        to: FilterItemParams;
 | 
			
		||||
    }) => void;
 | 
			
		||||
}> = ({ onRangeChange }) => {
 | 
			
		||||
    const rangeChangeHandler = (rangeType: RangeType) => () => {
 | 
			
		||||
        const [start, end] = calculateDateRange(rangeType);
 | 
			
		||||
        onRangeChange({
 | 
			
		||||
            from: {
 | 
			
		||||
                operator: 'IS',
 | 
			
		||||
                values: [start],
 | 
			
		||||
            },
 | 
			
		||||
            to: {
 | 
			
		||||
                operator: 'IS',
 | 
			
		||||
                values: [end],
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Box>
 | 
			
		||||
            <PresetsHeader variant='h3'>Presets</PresetsHeader>
 | 
			
		||||
            <List disablePadding sx={{ pb: 2 }}>
 | 
			
		||||
                <ListItem disablePadding>
 | 
			
		||||
                    <ListItemButton onClick={rangeChangeHandler('thisMonth')}>
 | 
			
		||||
                        This month
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
                <ListItem disablePadding>
 | 
			
		||||
                    <ListItemButton
 | 
			
		||||
                        onClick={rangeChangeHandler('previousMonth')}
 | 
			
		||||
                    >
 | 
			
		||||
                        Previous month
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
                <ListItem disablePadding>
 | 
			
		||||
                    <ListItemButton onClick={rangeChangeHandler('thisQuarter')}>
 | 
			
		||||
                        This quarter
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
                <ListItem disablePadding>
 | 
			
		||||
                    <ListItemButton
 | 
			
		||||
                        onClick={rangeChangeHandler('previousQuarter')}
 | 
			
		||||
                    >
 | 
			
		||||
                        Previous quarter
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
                <ListItem disablePadding>
 | 
			
		||||
                    <ListItemButton onClick={rangeChangeHandler('thisYear')}>
 | 
			
		||||
                        This year
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
                <ListItem disablePadding>
 | 
			
		||||
                    <ListItemButton
 | 
			
		||||
                        onClick={rangeChangeHandler('previousYear')}
 | 
			
		||||
                    >
 | 
			
		||||
                        Previous year
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
            </List>
 | 
			
		||||
        </Box>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -8,12 +8,17 @@ import { format } from 'date-fns';
 | 
			
		||||
import { useLocationSettings } from 'hooks/useLocationSettings';
 | 
			
		||||
import { getLocalizedDateString } from '../util';
 | 
			
		||||
import type { FilterItemParams } from 'component/filter/FilterItem/FilterItem';
 | 
			
		||||
import { DateRangePresets } from './DateRangePresets';
 | 
			
		||||
 | 
			
		||||
export interface IFilterDateItemProps {
 | 
			
		||||
    name: string;
 | 
			
		||||
    label: ReactNode;
 | 
			
		||||
    onChange: (value: FilterItemParams) => void;
 | 
			
		||||
    onChipClose: () => void;
 | 
			
		||||
    onRangeChange?: (value: {
 | 
			
		||||
        from: FilterItemParams;
 | 
			
		||||
        to: FilterItemParams;
 | 
			
		||||
    }) => void;
 | 
			
		||||
    onChipClose?: () => void;
 | 
			
		||||
    state: FilterItemParams | null | undefined;
 | 
			
		||||
    operators: [string, ...string[]];
 | 
			
		||||
}
 | 
			
		||||
@ -22,6 +27,7 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
 | 
			
		||||
    name,
 | 
			
		||||
    label,
 | 
			
		||||
    onChange,
 | 
			
		||||
    onRangeChange,
 | 
			
		||||
    onChipClose,
 | 
			
		||||
    state,
 | 
			
		||||
    operators,
 | 
			
		||||
@ -54,11 +60,13 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
 | 
			
		||||
        : [];
 | 
			
		||||
    const selectedDate = state ? new Date(state.values[0]) : null;
 | 
			
		||||
    const currentOperator = state ? state.operator : operators[0];
 | 
			
		||||
    const onDelete = () => {
 | 
			
		||||
    const onDelete = onChipClose
 | 
			
		||||
        ? () => {
 | 
			
		||||
              onChange({ operator: operators[0], values: [] });
 | 
			
		||||
              onClose();
 | 
			
		||||
              onChipClose();
 | 
			
		||||
    };
 | 
			
		||||
          }
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (state && !operators.includes(state.operator)) {
 | 
			
		||||
@ -115,6 +123,9 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
 | 
			
		||||
                            });
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                    {onRangeChange && (
 | 
			
		||||
                        <DateRangePresets onRangeChange={onRangeChange} />
 | 
			
		||||
                    )}
 | 
			
		||||
                </LocalizationProvider>
 | 
			
		||||
            </StyledPopover>
 | 
			
		||||
        </>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,40 @@
 | 
			
		||||
import { calculateDateRange, type RangeType } from './calculateDateRange';
 | 
			
		||||
 | 
			
		||||
describe('calculateDateRange', () => {
 | 
			
		||||
    const fixedDate = new Date('2024-06-16');
 | 
			
		||||
 | 
			
		||||
    test.each<[RangeType, string, string]>([
 | 
			
		||||
        ['thisMonth', '2024-06-01', '2024-06-30'],
 | 
			
		||||
        ['previousMonth', '2024-05-01', '2024-05-31'],
 | 
			
		||||
        ['thisQuarter', '2024-04-01', '2024-06-30'],
 | 
			
		||||
        ['previousQuarter', '2024-01-01', '2024-03-31'],
 | 
			
		||||
        ['thisYear', '2024-01-01', '2024-12-31'],
 | 
			
		||||
        ['previousYear', '2023-01-01', '2023-12-31'],
 | 
			
		||||
    ])(
 | 
			
		||||
        'should return correct range for %s',
 | 
			
		||||
        (rangeType, expectedStart, expectedEnd) => {
 | 
			
		||||
            const [start, end] = calculateDateRange(rangeType, fixedDate);
 | 
			
		||||
            expect(start).toBe(expectedStart);
 | 
			
		||||
            expect(end).toBe(expectedEnd);
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    test('should default to previousMonth if rangeType is invalid', () => {
 | 
			
		||||
        const [start, end] = calculateDateRange(
 | 
			
		||||
            'invalidRange' as RangeType,
 | 
			
		||||
            fixedDate,
 | 
			
		||||
        );
 | 
			
		||||
        expect(start).toBe('2024-05-01');
 | 
			
		||||
        expect(end).toBe('2024-05-31');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should handle edge case for previousMonth at year boundary', () => {
 | 
			
		||||
        const yearBoundaryDate = new Date('2024-01-15');
 | 
			
		||||
        const [start, end] = calculateDateRange(
 | 
			
		||||
            'previousMonth',
 | 
			
		||||
            yearBoundaryDate,
 | 
			
		||||
        );
 | 
			
		||||
        expect(start).toBe('2023-12-01');
 | 
			
		||||
        expect(end).toBe('2023-12-31');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,66 @@
 | 
			
		||||
import {
 | 
			
		||||
    endOfMonth,
 | 
			
		||||
    endOfQuarter,
 | 
			
		||||
    endOfYear,
 | 
			
		||||
    format,
 | 
			
		||||
    startOfMonth,
 | 
			
		||||
    startOfQuarter,
 | 
			
		||||
    startOfYear,
 | 
			
		||||
    subMonths,
 | 
			
		||||
    subQuarters,
 | 
			
		||||
    subYears,
 | 
			
		||||
} from 'date-fns';
 | 
			
		||||
 | 
			
		||||
export type RangeType =
 | 
			
		||||
    | 'thisMonth'
 | 
			
		||||
    | 'previousMonth'
 | 
			
		||||
    | 'thisQuarter'
 | 
			
		||||
    | 'previousQuarter'
 | 
			
		||||
    | 'thisYear'
 | 
			
		||||
    | 'previousYear';
 | 
			
		||||
 | 
			
		||||
export const calculateDateRange = (
 | 
			
		||||
    rangeType: RangeType,
 | 
			
		||||
    today = new Date(),
 | 
			
		||||
): [string, string] => {
 | 
			
		||||
    let start: Date;
 | 
			
		||||
    let end: Date;
 | 
			
		||||
 | 
			
		||||
    switch (rangeType) {
 | 
			
		||||
        case 'thisMonth': {
 | 
			
		||||
            start = startOfMonth(today);
 | 
			
		||||
            end = endOfMonth(today);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'thisQuarter': {
 | 
			
		||||
            start = startOfQuarter(today);
 | 
			
		||||
            end = endOfQuarter(today);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'previousQuarter': {
 | 
			
		||||
            const previousQuarter = subQuarters(today, 1);
 | 
			
		||||
            start = startOfQuarter(previousQuarter);
 | 
			
		||||
            end = endOfQuarter(previousQuarter);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'thisYear': {
 | 
			
		||||
            start = startOfYear(today);
 | 
			
		||||
            end = endOfYear(today);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case 'previousYear': {
 | 
			
		||||
            const lastYear = subYears(today, 1);
 | 
			
		||||
            start = startOfYear(lastYear);
 | 
			
		||||
            end = endOfYear(lastYear);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        default: {
 | 
			
		||||
            const lastMonth = subMonths(today, 1);
 | 
			
		||||
            start = startOfMonth(lastMonth);
 | 
			
		||||
            end = endOfMonth(lastMonth);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return [format(start, 'yyyy-MM-dd'), format(end, 'yyyy-MM-dd')];
 | 
			
		||||
};
 | 
			
		||||
@ -57,6 +57,9 @@ export const useEventLogFilters = (
 | 
			
		||||
                options: [],
 | 
			
		||||
                filterKey: 'from',
 | 
			
		||||
                dateOperators: ['IS'],
 | 
			
		||||
                fromFilterKey: 'from',
 | 
			
		||||
                toFilterKey: 'to',
 | 
			
		||||
                persistent: true,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                label: 'Date To',
 | 
			
		||||
@ -64,6 +67,9 @@ export const useEventLogFilters = (
 | 
			
		||||
                options: [],
 | 
			
		||||
                filterKey: 'to',
 | 
			
		||||
                dateOperators: ['IS'],
 | 
			
		||||
                fromFilterKey: 'from',
 | 
			
		||||
                toFilterKey: 'to',
 | 
			
		||||
                persistent: true,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                label: 'Created by',
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import mapValues from 'lodash.mapvalues';
 | 
			
		||||
import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch';
 | 
			
		||||
import type { SearchEventsParams } from 'openapi';
 | 
			
		||||
import type { FilterItemParamHolder } from 'component/filter/Filters/Filters';
 | 
			
		||||
import { format, subMonths } from 'date-fns';
 | 
			
		||||
 | 
			
		||||
type Log =
 | 
			
		||||
    | { type: 'global' }
 | 
			
		||||
@ -60,8 +61,14 @@ export const useEventLogSearch = (
 | 
			
		||||
        offset: withDefault(NumberParam, 0),
 | 
			
		||||
        limit: withDefault(NumberParam, DEFAULT_PAGE_SIZE),
 | 
			
		||||
        query: StringParam,
 | 
			
		||||
        from: FilterItemParam,
 | 
			
		||||
        to: FilterItemParam,
 | 
			
		||||
        from: withDefault(FilterItemParam, {
 | 
			
		||||
            values: [format(subMonths(new Date(), 1), 'yyyy-MM-dd')],
 | 
			
		||||
            operator: 'IS',
 | 
			
		||||
        }),
 | 
			
		||||
        to: withDefault(FilterItemParam, {
 | 
			
		||||
            values: [format(new Date(), 'yyyy-MM-dd')],
 | 
			
		||||
            operator: 'IS',
 | 
			
		||||
        }),
 | 
			
		||||
        createdBy: FilterItemParam,
 | 
			
		||||
        type: FilterItemParam,
 | 
			
		||||
        ...extraParameters(logType),
 | 
			
		||||
@ -81,6 +88,7 @@ export const useEventLogSearch = (
 | 
			
		||||
    const [tableState, setTableState] = usePersistentTableState(
 | 
			
		||||
        fullStorageKey,
 | 
			
		||||
        stateConfig,
 | 
			
		||||
        ['from', 'to', 'offset'],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const filterState = (() => {
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ const StyledLabel = styled('div')(({ theme }) => ({
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    fontWeight: theme.typography.fontWeightBold,
 | 
			
		||||
    minHeight: theme.spacing(3.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledOptions = styled('button')(({ theme }) => ({
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,9 @@ type ITextFilterItem = IBaseFilterItem & {
 | 
			
		||||
 | 
			
		||||
type IDateFilterItem = IBaseFilterItem & {
 | 
			
		||||
    dateOperators: [string, ...string[]];
 | 
			
		||||
    fromFilterKey?: string;
 | 
			
		||||
    toFilterKey?: string;
 | 
			
		||||
    persistent?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type IFilterItem = ITextFilterItem | IDateFilterItem;
 | 
			
		||||
@ -116,6 +119,22 @@ export const Filters: FC<IFilterProps> = ({
 | 
			
		||||
    }, [JSON.stringify(state), JSON.stringify(availableFilters)]);
 | 
			
		||||
 | 
			
		||||
    const hasAvailableFilters = unselectedFilters.length > 0;
 | 
			
		||||
 | 
			
		||||
    const rangeChangeHandler = (filter: IDateFilterItem) => {
 | 
			
		||||
        const fromKey = filter.fromFilterKey;
 | 
			
		||||
        const toKey = filter.toFilterKey;
 | 
			
		||||
        if (fromKey && toKey) {
 | 
			
		||||
            return (value: {
 | 
			
		||||
                from: FilterItemParams;
 | 
			
		||||
                to: FilterItemParams;
 | 
			
		||||
            }) => {
 | 
			
		||||
                onChange({ [fromKey]: value.from });
 | 
			
		||||
                onChange({ [toKey]: value.to });
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        return undefined;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledBox className={className}>
 | 
			
		||||
            {selectedFilters.map((selectedFilter) => {
 | 
			
		||||
@ -143,11 +162,16 @@ export const Filters: FC<IFilterProps> = ({
 | 
			
		||||
                            label={label}
 | 
			
		||||
                            name={filter.label}
 | 
			
		||||
                            state={state[filter.filterKey]}
 | 
			
		||||
                            onChange={(value) =>
 | 
			
		||||
                                onChange({ [filter.filterKey]: value })
 | 
			
		||||
                            }
 | 
			
		||||
                            onChange={(value) => {
 | 
			
		||||
                                onChange({ [filter.filterKey]: value });
 | 
			
		||||
                            }}
 | 
			
		||||
                            onRangeChange={rangeChangeHandler(filter)}
 | 
			
		||||
                            operators={filter.dateOperators}
 | 
			
		||||
                            onChipClose={() => deselectFilter(filter.label)}
 | 
			
		||||
                            onChipClose={
 | 
			
		||||
                                filter.persistent
 | 
			
		||||
                                    ? undefined
 | 
			
		||||
                                    : () => deselectFilter(filter.label)
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -33,9 +33,6 @@ test('Filter insights by project and date', async () => {
 | 
			
		||||
    render(<Insights withCharts={false} />);
 | 
			
		||||
    const addFilter = await screen.findByText('Add Filter');
 | 
			
		||||
    fireEvent.click(addFilter);
 | 
			
		||||
 | 
			
		||||
    const dateFromFilter = await screen.findByText('Date From');
 | 
			
		||||
    await screen.findByText('Date To');
 | 
			
		||||
    const projectFilter = await screen.findByText('Project');
 | 
			
		||||
 | 
			
		||||
    // filter by project
 | 
			
		||||
@ -45,11 +42,17 @@ test('Filter insights by project and date', async () => {
 | 
			
		||||
    await fireEvent.click(projectName);
 | 
			
		||||
    expect(window.location.href).toContain('project=IS%3AprojectB');
 | 
			
		||||
 | 
			
		||||
    // filter by from date
 | 
			
		||||
    fireEvent.click(dateFromFilter);
 | 
			
		||||
    const day = await screen.findByText('25');
 | 
			
		||||
    fireEvent.click(day);
 | 
			
		||||
    // last month moving window by default
 | 
			
		||||
    const fromDate = await screen.findByText('03/25/2024');
 | 
			
		||||
    await screen.findByText('04/25/2024');
 | 
			
		||||
 | 
			
		||||
    // change dates by preset range
 | 
			
		||||
    fireEvent.click(fromDate);
 | 
			
		||||
    const previousMonth = await screen.findByText('Previous month');
 | 
			
		||||
    fireEvent.click(previousMonth);
 | 
			
		||||
    await screen.findByText('03/01/2024');
 | 
			
		||||
    await screen.findByText('03/31/2024');
 | 
			
		||||
    expect(window.location.href).toContain(
 | 
			
		||||
        'project=IS%3AprojectB&from=IS%3A2024-04-25',
 | 
			
		||||
        '?project=IS%3AprojectB&from=IS%3A2024-03-01&to=IS%3A2024-03-31',
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,8 @@ import { InsightsCharts } from './InsightsCharts';
 | 
			
		||||
import { Sticky } from 'component/common/Sticky/Sticky';
 | 
			
		||||
import { InsightsFilters } from './InsightsFilters';
 | 
			
		||||
import { FilterItemParam } from '../../utils/serializeQueryParams';
 | 
			
		||||
import { format, subMonths } from 'date-fns';
 | 
			
		||||
import { withDefault } from 'use-query-params';
 | 
			
		||||
 | 
			
		||||
const StyledWrapper = styled('div')(({ theme }) => ({
 | 
			
		||||
    paddingTop: theme.spacing(2),
 | 
			
		||||
@ -32,10 +34,20 @@ export const Insights: FC<InsightsProps> = ({ withCharts = true }) => {
 | 
			
		||||
 | 
			
		||||
    const stateConfig = {
 | 
			
		||||
        project: FilterItemParam,
 | 
			
		||||
        from: FilterItemParam,
 | 
			
		||||
        to: FilterItemParam,
 | 
			
		||||
        from: withDefault(FilterItemParam, {
 | 
			
		||||
            values: [format(subMonths(new Date(), 1), 'yyyy-MM-dd')],
 | 
			
		||||
            operator: 'IS',
 | 
			
		||||
        }),
 | 
			
		||||
        to: withDefault(FilterItemParam, {
 | 
			
		||||
            values: [format(new Date(), 'yyyy-MM-dd')],
 | 
			
		||||
            operator: 'IS',
 | 
			
		||||
        }),
 | 
			
		||||
    };
 | 
			
		||||
    const [state, setState] = usePersistentTableState('insights', stateConfig);
 | 
			
		||||
    const [state, setState] = usePersistentTableState('insights', stateConfig, [
 | 
			
		||||
        'from',
 | 
			
		||||
        'to',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const { insights, loading } = useInsights(
 | 
			
		||||
        state.from?.values[0],
 | 
			
		||||
        state.to?.values[0],
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,9 @@ export const InsightsFilters: FC<IFeatureToggleFiltersProps> = ({
 | 
			
		||||
                options: [],
 | 
			
		||||
                filterKey: 'from',
 | 
			
		||||
                dateOperators: ['IS'],
 | 
			
		||||
                fromFilterKey: 'from',
 | 
			
		||||
                toFilterKey: 'to',
 | 
			
		||||
                persistent: true,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                label: 'Date To',
 | 
			
		||||
@ -41,6 +44,9 @@ export const InsightsFilters: FC<IFeatureToggleFiltersProps> = ({
 | 
			
		||||
                options: [],
 | 
			
		||||
                filterKey: 'to',
 | 
			
		||||
                dateOperators: ['IS'],
 | 
			
		||||
                fromFilterKey: 'from',
 | 
			
		||||
                toFilterKey: 'to',
 | 
			
		||||
                persistent: true,
 | 
			
		||||
            },
 | 
			
		||||
            ...(hasMultipleProjects
 | 
			
		||||
                ? ([
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user