Skip to content

Commit 4a5d642

Browse files
committed
🌱 [backport release-0.6] Drop use of @migtools/lib-ui (konveyor#2193)
Backport of: konveyor#2193 Resolves: konveyor#2044 To have direct control over storage and selection hooks used in the project, pull in the used code from `@migtools/lib-ui` and drop it as a dependency. Changes: - Drop `@migtools/lib-ui` from dependencies - Pull in hooks useSelectionState() and useStorage() - Adjust components to use the project's versions of the hooks Source: https://github.com/migtools/lib-ui Tag: [v10.0.1](https://github.com/migtools/lib-ui/tree/v10.0.1) Commit SHA: 0cb74ce6d4de236abe1b7d20bbe747ceca392f28 Signed-off-by: Scott J Dickerson <[email protected]>
1 parent c634b15 commit 4a5d642

File tree

18 files changed

+330
-42
lines changed

18 files changed

+330
-42
lines changed

client/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"@dnd-kit/sortable": "^7.0.2",
2525
"@hookform/resolvers": "^2.9.11",
2626
"@hot-loader/react-dom": "^17.0.2",
27-
"@migtools/lib-ui": "^10.0.1",
2827
"@patternfly/patternfly": "5.2.1",
2928
"@patternfly/react-charts": "7.2.2",
3029
"@patternfly/react-code-editor": "5.2.3",

client/src/app/hooks/table-controls/column/useColumnState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useLocalStorage } from "@migtools/lib-ui";
1+
import { useLocalStorage } from "@app/hooks/useStorage";
22
import { ColumnSetting } from "../types";
33
import { useEffect } from "react";
44

client/src/app/hooks/table-controls/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { TableProps, TdProps, ThProps, TrProps } from "@patternfly/react-table";
2-
import { ISelectionStateArgs, useSelectionState } from "@migtools/lib-ui";
2+
import {
3+
ISelectionStateArgs,
4+
useSelectionState,
5+
} from "@app/hooks/useSelectionState";
36
import {
47
DisallowCharacters,
58
DiscriminatedArgs,

client/src/app/hooks/table-controls/useLocalTableControls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useTableControlProps } from "./useTableControlProps";
22
import { ITableControls, IUseLocalTableControlsArgs } from "./types";
33
import { getLocalTableControlDerivedState } from "./getLocalTableControlDerivedState";
44
import { useTableControlState } from "./useTableControlState";
5-
import { useSelectionState } from "@migtools/lib-ui";
5+
import { useSelectionState } from "@app/hooks/useSelectionState";
66

77
/**
88
* Provides all state, derived state, side-effects and prop helpers needed to manage a local/client-computed table.

client/src/app/hooks/usePersistentState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
UseStorageTypeOptions,
55
useLocalStorage,
66
useSessionStorage,
7-
} from "@migtools/lib-ui";
7+
} from "@app/hooks/useStorage";
88
import { DisallowCharacters } from "@app/utils/type-utils";
99

1010
type PersistToStateOptions = { persistTo?: "state" };
@@ -122,7 +122,7 @@ const usePersistenceProvider = <TValue>({
122122
deserialize,
123123
defaultValue,
124124
}: PersistToProvider<TValue>): [TValue, (val: TValue) => void] => {
125-
// use default value if nulish value was deserialized
125+
// use default value if nullish value was deserialized
126126
return [deserialize() ?? defaultValue, serialize];
127127
};
128128

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./useSelectionState";
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { act } from "@testing-library/react";
2+
import { renderHook } from "@testing-library/react-hooks";
3+
4+
import { useSelectionState } from "./useSelectionState";
5+
6+
interface IFruit {
7+
name: string;
8+
}
9+
const fruits: IFruit[] = [
10+
{ name: "Apple" },
11+
{ name: "Orange" },
12+
{ name: "Banana" },
13+
];
14+
15+
describe("useSelectionState", () => {
16+
it("Initializes selection state and hook loads with no errors", () => {
17+
const { result } = renderHook(() =>
18+
useSelectionState<IFruit>({ items: fruits })
19+
);
20+
expect(result.current.selectedItems).toEqual([]);
21+
});
22+
23+
it("Updates state correctly when toggling an item", () => {
24+
const { result } = renderHook(() =>
25+
useSelectionState<string>({
26+
items: ["A", "B", "C"],
27+
})
28+
);
29+
30+
act(() => result.current.toggleItemSelected("A", true));
31+
expect(result.current.isItemSelected("A")).toBe(true);
32+
});
33+
34+
it("Updates state correctly when setting selected state", () => {
35+
const { result } = renderHook(() =>
36+
useSelectionState<string>({
37+
items: ["A", "B", "C"],
38+
})
39+
);
40+
41+
act(() => result.current.setSelectedItems(["A"]));
42+
expect(result.current.selectedItems).toEqual(["A"]);
43+
});
44+
45+
it("Updates state correctly when selecting/deselecting all", () => {
46+
const { result } = renderHook(() =>
47+
useSelectionState<string>({
48+
items: ["A", "B", "C"],
49+
})
50+
);
51+
act(() => result.current.selectAll(true));
52+
expect(result.current.areAllSelected).toEqual(true);
53+
expect(result.current.selectedItems).toEqual(["A", "B", "C"]);
54+
55+
act(() => result.current.selectAll(false));
56+
expect(result.current.areAllSelected).toEqual(false);
57+
expect(result.current.selectedItems).toEqual([]);
58+
});
59+
60+
it("Preserves the original order of items when selecting out of order", () => {
61+
const { result } = renderHook(() =>
62+
useSelectionState<IFruit>({ items: fruits })
63+
);
64+
act(() =>
65+
result.current.setSelectedItems([
66+
{ name: "Orange" },
67+
{ name: "Apple" },
68+
{ name: "Banana" },
69+
])
70+
);
71+
expect(result.current.selectedItems).toEqual(fruits);
72+
});
73+
74+
it("Handles initialSelected properly", () => {
75+
const { result, rerender } = renderHook(() =>
76+
useSelectionState<string>({
77+
items: ["A", "B", "C"],
78+
initialSelected: ["A"],
79+
})
80+
);
81+
rerender();
82+
expect(result.current.selectedItems).toEqual(["A"]);
83+
});
84+
85+
it("Handles a custom isEqual arg properly", () => {
86+
const { result } = renderHook(() =>
87+
useSelectionState<IFruit>({
88+
items: fruits,
89+
isEqual: (a, b) => a.name === b.name,
90+
})
91+
);
92+
act(() => result.current.setSelectedItems([{ name: "Apple" }]));
93+
expect(result.current.selectedItems).toEqual([{ name: "Apple" }]);
94+
});
95+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as React from "react";
2+
3+
export interface ISelectionStateArgs<T> {
4+
items: T[];
5+
initialSelected?: T[];
6+
isEqual?: (a: T, b: T) => boolean;
7+
isItemSelectable?: (item: T) => boolean;
8+
externalState?: [T[], React.Dispatch<React.SetStateAction<T[]>>];
9+
}
10+
11+
export interface ISelectionState<T> {
12+
selectedItems: T[];
13+
isItemSelected: (item: T) => boolean;
14+
isItemSelectable: (item: T) => boolean;
15+
toggleItemSelected: (item: T, isSelecting?: boolean) => void;
16+
selectMultiple: (items: T[], isSelecting: boolean) => void;
17+
areAllSelected: boolean;
18+
selectAll: (isSelecting?: boolean) => void;
19+
setSelectedItems: (items: T[]) => void;
20+
}
21+
22+
export const useSelectionState = <T>({
23+
items,
24+
initialSelected = [],
25+
isEqual = (a, b) => a === b,
26+
isItemSelectable = () => true,
27+
externalState,
28+
}: ISelectionStateArgs<T>): ISelectionState<T> => {
29+
const internalState = React.useState<T[]>(initialSelected);
30+
const [selectedItems, setSelectedItems] = externalState || internalState;
31+
32+
const selectableItems = React.useMemo(
33+
() => items.filter(isItemSelectable),
34+
[items, isItemSelectable]
35+
);
36+
37+
const isItemSelected = React.useCallback(
38+
(item: T) => selectedItems.some((i) => isEqual(item, i)),
39+
[isEqual, selectedItems]
40+
);
41+
42+
// If isItemSelectable changes and a selected item is no longer selectable, deselect it
43+
React.useEffect(() => {
44+
if (!selectedItems.every(isItemSelectable)) {
45+
setSelectedItems(selectedItems.filter(isItemSelectable));
46+
}
47+
}, [isItemSelectable, selectedItems, setSelectedItems]);
48+
49+
const toggleItemSelected = React.useCallback(
50+
(item: T, isSelecting = !isItemSelected(item)) => {
51+
if (isSelecting && isItemSelectable(item)) {
52+
setSelectedItems([...selectedItems, item]);
53+
} else {
54+
setSelectedItems(selectedItems.filter((i) => !isEqual(i, item)));
55+
}
56+
},
57+
[isEqual, isItemSelectable, isItemSelected, selectedItems, setSelectedItems]
58+
);
59+
60+
const selectMultiple = React.useCallback(
61+
(itemsSubset: T[], isSelecting: boolean) => {
62+
const otherSelectedItems = selectedItems.filter(
63+
(selected) => !itemsSubset.some((item) => isEqual(selected, item))
64+
);
65+
const itemsToSelect = itemsSubset.filter(isItemSelectable);
66+
if (isSelecting) {
67+
setSelectedItems([...otherSelectedItems, ...itemsToSelect]);
68+
} else {
69+
setSelectedItems(otherSelectedItems);
70+
}
71+
},
72+
[isEqual, isItemSelectable, selectedItems, setSelectedItems]
73+
);
74+
75+
const selectAll = React.useCallback(
76+
(isSelecting = true) =>
77+
setSelectedItems(isSelecting ? selectableItems : []),
78+
[selectableItems, setSelectedItems]
79+
);
80+
const areAllSelected = selectedItems.length === selectableItems.length;
81+
82+
// Preserve original order of items
83+
const selectedItemsInOrder = React.useMemo(() => {
84+
if (areAllSelected) {
85+
return selectableItems;
86+
} else if (selectedItems.length > 0) {
87+
return selectableItems.filter(isItemSelected);
88+
}
89+
return [];
90+
}, [areAllSelected, isItemSelected, selectableItems, selectedItems.length]);
91+
92+
return {
93+
selectedItems: selectedItemsInOrder,
94+
isItemSelected,
95+
isItemSelectable,
96+
toggleItemSelected,
97+
selectMultiple,
98+
areAllSelected,
99+
selectAll,
100+
setSelectedItems,
101+
};
102+
};

client/src/app/hooks/useStorage.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import * as React from "react";
2+
3+
type StorageType = "localStorage" | "sessionStorage";
4+
5+
const getValueFromStorage = <T>(
6+
storageType: StorageType,
7+
key: string,
8+
defaultValue: T
9+
): T => {
10+
if (typeof window === "undefined") return defaultValue;
11+
try {
12+
const itemJSON = window[storageType].getItem(key);
13+
return itemJSON ? (JSON.parse(itemJSON) as T) : defaultValue;
14+
} catch (error) {
15+
console.error(error);
16+
return defaultValue;
17+
}
18+
};
19+
20+
const setValueInStorage = <T>(
21+
storageType: StorageType,
22+
key: string,
23+
newValue: T | undefined
24+
) => {
25+
if (typeof window === "undefined") return;
26+
try {
27+
if (newValue !== undefined) {
28+
const newValueJSON = JSON.stringify(newValue);
29+
window[storageType].setItem(key, newValueJSON);
30+
if (storageType === "localStorage") {
31+
// setItem only causes the StorageEvent to be dispatched in other windows. We dispatch it here
32+
// manually so that all instances of useLocalStorage on this window also react to this change.
33+
window.dispatchEvent(
34+
new StorageEvent("storage", { key, newValue: newValueJSON })
35+
);
36+
}
37+
} else {
38+
window[storageType].removeItem(key);
39+
if (storageType === "localStorage") {
40+
window.dispatchEvent(
41+
new StorageEvent("storage", { key, newValue: null })
42+
);
43+
}
44+
}
45+
} catch (error) {
46+
console.error(error);
47+
}
48+
};
49+
50+
interface IUseStorageOptions<T> {
51+
isEnabled?: boolean;
52+
type: StorageType;
53+
key: string;
54+
defaultValue: T;
55+
}
56+
57+
const useStorage = <T>({
58+
isEnabled = true,
59+
type,
60+
key,
61+
defaultValue,
62+
}: IUseStorageOptions<T>): [T, React.Dispatch<React.SetStateAction<T>>] => {
63+
const [cachedValue, setCachedValue] = React.useState<T>(
64+
getValueFromStorage(type, key, defaultValue)
65+
);
66+
67+
const usingStorageEvents =
68+
type === "localStorage" && typeof window !== "undefined" && isEnabled;
69+
70+
const setValue: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
71+
(newValueOrFn: T | ((prevState: T) => T)) => {
72+
const newValue =
73+
newValueOrFn instanceof Function
74+
? newValueOrFn(getValueFromStorage(type, key, defaultValue))
75+
: newValueOrFn;
76+
setValueInStorage(type, key, newValue);
77+
if (!usingStorageEvents) {
78+
// The cache won't update automatically if there is no StorageEvent dispatched.
79+
setCachedValue(newValue);
80+
}
81+
},
82+
[type, key, defaultValue, usingStorageEvents]
83+
);
84+
85+
React.useEffect(() => {
86+
if (!usingStorageEvents) return;
87+
const onStorageUpdated = (event: StorageEvent) => {
88+
if (event.key === key) {
89+
setCachedValue(
90+
event.newValue ? JSON.parse(event.newValue) : defaultValue
91+
);
92+
}
93+
};
94+
window.addEventListener("storage", onStorageUpdated);
95+
return () => {
96+
window.removeEventListener("storage", onStorageUpdated);
97+
};
98+
}, [key, defaultValue, usingStorageEvents]);
99+
100+
return [cachedValue, setValue];
101+
};
102+
103+
export type UseStorageTypeOptions<T> = Omit<IUseStorageOptions<T>, "type">;
104+
105+
export const useLocalStorage = <T>(
106+
options: UseStorageTypeOptions<T>
107+
): [T, React.Dispatch<React.SetStateAction<T>>] =>
108+
useStorage({ ...options, type: "localStorage" });
109+
110+
export const useSessionStorage = <T>(
111+
options: UseStorageTypeOptions<T>
112+
): [T, React.Dispatch<React.SetStateAction<T>>] =>
113+
useStorage({ ...options, type: "sessionStorage" });

client/src/app/pages/dependencies/dependencies.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
TableRowContentWithControls,
2828
} from "@app/components/TableControls";
2929
import { useFetchDependencies } from "@app/queries/dependencies";
30-
import { useSelectionState } from "@migtools/lib-ui";
30+
import { useSelectionState } from "@app/hooks/useSelectionState";
3131
import { DependencyAppsDetailDrawer } from "./dependency-apps-detail-drawer";
3232
import { useSharedAffectedApplicationFilterCategories } from "../issues/helpers";
3333
import { getParsedLabel } from "@app/utils/rules-utils";

0 commit comments

Comments
 (0)