diff --git a/package-lock.json b/package-lock.json
index 7050eb5..c488bca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -67,6 +67,7 @@
"@tanstack/react-pacer": ">= 0.15.0",
"@tanstack/react-query": ">= 5.83.0",
"p-all": ">= 4.0.0",
+ "qs": ">= 6.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"use-deep-compare": "^1.3.0"
@@ -6959,7 +6960,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -17264,7 +17264,6 @@
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -18329,6 +18328,22 @@
],
"license": "MIT"
},
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -19606,7 +19621,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -19626,7 +19640,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -19643,7 +19656,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -19662,7 +19674,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
diff --git a/package.json b/package.json
index a6fb8a5..877f920 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@tanstack/react-pacer": ">= 0.15.0",
"@tanstack/react-query": ">= 5.83.0",
"p-all": ">= 4.0.0",
+ "qs": ">= 6.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"use-deep-compare": "^1.3.0"
diff --git a/src/components/TableToolsTable/TableToolsTable.js b/src/components/TableToolsTable/TableToolsTable.js
index c4df589..1d09f76 100644
--- a/src/components/TableToolsTable/TableToolsTable.js
+++ b/src/components/TableToolsTable/TableToolsTable.js
@@ -15,6 +15,7 @@ import PrimaryToolbar from '@redhat-cloud-services/frontend-components/PrimaryTo
import TableToolbar from '@redhat-cloud-services/frontend-components/TableToolbar';
import useTableTools from '~/hooks/useTableTools';
+import useTableSearchParams from '~/hooks/useTableSearchParams';
import { TableStateProvider, FilterModal, TableViewToggle } from '~/components';
const TableToolsTable = ({
@@ -34,6 +35,7 @@ const TableToolsTable = ({
paginationProps,
...tablePropsRest
}) => {
+ const searchParamsState = useTableSearchParams(options);
const {
view,
loading,
@@ -49,10 +51,34 @@ const TableToolsTable = ({
externalTotal,
{
treeTable,
- filters,
+ filters: {
+ ...(filters || {}),
+ activeFilters: {
+ ...filters?.activeFilters,
+ ...(searchParamsState?.filters || {}),
+ },
+ },
columns,
toolbarProps: toolbarPropsProp,
tableProps: tablePropsRest,
+ ...(searchParamsState
+ ? {
+ ...(searchParamsState.page
+ ? { page: parseInt(searchParamsState.page) }
+ : {}),
+ ...(searchParamsState.perPage
+ ? { perPage: parseInt(searchParamsState.perPage) }
+ : {}),
+ ...(searchParamsState.sort
+ ? {
+ sortBy: {
+ ...searchParamsState.sort,
+ index: parseInt(searchParamsState.sort.index),
+ },
+ }
+ : {}),
+ }
+ : {}),
...options,
},
);
diff --git a/src/components/TableToolsTable/TableToolsTableExperiments.stories.js b/src/components/TableToolsTable/TableToolsTableExperiments.stories.js
index 9575eec..c9f3eb1 100644
--- a/src/components/TableToolsTable/TableToolsTableExperiments.stories.js
+++ b/src/components/TableToolsTable/TableToolsTableExperiments.stories.js
@@ -4,6 +4,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
Card,
CardBody,
+ Content,
+ ContentVariants,
Spinner,
Button,
Label,
@@ -536,4 +538,62 @@ export const AccessItemsStory = {
render: (args) => ,
};
+const UrlParamsExample = () => {
+ const exampleUrl =
+ 'perPage=20&page=1&filters[title][0]=ds&filters[rating-above][0]=4&sort[index]=1&sort[direction]=desc';
+ const searchParams = new URLSearchParams(exampleUrl);
+ const [searchParamsState, setSearchParamsState] = useState();
+ const setSearchParams = useCallback((params) => {
+ setSearchParamsState(params);
+ }, []);
+
+ const {
+ loading,
+ result: { data, meta: { total } = {} } = {},
+ error,
+ } = useExampleDataQuery({
+ endpoint: '/api',
+ useTableState: true,
+ });
+
+ return (
+ <>
+
+ URL would be:{' '}
+ {decodeURI(searchParamsState?.toString() || '')}
+
+
+ >
+ );
+};
+
+export const UrlParamsStory = {
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+ render: (args) => ,
+};
+
export default meta;
diff --git a/src/hooks/useFilterConfig/useFilterConfig.js b/src/hooks/useFilterConfig/useFilterConfig.js
index b323dcc..1255d49 100644
--- a/src/hooks/useFilterConfig/useFilterConfig.js
+++ b/src/hooks/useFilterConfig/useFilterConfig.js
@@ -81,6 +81,8 @@ const useFilterConfig = (options) => {
: {},
);
+ // TODO with URL params it can (for some reason) happen that initial values get debounced
+ // and the first request won't include filters.
const debouncedSetState = useDebouncedCallback(setTableState, { wait: 500 });
useEffect(() => {
diff --git a/src/hooks/usePagination/hooks/usePaginationState.js b/src/hooks/usePagination/hooks/usePaginationState.js
index 9e67c1f..ef5fc7d 100644
--- a/src/hooks/usePagination/hooks/usePaginationState.js
+++ b/src/hooks/usePagination/hooks/usePaginationState.js
@@ -8,13 +8,13 @@ import useTableState from '~/hooks/useTableState';
import { TABLE_STATE_NAMESPACE } from '../constants';
const usePaginationState = (options) => {
- const { perPage = 10, serialisers } = options;
+ const { perPage = 10, page = 1, serialisers } = options;
const defaultState = useMemo(() => {
return {
perPage,
- page: 1,
+ page,
};
- }, [perPage]);
+ }, [perPage, page]);
const resetPage = useCallback(
(currentState) => {
return {
diff --git a/src/hooks/useTableSearchParams/hooks/useSearchParamsState.js b/src/hooks/useTableSearchParams/hooks/useSearchParamsState.js
new file mode 100644
index 0000000..a2ea820
--- /dev/null
+++ b/src/hooks/useTableSearchParams/hooks/useSearchParamsState.js
@@ -0,0 +1,22 @@
+import { useCallback, useRef } from 'react';
+import { parse, stringify } from 'qs';
+
+const useSearchParamsState = ({ searchParams, setSearchParams }) => {
+ const searchParamsState = useRef(
+ searchParams ? parse(searchParams.toString()) : undefined,
+ );
+
+ const setSearchParamsState = useCallback(
+ (params) => {
+ setSearchParams(new URLSearchParams(stringify(params)));
+ },
+ [setSearchParams],
+ );
+
+ return [
+ searchParams && searchParamsState.current,
+ setSearchParams && setSearchParamsState,
+ ];
+};
+
+export default useSearchParamsState;
diff --git a/src/hooks/useTableSearchParams/index.js b/src/hooks/useTableSearchParams/index.js
new file mode 100644
index 0000000..6c8735d
--- /dev/null
+++ b/src/hooks/useTableSearchParams/index.js
@@ -0,0 +1 @@
+export { default } from './useTableSearchParams';
diff --git a/src/hooks/useTableSearchParams/useTableSearchParams.js b/src/hooks/useTableSearchParams/useTableSearchParams.js
new file mode 100644
index 0000000..b80d24d
--- /dev/null
+++ b/src/hooks/useTableSearchParams/useTableSearchParams.js
@@ -0,0 +1,30 @@
+import { useDeepCompareEffect } from 'use-deep-compare';
+import { useFullTableState } from '~/hooks';
+
+import useSearchParamsState from './hooks/useSearchParamsState';
+
+const useTableSearchParams = ({ searchParams, setSearchParams }) => {
+ const fullTableState = useFullTableState();
+ const {
+ tableState: { pagination: { state: pagination } = {}, filters, sort } = {},
+ } = fullTableState || {};
+
+ const [searchParamsState, setSearchParamsState] = useSearchParamsState({
+ searchParams,
+ setSearchParams,
+ });
+
+ useDeepCompareEffect(() => {
+ if (setSearchParamsState) {
+ setSearchParamsState({
+ ...(pagination || {}),
+ filters,
+ ...(sort ? { sort } : {}),
+ });
+ }
+ }, [pagination, filters, sort, setSearchParamsState]);
+
+ return searchParamsState;
+};
+
+export default useTableSearchParams;
diff --git a/src/hooks/useTableSort/useTableSort.js b/src/hooks/useTableSort/useTableSort.js
index b9b2c2a..49debb1 100644
--- a/src/hooks/useTableSort/useTableSort.js
+++ b/src/hooks/useTableSort/useTableSort.js
@@ -1,4 +1,4 @@
-import { useCallback } from 'react';
+import { useCallback, useMemo } from 'react';
import { useDeepCompareMemo } from 'use-deep-compare';
import useTableState, { useRawTableState } from '~/hooks/useTableState';
@@ -41,7 +41,14 @@ const useTableSort = (columns, options = {}) => {
serialisers: { sort: serialiser } = {},
onSort: onSortOption,
} = options;
-
+ const defaultState = useMemo(
+ () =>
+ initialSortBy || {
+ index: 0,
+ direction: 'asc',
+ },
+ [initialSortBy],
+ );
const { tableView } = useRawTableState() || {};
const offset = columnOffset({ ...options, tableView });
@@ -57,10 +64,7 @@ const useTableSort = (columns, options = {}) => {
);
const [sortBy, setSortBy] = useTableState(
TABLE_STATE_NAMESPACE,
- initialSortBy || {
- index: 0,
- direction: 'asc',
- },
+ defaultState,
stateOptions,
);
diff --git a/src/support/factories/filters.js b/src/support/factories/filters.js
index 93666eb..0851538 100644
--- a/src/support/factories/filters.js
+++ b/src/support/factories/filters.js
@@ -45,7 +45,7 @@ export const rating = {
label: 'Rating above',
items: [...new Array(5)].map((_, idx) => ({
label: [...new Array(idx + 1)].map(() => '★'),
- value: idx + 1,
+ value: `${idx + 1}`,
})),
filterSerialiser: (_config, value) => `.rating >= ${value}`,
};