Skip to content

Commit a3ff051

Browse files
committed
fix: fixes tsdx lint issues, adds 98%+ coverage, copy useList src code into repository and remove react-use
1 parent 4820f89 commit a3ff051

File tree

10 files changed

+381
-228
lines changed

10 files changed

+381
-228
lines changed

.eslintrc.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
"extends": [
3+
"react-app",
4+
"prettier/@typescript-eslint",
5+
"plugin:prettier/recommended"
6+
],
7+
"settings": {
8+
"react": {
9+
"version": "detect"
10+
}
11+
},
12+
"rules": {
13+
"no-redeclare": "off",
14+
}
15+
}

.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"jsxBracketSameLine": true,
3+
"printWidth": 70,
4+
"singleQuote": true,
5+
"trailingComma": "es5"
6+
}

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
"singleQuote": true,
4141
"trailingComma": "es5"
4242
},
43+
"resolutions": {
44+
"**/@typescript-eslint/eslint-plugin": "^4.1.1",
45+
"**/@typescript-eslint/parser": "^4.1.1"
46+
},
4347
"name": "react-query-filter",
4448
"author": "Armando Magalhães",
4549
"module": "dist/react-query-filter.esm.js",
@@ -81,10 +85,9 @@
8185
"size-limit": "^4.9.1",
8286
"tsdx": "^0.14.1",
8387
"tslib": "^2.0.3",
84-
"typescript": "^4.1.3"
88+
"typescript": "^4.3.2"
8589
},
8690
"dependencies": {
87-
"react-use": "^15.3.4",
8891
"uuid": "^8.3.2"
8992
},
9093
"repository": {

src/hooks/misc/hookState.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export type IHookStateInitialSetter<S> = () => S;
2+
export type IHookStateInitAction<S> = S | IHookStateInitialSetter<S>;
3+
4+
export type IHookStateSetter<S> = ((prevState: S) => S) | (() => S);
5+
export type IHookStateSetAction<S> = S | IHookStateSetter<S>;
6+
7+
export type IHookStateResolvable<S> =
8+
| S
9+
| IHookStateInitialSetter<S>
10+
| IHookStateSetter<S>;
11+
12+
export function resolveHookState<S>(nextState: IHookStateInitAction<S>): S;
13+
export function resolveHookState<S, C extends S>(
14+
nextState: IHookStateSetAction<S>,
15+
currentState?: C
16+
): S;
17+
export function resolveHookState<S, C extends S>(
18+
nextState: IHookStateResolvable<S>,
19+
currentState?: C
20+
): S;
21+
export function resolveHookState<S, C extends S>(
22+
nextState: IHookStateResolvable<S>,
23+
currentState?: C
24+
): S {
25+
if (typeof nextState === 'function') {
26+
return nextState.length
27+
? (nextState as Function)(currentState)
28+
: (nextState as Function)();
29+
}
30+
31+
return nextState;
32+
}

src/hooks/useList.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { useMemo, useRef } from 'react';
2+
import useUpdate from './useUpdate';
3+
import {
4+
IHookStateInitAction,
5+
IHookStateSetAction,
6+
resolveHookState,
7+
} from './misc/hookState';
8+
9+
export interface ListActions<T> {
10+
/**
11+
* @description Set new list instead old one
12+
*/
13+
set: (newList: IHookStateSetAction<T[]>) => void;
14+
/**
15+
* @description Add item(s) at the end of list
16+
*/
17+
push: (...items: T[]) => void;
18+
19+
/**
20+
* @description Replace item at given position. If item at given position not exists it will be set.
21+
*/
22+
updateAt: (index: number, item: T) => void;
23+
/**
24+
* @description Insert item at given position, all items to the right will be shifted.
25+
*/
26+
insertAt: (index: number, item: T) => void;
27+
28+
/**
29+
* @description Replace all items that matches predicate with given one.
30+
*/
31+
update: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
32+
/**
33+
* @description Replace first item matching predicate with given one.
34+
*/
35+
updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
36+
/**
37+
* @description Like `updateFirst` bit in case of predicate miss - pushes item to the list
38+
*/
39+
upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
40+
41+
/**
42+
* @description Sort list with given sorting function
43+
*/
44+
sort: (compareFn?: (a: T, b: T) => number) => void;
45+
/**
46+
* @description Same as native Array's method
47+
*/
48+
filter: (
49+
callbackFn: (value: T, index?: number, array?: T[]) => boolean,
50+
thisArg?: any
51+
) => void;
52+
53+
/**
54+
* @description Removes item at given position. All items to the right from removed will be shifted.
55+
*/
56+
removeAt: (index: number) => void;
57+
/**
58+
* @deprecated Use removeAt method instead
59+
*/
60+
remove: (index: number) => void;
61+
62+
/**
63+
* @description Make the list empty
64+
*/
65+
clear: () => void;
66+
/**
67+
* @description Reset list to initial value
68+
*/
69+
reset: () => void;
70+
}
71+
72+
export function useList<T>(
73+
initialList: IHookStateInitAction<T[]> = []
74+
): [T[], ListActions<T>] {
75+
const list = useRef(resolveHookState(initialList));
76+
const update = useUpdate();
77+
78+
const actions = useMemo<ListActions<T>>(() => {
79+
const a = {
80+
set: (newList: IHookStateSetAction<T[]>) => {
81+
list.current = resolveHookState(newList, list.current);
82+
update();
83+
},
84+
85+
push: (...items: T[]) => {
86+
items.length && actions.set((curr: T[]) => curr.concat(items));
87+
},
88+
89+
updateAt: (index: number, item: T) => {
90+
actions.set((curr: T[]) => {
91+
const arr = curr.slice();
92+
93+
arr[index] = item;
94+
95+
return arr;
96+
});
97+
},
98+
99+
insertAt: (index: number, item: T) => {
100+
actions.set((curr: T[]) => {
101+
const arr = curr.slice();
102+
103+
index > arr.length ? (arr[index] = item) : arr.splice(index, 0, item);
104+
105+
return arr;
106+
});
107+
},
108+
109+
update: (predicate: (a: T, b: T) => boolean, newItem: T) => {
110+
actions.set((curr: T[]) =>
111+
curr.map(item => (predicate(item, newItem) ? newItem : item))
112+
);
113+
},
114+
115+
updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => {
116+
const index = list.current.findIndex(item => predicate(item, newItem));
117+
118+
index >= 0 && actions.updateAt(index, newItem);
119+
},
120+
121+
upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => {
122+
const index = list.current.findIndex(item => predicate(item, newItem));
123+
124+
index >= 0 ? actions.updateAt(index, newItem) : actions.push(newItem);
125+
},
126+
127+
sort: (compareFn?: (a: T, b: T) => number) => {
128+
actions.set((curr: T[]) => curr.slice().sort(compareFn));
129+
},
130+
131+
filter: <S extends T>(
132+
callbackFn: (value: T, index: number, array: T[]) => value is S,
133+
thisArg?: any
134+
) => {
135+
actions.set((curr: T[]) => curr.slice().filter(callbackFn, thisArg));
136+
},
137+
138+
removeAt: (index: number) => {
139+
actions.set((curr: T[]) => {
140+
const arr = curr.slice();
141+
142+
arr.splice(index, 1);
143+
144+
return arr;
145+
});
146+
},
147+
148+
clear: () => {
149+
actions.set([]);
150+
},
151+
152+
reset: () => {
153+
actions.set(resolveHookState(initialList).slice());
154+
},
155+
};
156+
157+
return a as ListActions<T>;
158+
/* eslint-disable react-hooks/exhaustive-deps */
159+
}, []);
160+
161+
return [list.current, actions];
162+
}

src/hooks/useQueryFilters.test.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
defaultTypeOperationsMap,
55
defaultOperationLabels,
66
mapOperationToSelectOption,
7+
OperationType,
78
} from '../operations';
89
import { useQueryFilters, HookProps } from './useQueryFilters';
910

@@ -106,6 +107,7 @@ describe('useQueryFilters', () => {
106107
const updatedRowProps = result.current.createFilterRowProps(0);
107108

108109
expect(updatedRowProps.filter.binding).toBe(undefined);
110+
expect(updatedRowProps.shouldRenderBindingSelect).toBeFalsy();
109111
});
110112

111113
it('should set the binding if the filter being is not the first one', () => {
@@ -129,8 +131,6 @@ describe('useQueryFilters', () => {
129131
expect(updatedRowProps.filter.binding).toBe(orBinding.value);
130132
});
131133

132-
it.skip('should return the proper operation options based on the type of the field selected in the filter', () => {});
133-
134134
it('should remove an existing row', () => {
135135
const result = createTestFilter();
136136
const rowProps = result.current.createFilterRowProps(0);
@@ -157,6 +157,7 @@ describe('useQueryFilters', () => {
157157
});
158158

159159
const updatedRowProps = result.current.createFilterRowProps(0);
160+
160161
expect(updatedRowProps.filter.value).toBe(changeEvent.target.value);
161162
});
162163

@@ -167,7 +168,7 @@ describe('useQueryFilters', () => {
167168
const nextField = fields.find(field => field.value === 'has_owner');
168169

169170
if (!nextField) {
170-
throw new Error("Failed to find owner field");
171+
throw new Error('Failed to find owner field');
171172
}
172173

173174
act(() => {
@@ -192,5 +193,23 @@ describe('useQueryFilters', () => {
192193

193194
expect(updatedRowProps.filter.value).toBe(changeEvent.target.checked);
194195
});
196+
197+
it('should clear the filter value when the operation does not require a value to be set', () => {
198+
const result = createTestFilter();
199+
const initialRowProps = result.current.createFilterRowProps(0);
200+
const emptyOperation = mapOperationToSelectOption(
201+
OperationType.IS_EMPTY,
202+
defaultOperationLabels
203+
);
204+
205+
act(() => {
206+
initialRowProps.selectStates.onChangeOperation(emptyOperation);
207+
});
208+
209+
const updatedRowProps = result.current.createFilterRowProps(0);
210+
211+
expect(updatedRowProps.filter.value).toBeUndefined();
212+
expect(updatedRowProps.shouldRenderValueInput).toBeFalsy();
213+
});
195214
});
196215
});

src/hooks/useQueryFilters.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { useEffect } from 'react';
2-
3-
/** TODO: Extract useList from react-use so we can eliminate it from the dependency tree **/
4-
import { useList } from 'react-use';
52
import { v4 as uuidv4 } from 'uuid';
3+
import { useList } from './useList';
64
import { Binding, defaultBindingOptions } from '../bindings';
75
import {
86
OperationType,
@@ -97,9 +95,10 @@ export const useQueryFilters = ({
9795
value: property.key,
9896
}));
9997

98+
// TODO: make this a component property
10099
const bindings = defaultBindingOptions;
101100

102-
const getFieldType = (fieldKey?: string) => {
101+
const getFieldType = (fieldKey: string) => {
103102
return properties.find(prop => prop.key === fieldKey)?.type;
104103
};
105104

@@ -167,9 +166,7 @@ export const useQueryFilters = ({
167166
});
168167
},
169168
onChangeOperation: operation => {
170-
const shouldClearValue = operation
171-
? noValueOperations.includes(operation.value)
172-
: false;
169+
const shouldClearValue = noValueOperations.includes(operation.value);
173170

174171
selectStateActions.updateAt(index, {
175172
...selectState,

src/hooks/useUpdate.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useReducer } from 'react';
2+
3+
const updateReducer = (num: number): number => (num + 1) % 1_000_000;
4+
5+
export default function useUpdate(): () => void {
6+
const [, update] = useReducer(updateReducer, 0);
7+
8+
return update;
9+
}

src/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface BooleanPropertyDescription {
2020
label: string;
2121
key: string;
2222
type: 'boolean';
23-
suggestions?: [true, false],
23+
suggestions?: [true, false];
2424
}
2525

2626
export type PropertyDescription =
@@ -38,9 +38,9 @@ export interface Filter {
3838
}
3939

4040
export interface FilterSelectState {
41-
field?: SelectOption<string>;
42-
operation?: SelectOption<OperationType>;
43-
binding?: SelectOption<Binding>;
41+
field?: SelectOption<string>;
42+
operation?: SelectOption<OperationType>;
43+
binding?: SelectOption<Binding>;
4444
fieldIndex?: number;
4545
operationIndex?: number;
4646
bindingIndex?: number;
@@ -57,7 +57,7 @@ export interface FilterRowProps {
5757
onChangeBinding: (selectedBinding: SelectOption<Binding>) => void;
5858
/** onChange handler for the `operation` property of the current row filter. */
5959
onChangeOperation: (selectedOperation: SelectOption<OperationType>) => void;
60-
}
60+
};
6161
/** List of Select Options for available fields. */
6262
fields: SelectOption<string>[];
6363
/** List of Select Options for available bindings. */

0 commit comments

Comments
 (0)