Skip to content

Commit 7cc15f9

Browse files
committed
fix(megapixels): make Filter accept null as input value
1 parent 8b9e8d0 commit 7cc15f9

File tree

3 files changed

+83
-31
lines changed

3 files changed

+83
-31
lines changed

packages/megapixels/src/Filter/Filter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type ClassName = TVClassName<typeof filterVariants>;
5656

5757
export interface FilterValues {
5858
search?: string;
59-
filter?: string | Record<string, unknown>;
59+
filter?: string | Record<string, unknown> | null;
6060
}
6161

6262
export type FilterChildRenderFn = (values: FilterValues) => ReactNode;

packages/megapixels/src/Filter/hooks/useFilterValidation.test.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,79 @@ describe('useFilterValidation', () => {
2828
const schema = result.current;
2929

3030
const valid = schema.validate({ filter: { status: true, owners: ['a'] } });
31-
expect(valid.errors).toBeNull();
31+
expect(valid).toMatchObject({
32+
success: true,
33+
data: { filter: { status: true, owners: ['a'] } },
34+
errors: null,
35+
});
3236

3337
const invalid = schema.validate({
3438
filter: { status: 'invalid', owners: [] },
3539
});
36-
expect(invalid.errors).not.toBeNull();
40+
expect(invalid).toMatchObject({ success: false, data: null });
3741
});
3842

3943
it('validates optional search string when enabled', () => {
4044
const { result } = renderHook(() => useFilterValidation(filters, true));
4145
const schema = result.current;
4246

4347
// include valid filter content so only search is under test
44-
expect(
45-
schema.validate({ search: 'abc', filter: { owners: ['x'] } }).errors,
46-
).toBe(null);
47-
expect(
48-
schema.validate({ search: 123, filter: { owners: ['x'] } }).errors,
49-
).not.toBeNull();
50-
expect(
51-
schema.validate({
52-
search: 123,
53-
filter: JSON.stringify({ owners: ['x'] }),
54-
}).errors,
55-
).not.toBeNull();
48+
const ok = schema.validate({ search: 'abc', filter: { owners: ['x'] } });
49+
expect(ok).toMatchObject({
50+
success: true,
51+
data: { search: 'abc', filter: { owners: ['x'] } },
52+
errors: null,
53+
});
54+
55+
const invalidSearch = schema.validate({
56+
search: 123 as unknown as string,
57+
filter: { owners: ['x'] },
58+
});
59+
expect(invalidSearch).toMatchObject({ success: false, data: null });
60+
});
61+
62+
it('parses stringified JSON filter into object in output data', () => {
63+
const { result } = renderHook(() => useFilterValidation(filters, true));
64+
const schema = result.current;
65+
66+
const jsonFilter = JSON.stringify({ status: true, owners: ['x'] });
67+
const validationResult = schema.validate({
68+
search: 'ok',
69+
filter: jsonFilter,
70+
});
71+
expect(validationResult).toMatchObject({
72+
success: true,
73+
data: { search: 'ok', filter: { status: true, owners: ['x'] } },
74+
errors: null,
75+
});
76+
});
77+
78+
it('accepts when filter is null and returns undefined filter data', () => {
79+
const { result } = renderHook(() => useFilterValidation(filters, true));
80+
const schema = result.current;
81+
82+
const validationResult = schema.validate({ search: 'test', filter: null });
83+
expect(validationResult).toMatchObject({
84+
success: true,
85+
// filter should be undefined
86+
data: { filter: undefined, search: 'test' },
87+
errors: null,
88+
});
89+
});
90+
91+
it('accepts when filter is omitted and and returns undefined filter data', () => {
92+
const { result } = renderHook(() => useFilterValidation(filters, true));
93+
const schema = result.current;
94+
95+
const validationResult = schema.validate({
96+
search: 'test',
97+
filter: undefined,
98+
});
99+
expect(validationResult).toMatchObject({
100+
success: true,
101+
// filter should be undefined
102+
data: { filter: undefined, search: 'test' },
103+
errors: null,
104+
});
56105
});
57106
});

packages/megapixels/src/Filter/hooks/useFilterValidation.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import type { VetoTypeAny } from '@fuf-stack/veto';
1+
import type { VetoInstance, VetoRawShape, VetoTypeAny } from '@fuf-stack/veto';
22
import type { FilterInstance } from '../filters/types';
33

44
import { useMemo } from 'react';
55

66
import { object, string, stringToJSON, veto } from '@fuf-stack/veto';
77

8-
/** Validation function return type alias. */
9-
type ValidationSchema = ReturnType<typeof veto>;
10-
118
/**
129
* useFilterValidation
1310
*
@@ -19,28 +16,34 @@ export const useFilterValidation = (
1916
filters: FilterInstance<unknown, unknown>[],
2017
withSearch?: boolean,
2118
) => {
22-
return useMemo<ValidationSchema>(() => {
23-
let validationObject: Record<string, VetoTypeAny> = {};
24-
let filterValidation: Record<string, VetoTypeAny> = {};
25-
19+
return useMemo<VetoInstance>(() => {
20+
// build filter validation
21+
let filterSchema: Record<string, VetoTypeAny> = {};
2622
filters.forEach((f) => {
27-
filterValidation = {
28-
...filterValidation,
29-
[f.name]: f.validation(f.config),
23+
filterSchema = {
24+
...filterSchema,
25+
[f.name]: f.validation(f.config) as VetoTypeAny,
3026
};
3127
});
3228

33-
validationObject = {
29+
const validationSchema: VetoRawShape = {
30+
// filter validation
3431
filter: stringToJSON()
35-
.pipe(object(filterValidation))
36-
.or(object(filterValidation))
37-
.optional(),
32+
.pipe(object(filterSchema))
33+
.or(object(filterSchema))
34+
.optional()
35+
.nullable()
36+
// transform null to undefined
37+
.transform((val) => {
38+
return val ?? undefined;
39+
}),
40+
// optional search validation
3841
...(withSearch
3942
? { search: string({ min: 0 }).nullable().optional() }
4043
: {}),
4144
};
4245

43-
return veto(validationObject);
46+
return veto(validationSchema);
4447
}, [filters, withSearch]);
4548
};
4649

0 commit comments

Comments
 (0)