Skip to content

Commit a3d6ac4

Browse files
authored
Workflow history filters (#685)
* workflow history filters * add missing files * fix lint * add filter helpers * fix lint warning * fix workflow history tests * update buttons sizes * fix filter icon size
1 parent 0666adb commit a3d6ac4

32 files changed

+1048
-176
lines changed

src/components/page-filters/__fixtures__/page-filters.fixtures.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { PageQueryParam } from '@/hooks/use-page-query-params/use-page-query-params.types';
22

3+
import { type PageFilterConfig } from '../page-filters.types';
4+
35
const defaultParamA = 'valueA1';
46
const defaultParamB = 'valueB1';
57

@@ -21,3 +23,21 @@ export const mockQueryParamsValues = {
2123
paramA: defaultParamA,
2224
paramB: defaultParamB,
2325
};
26+
27+
export const mockFiltersConfig: [
28+
PageFilterConfig<typeof mockPageQueryParamConfig, { paramA: string }>,
29+
PageFilterConfig<typeof mockPageQueryParamConfig, { paramB: string }>,
30+
] = [
31+
{
32+
id: 'filterA',
33+
getValue: (v) => ({ paramA: v.paramA }),
34+
formatValue: (v) => v,
35+
component: () => 'FilterA',
36+
},
37+
{
38+
id: 'filterB',
39+
getValue: (v) => ({ paramB: v.paramB }),
40+
formatValue: (v) => v,
41+
component: () => 'FilterB',
42+
},
43+
];

src/components/page-filters/__tests__/page-filters.test.tsx

Lines changed: 15 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,65 +8,25 @@ import { type PageQueryParamValues } from '@/hooks/use-page-query-params/use-pag
88
import {
99
mockQueryParamsValues,
1010
mockPageQueryParamConfig,
11+
mockFiltersConfig,
1112
} from '../__fixtures__/page-filters.fixtures';
1213
import PageFilters from '../page-filters';
13-
import {
14-
type PageFilterComponentProps,
15-
type PageFilterConfig,
16-
} from '../page-filters.types';
14+
import { type Props as PageFiltersToggleProps } from '../page-filters-toggle/page-filters-toggle.types';
1715

1816
const mockSetQueryParams = jest.fn();
1917
jest.mock('../../../hooks/use-page-query-params/use-page-query-params', () =>
2018
jest.fn(() => [mockQueryParamsValues, mockSetQueryParams])
2119
);
2220

23-
const MockFilterA = ({
24-
value,
25-
setValue,
26-
}: PageFilterComponentProps<{ paramA: string }>) => {
27-
return (
28-
<div>
29-
<div>Value 1: {value.paramA}</div>
30-
<div
31-
data-testid="change-filters"
32-
onClick={() => setValue({ paramA: 'valueA2' })}
33-
/>
34-
</div>
35-
);
36-
};
37-
38-
const MockFilterB = ({
39-
value,
40-
setValue,
41-
}: PageFilterComponentProps<{ paramB: string }>) => {
42-
return (
43-
<div>
44-
<div>Value 2: {value.paramB}</div>
45-
<div
46-
data-testid="change-filters"
47-
onClick={() => setValue({ paramB: 'valueB2' })}
48-
/>
49-
</div>
50-
);
51-
};
52-
53-
const MOCK_FILTERS_CONFIG: [
54-
PageFilterConfig<typeof mockPageQueryParamConfig, { paramA: string }>,
55-
PageFilterConfig<typeof mockPageQueryParamConfig, { paramB: string }>,
56-
] = [
57-
{
58-
id: 'filterA',
59-
getValue: (v) => ({ paramA: v.paramA }),
60-
formatValue: (v) => v,
61-
component: MockFilterA,
62-
},
63-
{
64-
id: 'filterB',
65-
getValue: (v) => ({ paramB: v.paramB }),
66-
formatValue: (v) => v,
67-
component: MockFilterB,
68-
},
69-
];
21+
jest.mock('../page-filters-fields/page-filters-fields', () =>
22+
jest.fn(() => <div data-testid="filter-fields">Filter Fields</div>)
23+
);
24+
25+
jest.mock('../page-filters-toggle/page-filters-toggle', () =>
26+
jest.fn((props: PageFiltersToggleProps) => (
27+
<button onClick={props.onClick}>Filters Button</button>
28+
))
29+
);
7030

7131
afterEach(() => {
7232
jest.clearAllMocks();
@@ -88,43 +48,15 @@ describe('PageFilters', () => {
8848
it('should show filters when Filters button is clicked, and modify additional filters', async () => {
8949
setup({});
9050

91-
const filtersButton = await screen.findByText('Filters');
92-
93-
act(() => {
94-
fireEvent.click(filtersButton);
51+
const filtersButton = await screen.findByRole('button', {
52+
name: 'Filters Button',
9553
});
9654

97-
const filtersButtons = await screen.findAllByTestId('change-filters');
98-
expect(filtersButtons).toHaveLength(2);
99-
100-
act(() => {
101-
fireEvent.click(filtersButtons[0]);
102-
});
103-
104-
expect(mockSetQueryParams).toHaveBeenCalledWith({ paramA: 'valueA2' });
105-
});
106-
107-
it('should reset filters when clear filters button is pressed', async () => {
108-
setup({
109-
valuesOverrides: { paramA: 'valueA2', paramB: 'valueB2' },
110-
});
111-
112-
const filtersButton = await screen.findByText('Filters (2)');
113-
11455
act(() => {
11556
fireEvent.click(filtersButton);
11657
});
11758

118-
const clearFiltersButton = await screen.findByText('Clear filters');
119-
120-
act(() => {
121-
fireEvent.click(clearFiltersButton);
122-
});
123-
124-
expect(mockSetQueryParams).toHaveBeenCalledWith({
125-
paramA: undefined,
126-
paramB: undefined,
127-
});
59+
expect(screen.getByTestId('filter-fields')).toBeInTheDocument();
12860
});
12961
});
13062

@@ -148,7 +80,7 @@ function setup({
14880
<PageFilters
14981
searchQueryParamKey="search"
15082
searchPlaceholder="placeholder"
151-
pageFiltersConfig={MOCK_FILTERS_CONFIG}
83+
pageFiltersConfig={mockFiltersConfig}
15284
pageQueryParamsConfig={mockPageQueryParamConfig}
15385
/>
15486
);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useMemo, useCallback } from 'react';
2+
3+
import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params';
4+
import {
5+
type PageQueryParams,
6+
type PageQueryParamKeys,
7+
type PageQueryParamValues,
8+
} from '@/hooks/use-page-query-params/use-page-query-params.types';
9+
10+
import { type PageFilterConfig } from '../page-filters.types';
11+
12+
export default function usePageFilters<P extends PageQueryParams>({
13+
pageFiltersConfig,
14+
pageQueryParamsConfig,
15+
}: {
16+
pageFiltersConfig: Array<PageFilterConfig<P, any>>;
17+
pageQueryParamsConfig: P;
18+
}) {
19+
const [queryParams, setQueryParams] = usePageQueryParams(
20+
pageQueryParamsConfig,
21+
{ pageRerender: false }
22+
);
23+
24+
const activeFiltersCount = useMemo(() => {
25+
const configsByKey = Object.fromEntries(
26+
pageQueryParamsConfig.map((c) => [c.key, c])
27+
);
28+
return pageFiltersConfig.filter((pageFilter) =>
29+
Object.keys(pageFilter.getValue(queryParams)).every(
30+
(queryParamKey: PageQueryParamKeys<P>) =>
31+
queryParams[queryParamKey] &&
32+
queryParams[queryParamKey] !==
33+
configsByKey[queryParamKey].defaultValue
34+
)
35+
).length;
36+
}, [pageFiltersConfig, pageQueryParamsConfig, queryParams]);
37+
38+
const resetAllFilters = useCallback(() => {
39+
const emptyQueryParamsObject = pageQueryParamsConfig.reduce(
40+
(acc, config) => ({
41+
...acc,
42+
[config.key]: undefined,
43+
}),
44+
{}
45+
) as PageQueryParamValues<P>;
46+
47+
setQueryParams(
48+
pageFiltersConfig.reduce((acc, pageFilter) => {
49+
return {
50+
...acc,
51+
...pageFilter.getValue(emptyQueryParamsObject),
52+
};
53+
}, {})
54+
);
55+
}, [pageFiltersConfig, pageQueryParamsConfig, setQueryParams]);
56+
57+
return { resetAllFilters, activeFiltersCount, queryParams, setQueryParams };
58+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React from 'react';
2+
3+
import { render, screen, act, fireEvent } from '@/test-utils/rtl';
4+
5+
import { type PageQueryParamValues } from '@/hooks/use-page-query-params/use-page-query-params.types';
6+
7+
import {
8+
type mockPageQueryParamConfig,
9+
mockQueryParamsValues,
10+
} from '../../__fixtures__/page-filters.fixtures';
11+
import {
12+
type PageFilterComponentProps,
13+
type PageFilterConfig,
14+
} from '../../page-filters.types';
15+
import PageFiltersFields from '../page-filters-fields';
16+
17+
const MockFilterA = ({
18+
value,
19+
setValue,
20+
}: PageFilterComponentProps<{ paramA: string }>) => {
21+
return (
22+
<div>
23+
<div>Value 1: {value.paramA}</div>
24+
<div
25+
data-testid="change-filters"
26+
onClick={() => setValue({ paramA: 'valueA2' })}
27+
/>
28+
</div>
29+
);
30+
};
31+
32+
const MockFilterB = ({
33+
value,
34+
setValue,
35+
}: PageFilterComponentProps<{ paramB: string }>) => {
36+
return (
37+
<div>
38+
<div>Value 2: {value.paramB}</div>
39+
<div
40+
data-testid="change-filters"
41+
onClick={() => setValue({ paramB: 'valueB2' })}
42+
/>
43+
</div>
44+
);
45+
};
46+
47+
export const mockFiltersConfig: [
48+
PageFilterConfig<typeof mockPageQueryParamConfig, { paramA: string }>,
49+
PageFilterConfig<typeof mockPageQueryParamConfig, { paramB: string }>,
50+
] = [
51+
{
52+
id: 'filterA',
53+
getValue: (v) => ({ paramA: v.paramA }),
54+
formatValue: (v) => v,
55+
component: MockFilterA,
56+
},
57+
{
58+
id: 'filterB',
59+
getValue: (v) => ({ paramB: v.paramB }),
60+
formatValue: (v) => v,
61+
component: MockFilterB,
62+
},
63+
];
64+
65+
describe('PageFiltersFields', () => {
66+
it('should reset filters when clear filters button is pressed', async () => {
67+
const { mockedResetAllFilters } = setup({
68+
valuesOverrides: { paramA: 'valueA2', paramB: 'valueB2' },
69+
});
70+
71+
const clearFiltersButton = await screen.findByText('Clear filters');
72+
73+
act(() => {
74+
fireEvent.click(clearFiltersButton);
75+
});
76+
77+
expect(mockedResetAllFilters).toHaveBeenCalled();
78+
});
79+
80+
it('should call setQueryParams when filter setValue is called', async () => {
81+
const { mockedSetQueryParams } = setup({});
82+
83+
const filtersButtons = await screen.findAllByTestId('change-filters');
84+
expect(filtersButtons).toHaveLength(2);
85+
86+
act(() => {
87+
fireEvent.click(filtersButtons[0]);
88+
});
89+
90+
expect(mockedSetQueryParams).toHaveBeenCalledWith({ paramA: 'valueA2' });
91+
});
92+
});
93+
94+
function setup({
95+
valuesOverrides,
96+
}: {
97+
valuesOverrides?: Partial<
98+
PageQueryParamValues<typeof mockPageQueryParamConfig>
99+
>;
100+
}) {
101+
const mockedResetAllFilters = jest.fn();
102+
const mockedSetQueryParams = jest.fn();
103+
104+
render(
105+
<PageFiltersFields
106+
pageFiltersConfig={mockFiltersConfig}
107+
resetAllFilters={mockedResetAllFilters}
108+
setQueryParams={mockedSetQueryParams}
109+
queryParams={{ ...mockQueryParamsValues, ...valuesOverrides }}
110+
/>
111+
);
112+
return { mockedResetAllFilters, mockedSetQueryParams };
113+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { styled as createStyled, type Theme } from 'baseui';
2+
import type { ButtonOverrides } from 'baseui/button';
3+
import { type StyleObject } from 'styletron-react';
4+
5+
export const styled = {
6+
SearchFiltersContainer: createStyled(
7+
'div',
8+
({ $theme }: { $theme: Theme }) => ({
9+
display: 'flex',
10+
flexDirection: 'column',
11+
flexWrap: 'wrap',
12+
gap: $theme.sizing.scale500,
13+
marginBottom: $theme.sizing.scale700,
14+
[$theme.mediaQuery.medium]: {
15+
flexDirection: 'row',
16+
},
17+
})
18+
),
19+
SearchFilterContainer: createStyled('div', {
20+
flexGrow: 2,
21+
flexBasis: 0,
22+
}),
23+
};
24+
25+
export const overrides = {
26+
clearFiltersButton: {
27+
Root: {
28+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
29+
whiteSpace: 'nowrap',
30+
flexGrow: 2,
31+
height: $theme.sizing.scale950,
32+
[$theme.mediaQuery.medium]: {
33+
flexGrow: 0,
34+
alignSelf: 'flex-end',
35+
marginTop: $theme.sizing.scale700,
36+
},
37+
}),
38+
},
39+
} satisfies ButtonOverrides,
40+
};

0 commit comments

Comments
 (0)