@squiz/generic-browser-lib 1.66.0 → 1.67.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/Hooks/useAsync.d.ts +3 -1
- package/lib/Hooks/useAsync.js +13 -5
- package/lib/ResourceItem/ResourceItem.d.ts +2 -1
- package/lib/ResourceItem/ResourceItem.js +2 -2
- package/package.json +1 -1
- package/src/Hooks/useAsync.spec.ts +3 -2
- package/src/Hooks/useAsync.ts +17 -6
- package/src/ResourceItem/ResourceItem.tsx +3 -0
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# @squiz/generic-browser-lib
|
2
2
|
|
3
|
+
## 1.67.1
|
4
|
+
|
5
|
+
### Patch Changes
|
6
|
+
|
7
|
+
- e60bb77: Added ID attribute to component buttons
|
8
|
+
|
9
|
+
## 1.67.0
|
10
|
+
|
11
|
+
### Minor Changes
|
12
|
+
|
13
|
+
- 7c324d7: Added ignorePrevious option to useAsync hook (to ignore previous data)
|
14
|
+
|
3
15
|
## 1.66.0
|
4
16
|
|
5
17
|
### Minor Changes
|
package/lib/Hooks/useAsync.d.ts
CHANGED
@@ -4,6 +4,8 @@ export type UseAsyncProps<TReturnType, TDefaultValueType> = {
|
|
4
4
|
callback: (() => TReturnType | Promise<TReturnType>) | Array<() => TReturnType | Promise<TReturnType>>;
|
5
5
|
/** The default value to populate the data as when initially mounted or reloading data. */
|
6
6
|
defaultValue: TReturnType | TDefaultValueType;
|
7
|
+
/** Optional toggle to ignore previous async callback data */
|
8
|
+
ignorePrevious?: boolean;
|
7
9
|
};
|
8
10
|
/**
|
9
11
|
* Hook for invoking async code and keeping track of its state.
|
@@ -13,7 +15,7 @@ export type UseAsyncProps<TReturnType, TDefaultValueType> = {
|
|
13
15
|
* 2. When any of the `deps` change.
|
14
16
|
* 3. When the `reload` function is called.
|
15
17
|
*/
|
16
|
-
export declare const useAsync: <TReturnType, TDefaultValueType>({ callback, defaultValue }: UseAsyncProps<TReturnType, TDefaultValueType>, deps: DependencyList) => {
|
18
|
+
export declare const useAsync: <TReturnType, TDefaultValueType>({ callback, defaultValue, ignorePrevious }: UseAsyncProps<TReturnType, TDefaultValueType>, deps: DependencyList) => {
|
17
19
|
data: TReturnType | TDefaultValueType;
|
18
20
|
error: Error | null;
|
19
21
|
isLoading: boolean;
|
package/lib/Hooks/useAsync.js
CHANGED
@@ -10,14 +10,16 @@ const react_1 = require("react");
|
|
10
10
|
* 2. When any of the `deps` change.
|
11
11
|
* 3. When the `reload` function is called.
|
12
12
|
*/
|
13
|
-
const useAsync = ({ callback, defaultValue }, deps) => {
|
13
|
+
const useAsync = ({ callback, defaultValue, ignorePrevious = false }, deps) => {
|
14
14
|
const [data, setData] = (0, react_1.useState)(defaultValue);
|
15
15
|
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
16
16
|
const [error, setError] = (0, react_1.useState)(null);
|
17
|
+
const requestCountRef = (0, react_1.useRef)(0);
|
17
18
|
const reload = (0, react_1.useCallback)(() => {
|
18
19
|
setIsLoading(true);
|
19
20
|
setError(null);
|
20
21
|
setData(defaultValue);
|
22
|
+
const currentRequestCount = ignorePrevious ? ++requestCountRef.current : 0;
|
21
23
|
try {
|
22
24
|
const isArrayOfCallbacks = Array.isArray(callback);
|
23
25
|
const promises = isArrayOfCallbacks ? callback.map((cb) => cb()) : [callback()];
|
@@ -29,12 +31,18 @@ const useAsync = ({ callback, defaultValue }, deps) => {
|
|
29
31
|
}
|
30
32
|
Promise.all(promises)
|
31
33
|
.then((resolved) => {
|
32
|
-
|
33
|
-
|
34
|
+
// If ignorePrevious is set to TRUE we only set the new data if it's the most recent request
|
35
|
+
// and not a previous request that has taken longer than expected.
|
36
|
+
if (!ignorePrevious || currentRequestCount === requestCountRef.current) {
|
37
|
+
setData(isArrayOfCallbacks ? resolved : resolved[0]);
|
38
|
+
setIsLoading(false);
|
39
|
+
}
|
34
40
|
})
|
35
41
|
.catch((e) => {
|
36
|
-
|
37
|
-
|
42
|
+
if (!ignorePrevious || currentRequestCount === requestCountRef.current) {
|
43
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
44
|
+
setIsLoading(false);
|
45
|
+
}
|
38
46
|
});
|
39
47
|
}
|
40
48
|
catch (e) {
|
@@ -16,6 +16,7 @@ interface ResourceItem<T> {
|
|
16
16
|
componentIcon?: ReactElement | undefined;
|
17
17
|
iconSource?: ResourceSources;
|
18
18
|
showChevron?: boolean;
|
19
|
+
id?: string;
|
19
20
|
}
|
20
|
-
declare const ResourceItem: <T>({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, componentIcon, iconSource, showChevron, }: ResourceItem<T>) => React.JSX.Element;
|
21
|
+
declare const ResourceItem: <T>({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, componentIcon, iconSource, showChevron, id, }: ResourceItem<T>) => React.JSX.Element;
|
21
22
|
export { ResourceItem };
|
@@ -9,14 +9,14 @@ const react_aria_1 = require("react-aria");
|
|
9
9
|
const ModalOpeningButton_1 = require("../Modal/ModalOpeningButton");
|
10
10
|
const Icon_1 = require("../Icons/Icon");
|
11
11
|
const utils_1 = require("../Utils/utils");
|
12
|
-
const ResourceItem = ({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, componentIcon, iconSource = 'matrix', showChevron = false, }) => {
|
12
|
+
const ResourceItem = ({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, componentIcon, iconSource = 'matrix', showChevron = false, id, }) => {
|
13
13
|
const { triggerProps, overlayProps } = (0, react_aria_1.useOverlayTrigger)({ type: 'dialog' }, previewModalState);
|
14
14
|
const isDisabled = allowedTypes !== undefined && !allowedTypes.includes((0, utils_1.transformSnakeCase)(type));
|
15
15
|
const title = isDisabled ? "You can't select this item" : label;
|
16
16
|
return (react_1.default.createElement("li", { className: `squiz-gb-scope flex items-stretch p-1 bg-white border-1 border-grey-200 min-h-[64px] ${className}`, key: (0, utils_1.uuid)() },
|
17
17
|
react_1.default.createElement(ModalOpeningButton_1.ModalOpeningButton, { type: "button", ...triggerProps, isDisabled: isDisabled, onPress: () => onSelect(item, overlayProps), "aria-label": childCount === undefined ? `Drill down to ${label} children` : '', className: `
|
18
18
|
relative grow flex items-center px-4 py-2 rounded outline-0 ${selected ? 'bg-blue-100 text-blue-400' : ''} ${childCount !== undefined && childCount > 0 ? 'mr-2' : ''} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
|
19
|
-
`, title: title },
|
19
|
+
`, title: title, id: id },
|
20
20
|
react_1.default.createElement(Icon_1.Icon, { icon: type, resourceSource: iconSource, "aria-label": type, className: `mr-4 shrink-0 ${isDisabled && 'opacity-40'}`, componentIcon: componentIcon }),
|
21
21
|
react_1.default.createElement("span", { className: `relative flex items-center ${selected ? 'mr-8' : ''}` },
|
22
22
|
react_1.default.createElement("span", { className: "line-clamp-2 text-left break-word" }, label),
|
package/package.json
CHANGED
@@ -3,9 +3,10 @@ import { renderHook, waitFor } from '@testing-library/react';
|
|
3
3
|
import { useAsync } from './useAsync';
|
4
4
|
|
5
5
|
describe('useAsync', () => {
|
6
|
-
const renderAsyncHook = (callback: () => any | [], deps: DependencyList) => {
|
6
|
+
const renderAsyncHook = (callback: () => any | [], deps: DependencyList, ignorePrevious = false) => {
|
7
7
|
return renderHook(
|
8
|
-
({ deps }: { deps: DependencyList }) =>
|
8
|
+
({ deps }: { deps: DependencyList }) =>
|
9
|
+
useAsync({ callback, defaultValue: 'Initial state', ignorePrevious }, deps),
|
9
10
|
{ initialProps: { deps } },
|
10
11
|
);
|
11
12
|
};
|
package/src/Hooks/useAsync.ts
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
import { DependencyList, useState, useCallback, useEffect } from 'react';
|
1
|
+
import { DependencyList, useState, useCallback, useEffect, useRef } from 'react';
|
2
2
|
|
3
3
|
export type UseAsyncProps<TReturnType, TDefaultValueType> = {
|
4
4
|
/** The async callback or an array of async callbacks to call for fetching data. */
|
5
5
|
callback: (() => TReturnType | Promise<TReturnType>) | Array<() => TReturnType | Promise<TReturnType>>;
|
6
6
|
/** The default value to populate the data as when initially mounted or reloading data. */
|
7
7
|
defaultValue: TReturnType | TDefaultValueType;
|
8
|
+
/** Optional toggle to ignore previous async callback data */
|
9
|
+
ignorePrevious?: boolean;
|
8
10
|
};
|
9
11
|
|
10
12
|
/**
|
@@ -16,18 +18,21 @@ export type UseAsyncProps<TReturnType, TDefaultValueType> = {
|
|
16
18
|
* 3. When the `reload` function is called.
|
17
19
|
*/
|
18
20
|
export const useAsync = <TReturnType, TDefaultValueType>(
|
19
|
-
{ callback, defaultValue }: UseAsyncProps<TReturnType, TDefaultValueType>,
|
21
|
+
{ callback, defaultValue, ignorePrevious = false }: UseAsyncProps<TReturnType, TDefaultValueType>,
|
20
22
|
deps: DependencyList,
|
21
23
|
) => {
|
22
24
|
const [data, setData] = useState(defaultValue);
|
23
25
|
const [isLoading, setIsLoading] = useState(false);
|
24
26
|
const [error, setError] = useState<Error | null>(null);
|
27
|
+
const requestCountRef = useRef(0);
|
25
28
|
|
26
29
|
const reload = useCallback(() => {
|
27
30
|
setIsLoading(true);
|
28
31
|
setError(null);
|
29
32
|
setData(defaultValue);
|
30
33
|
|
34
|
+
const currentRequestCount = ignorePrevious ? ++requestCountRef.current : 0;
|
35
|
+
|
31
36
|
try {
|
32
37
|
const isArrayOfCallbacks = Array.isArray(callback);
|
33
38
|
const promises = isArrayOfCallbacks ? callback.map((cb) => cb()) : [callback()];
|
@@ -41,12 +46,18 @@ export const useAsync = <TReturnType, TDefaultValueType>(
|
|
41
46
|
|
42
47
|
Promise.all(promises)
|
43
48
|
.then((resolved: TReturnType[]) => {
|
44
|
-
|
45
|
-
|
49
|
+
// If ignorePrevious is set to TRUE we only set the new data if it's the most recent request
|
50
|
+
// and not a previous request that has taken longer than expected.
|
51
|
+
if (!ignorePrevious || currentRequestCount === requestCountRef.current) {
|
52
|
+
setData(isArrayOfCallbacks ? (resolved as TReturnType) : resolved[0]);
|
53
|
+
setIsLoading(false);
|
54
|
+
}
|
46
55
|
})
|
47
56
|
.catch((e: unknown) => {
|
48
|
-
|
49
|
-
|
57
|
+
if (!ignorePrevious || currentRequestCount === requestCountRef.current) {
|
58
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
59
|
+
setIsLoading(false);
|
60
|
+
}
|
50
61
|
});
|
51
62
|
} catch (e: unknown) {
|
52
63
|
setError(e instanceof Error ? e : new Error(String(e)));
|
@@ -21,6 +21,7 @@ interface ResourceItem<T> {
|
|
21
21
|
componentIcon?: ReactElement | undefined;
|
22
22
|
iconSource?: ResourceSources;
|
23
23
|
showChevron?: boolean;
|
24
|
+
id?: string;
|
24
25
|
}
|
25
26
|
|
26
27
|
const ResourceItem = <T,>({
|
@@ -37,6 +38,7 @@ const ResourceItem = <T,>({
|
|
37
38
|
componentIcon,
|
38
39
|
iconSource = 'matrix',
|
39
40
|
showChevron = false,
|
41
|
+
id,
|
40
42
|
}: ResourceItem<T>) => {
|
41
43
|
const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
|
42
44
|
const isDisabled = allowedTypes !== undefined && !allowedTypes.includes(transformSnakeCase(type));
|
@@ -59,6 +61,7 @@ const ResourceItem = <T,>({
|
|
59
61
|
} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
|
60
62
|
`}
|
61
63
|
title={title}
|
64
|
+
id={id}
|
62
65
|
>
|
63
66
|
<Icon
|
64
67
|
icon={type as IconOptions}
|