0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): replace useTableSearchParams with useSearchParametersWatcher (#3027)

This commit is contained in:
Xiao Yijun 2023-02-01 12:40:20 +08:00 committed by GitHub
parent f16f9d2403
commit bc62796e5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 182 additions and 212 deletions

View file

@ -9,7 +9,7 @@ import ApplicationName from '@/components/ApplicationName';
import UserName from '@/components/UserName'; import UserName from '@/components/UserName';
import { defaultPageSize } from '@/consts'; import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import usePageSearchParameters from '@/hooks/use-page-search-parameters'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utilities/url'; import { buildUrl } from '@/utilities/url';
import Table from '../Table'; import Table from '../Table';
@ -28,7 +28,7 @@ const AuditLogTable = ({ userId, className }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { pathname } = useLocation(); const { pathname } = useLocation();
const pageSize = defaultPageSize; const pageSize = defaultPageSize;
const [{ page, event, applicationId }, updatePageSearchParameters] = usePageSearchParameters({ const [{ page, event, applicationId }, updateSearchParameters] = useSearchParametersWatcher({
page: 1, page: 1,
event: '', event: '',
applicationId: '', applicationId: '',
@ -103,7 +103,7 @@ const AuditLogTable = ({ userId, className }: Props) => {
<EventSelector <EventSelector
value={event} value={event}
onChange={(event) => { onChange={(event) => {
updatePageSearchParameters({ event, page: undefined }); updateSearchParameters({ event, page: undefined });
}} }}
/> />
</div> </div>
@ -111,18 +111,18 @@ const AuditLogTable = ({ userId, className }: Props) => {
<ApplicationSelector <ApplicationSelector
value={applicationId} value={applicationId}
onChange={(applicationId) => { onChange={(applicationId) => {
updatePageSearchParameters({ applicationId, page: undefined }); updateSearchParameters({ applicationId, page: undefined });
}} }}
/> />
</div> </div>
</div> </div>
} }
pagination={{ pagination={{
pageIndex: Number(page), page,
totalCount, totalCount,
pageSize, pageSize,
onChange: (page) => { onChange: (page) => {
updatePageSearchParameters({ page }); updateSearchParameters({ page });
}, },
}} }}
isLoading={isLoading} isLoading={isLoading}

View file

@ -11,7 +11,7 @@ import Previous from './Previous';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
export type Props = { export type Props = {
pageIndex: number; page: number;
totalCount?: number; totalCount?: number;
pageSize: number; pageSize: number;
className?: string; className?: string;
@ -20,7 +20,7 @@ export type Props = {
}; };
const Pagination = ({ const Pagination = ({
pageIndex, page,
totalCount, totalCount,
pageSize, pageSize,
className, className,
@ -42,8 +42,8 @@ const Pagination = ({
return null; return null;
} }
const min = (pageIndex - 1) * pageSize + 1; const min = (page - 1) * pageSize + 1;
const max = Math.min(pageIndex * pageSize, cachedTotalCount); const max = Math.min(page * pageSize, cachedTotalCount);
const isPicoMode = mode === 'pico'; const isPicoMode = mode === 'pico';
return ( return (
@ -54,13 +54,13 @@ const Pagination = ({
<ReactPaginate <ReactPaginate
className={styles.pagination} className={styles.pagination}
pageCount={pageCount} pageCount={pageCount}
forcePage={pageIndex - 1} forcePage={page - 1}
pageLabelBuilder={(page: number) => ( pageLabelBuilder={(pageNumber: number) => (
<Button <Button
type={page === pageIndex ? 'outline' : 'default'} type={pageNumber === page ? 'outline' : 'default'}
className={classNames(styles.button, page === pageIndex && styles.active)} className={classNames(styles.button, pageNumber === page && styles.active)}
size="small" size="small"
title={<DangerousRaw>{page}</DangerousRaw>} title={<DangerousRaw>{pageNumber}</DangerousRaw>}
/> />
)} )}
previousLabel={<Button className={styles.button} size="small" icon={<Previous />} />} previousLabel={<Button className={styles.button} size="small" icon={<Previous />} />}

View file

@ -13,9 +13,8 @@ import TextInput from '@/components/TextInput';
import { defaultPageSize } from '@/consts'; import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useDebounce from '@/hooks/use-debounce'; import useDebounce from '@/hooks/use-debounce';
import { formatKeyword } from '@/hooks/use-table-search-params';
import * as transferLayout from '@/scss/transfer.module.scss'; import * as transferLayout from '@/scss/transfer.module.scss';
import { buildUrl } from '@/utilities/url'; import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import SourceUserItem from '../SourceUserItem'; import SourceUserItem from '../SourceUserItem';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -26,20 +25,21 @@ type Props = {
selectedUsers: User[]; selectedUsers: User[];
}; };
const pageSize = defaultPageSize;
const searchDelay = 500; const searchDelay = 500;
const SourceUsersBox = ({ roleId, selectedUsers, onChange }: Props) => { const SourceUsersBox = ({ roleId, selectedUsers, onChange }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [pageIndex, setPageIndex] = useState(1); const [page, setPage] = useState(1);
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const debounce = useDebounce(); const debounce = useDebounce();
const url = buildUrl('/api/users', { const url = buildUrl('/api/users', {
excludeRoleId: roleId, excludeRoleId: roleId,
hideAdminUser: String(true), hideAdminUser: String(true),
page: String(pageIndex), page: String(page),
page_size: String(defaultPageSize), page_size: String(pageSize),
...conditional(keyword && { search: formatKeyword(keyword) }), ...conditional(keyword && { search: formatSearchKeyword(keyword) }),
}); });
const { data, error } = useSWR<[User[], number], RequestError>(url); const { data, error } = useSWR<[User[], number], RequestError>(url);
@ -50,7 +50,7 @@ const SourceUsersBox = ({ roleId, selectedUsers, onChange }: Props) => {
const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => { const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => {
debounce(() => { debounce(() => {
setPageIndex(1); setPage(1);
setKeyword(event.target.value); setKeyword(event.target.value);
}, searchDelay); }, searchDelay);
}; };
@ -97,12 +97,12 @@ const SourceUsersBox = ({ roleId, selectedUsers, onChange }: Props) => {
</div> </div>
<Pagination <Pagination
mode="pico" mode="pico"
pageIndex={pageIndex} page={page}
totalCount={totalCount} totalCount={totalCount}
pageSize={defaultPageSize} pageSize={pageSize}
className={transferLayout.boxPagination} className={transferLayout.boxPagination}
onChange={(page) => { onChange={(page) => {
setPageIndex(page); setPage(page);
}} }}
/> />
</div> </div>

View file

@ -10,6 +10,7 @@ import Search from '@/assets/images/search.svg';
import DataEmpty from '@/components/DataEmpty'; import DataEmpty from '@/components/DataEmpty';
import Pagination from '@/components/Pagination'; import Pagination from '@/components/Pagination';
import TextInput from '@/components/TextInput'; import TextInput from '@/components/TextInput';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useDebounce from '@/hooks/use-debounce'; import useDebounce from '@/hooks/use-debounce';
import * as transferLayout from '@/scss/transfer.module.scss'; import * as transferLayout from '@/scss/transfer.module.scss';
@ -24,20 +25,20 @@ type Props = {
onChange: (value: RoleResponse[]) => void; onChange: (value: RoleResponse[]) => void;
}; };
const pageSize = 20; const pageSize = defaultPageSize;
const searchDelay = 500; const searchDelay = 500;
const SourceRolesBox = ({ userId, selectedRoles, onChange }: Props) => { const SourceRolesBox = ({ userId, selectedRoles, onChange }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [pageIndex, setPageIndex] = useState(1); const [page, setPage] = useState(1);
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const debounce = useDebounce(); const debounce = useDebounce();
const url = buildUrl('/api/roles', { const url = buildUrl('/api/roles', {
excludeUserId: userId, excludeUserId: userId,
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
...conditional(keyword && { search: `%${keyword}%` }), ...conditional(keyword && { search: `%${keyword}%` }),
}); });
@ -53,7 +54,7 @@ const SourceRolesBox = ({ userId, selectedRoles, onChange }: Props) => {
const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => { const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => {
debounce(() => { debounce(() => {
setPageIndex(1); setPage(1);
setKeyword(event.target.value); setKeyword(event.target.value);
}, searchDelay); }, searchDelay);
}; };
@ -98,12 +99,12 @@ const SourceRolesBox = ({ userId, selectedRoles, onChange }: Props) => {
</div> </div>
<Pagination <Pagination
mode="pico" mode="pico"
pageIndex={pageIndex} page={page}
totalCount={totalCount} totalCount={totalCount}
pageSize={pageSize} pageSize={pageSize}
className={transferLayout.boxPagination} className={transferLayout.boxPagination}
onChange={(page) => { onChange={(page) => {
setPageIndex(page); setPage(page);
}} }}
/> />
</div> </div>

View file

@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router-dom';
type Parameters = Record<string, string | number>; type Parameters = Record<string, string | number>;
type UsePageSearchParametersReturn<T extends Parameters = Parameters> = [ type UseSearchParametersWatcherReturn<T extends Parameters = Parameters> = [
{ {
[K in keyof T]: T[K]; [K in keyof T]: T[K];
}, },
@ -12,17 +12,17 @@ type UsePageSearchParametersReturn<T extends Parameters = Parameters> = [
]; ];
/** /**
* Manage page search parameters * Watch search parameters
* *
* @param config Define search parameter keys and their default value. E.g., `{ page: 1, keyword: '' }` * @param config Define search parameter keys and their default value. E.g., `{ page: 1, keyword: '' }`
* @returns [pageSearchParams, updatePageSearchParams] * @returns [searchParams, updateSearchParams]
*/ */
const usePageSearchParameters = <T extends Parameters>( const useSearchParametersWatcher = <T extends Parameters>(
config: T config: T
): UsePageSearchParametersReturn<T> => { ): UseSearchParametersWatcherReturn<T> => {
const [searchParameters, setSearchParameters] = useSearchParams(); const [searchParameters, setSearchParameters] = useSearchParams();
const updatePageSearchParameters = useCallback( const updateSearchParameters = useCallback(
(parameters: Partial<T>) => { (parameters: Partial<T>) => {
const baseParameters = new URLSearchParams(searchParameters); const baseParameters = new URLSearchParams(searchParameters);
@ -54,9 +54,9 @@ const usePageSearchParameters = <T extends Parameters>(
return [parameterKey, parameterValue]; return [parameterKey, parameterValue];
}) })
) as UsePageSearchParametersReturn<T>[0], ) as UseSearchParametersWatcherReturn<T>[0],
updatePageSearchParameters, updateSearchParameters,
]; ];
}; };
export default usePageSearchParameters; export default useSearchParametersWatcher;

View file

@ -1,62 +0,0 @@
/* eslint-disable unicorn/prevent-abbreviations */
import type { Optional } from '@silverhand/essentials';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { defaultPageSize } from '@/consts';
type Props = Optional<{
pageSize?: number;
}>;
const pageIndexKey = 'page';
const keywordKey = 'keyword';
export const formatKeyword = (keyword: string) => `%${keyword}%`;
const useTableSearchParams = ({ pageSize = defaultPageSize }: Props = {}) => {
const [searchParams, setSearchParams] = useSearchParams();
const [searchParamsState, setSearchParamsState] = useState<URLSearchParams>(searchParams);
useEffect(() => {
setSearchParams(searchParamsState);
}, [searchParamsState, setSearchParams]);
const setPageIndex = (pageIndex: number) => {
setSearchParamsState((previousParams) => {
const params = new URLSearchParams(previousParams);
params.set(pageIndexKey, String(pageIndex));
return params;
});
};
const setKeyword = (value: string) => {
setSearchParamsState((previousParams) => {
const params = new URLSearchParams(previousParams);
if (value) {
params.set(keywordKey, value);
} else {
params.delete(keywordKey);
}
return params;
});
};
return {
pagination: {
pageIndex: Number(searchParamsState.get(pageIndexKey) ?? 1),
pageSize,
setPageIndex,
},
search: {
keyword: searchParamsState.get(keywordKey) ?? '',
setKeyword,
},
};
};
export default useTableSearchParams;
/* eslint-enable unicorn/prevent-abbreviations */

View file

@ -8,14 +8,17 @@ import useSWR from 'swr';
import ConfirmModal from '@/components/ConfirmModal'; import ConfirmModal from '@/components/ConfirmModal';
import PermissionsTable from '@/components/PermissionsTable'; import PermissionsTable from '@/components/PermissionsTable';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import useTableSearchParams, { formatKeyword } from '@/hooks/use-table-search-params'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utilities/url'; import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import type { ApiResourceDetailsOutletContext } from '../types'; import type { ApiResourceDetailsOutletContext } from '../types';
import CreatePermissionModal from './components/CreatePermissionModal'; import CreatePermissionModal from './components/CreatePermissionModal';
const pageSize = defaultPageSize;
const ApiResourcePermissions = () => { const ApiResourcePermissions = () => {
const { const {
resource: { id: resourceId }, resource: { id: resourceId },
@ -24,17 +27,17 @@ const ApiResourcePermissions = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { const [{ page, keyword }, updateSearchParameters] = useSearchParametersWatcher({
pagination: { pageIndex, pageSize, setPageIndex }, page: 1,
search: { keyword, setKeyword }, keyword: '',
} = useTableSearchParams(); });
const { data, error, mutate } = useSWR<[ScopeResponse[], number], RequestError>( const { data, error, mutate } = useSWR<[ScopeResponse[], number], RequestError>(
resourceId && resourceId &&
buildUrl(`/api/resources/${resourceId}/scopes`, { buildUrl(`/api/resources/${resourceId}/scopes`, {
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
...conditional(keyword && { search: formatKeyword(keyword) }), ...conditional(keyword && { search: formatSearchKeyword(keyword) }),
}) })
); );
@ -77,20 +80,26 @@ const ApiResourcePermissions = () => {
errorMessage={error?.body?.message ?? error?.message} errorMessage={error?.body?.message ?? error?.message}
retryHandler={async () => mutate(undefined, true)} retryHandler={async () => mutate(undefined, true)}
pagination={{ pagination={{
pageIndex, page,
pageSize, pageSize,
totalCount, totalCount,
onChange: setPageIndex, onChange: (page) => {
updateSearchParameters({ page });
},
}} }}
search={{ search={{
keyword, keyword,
searchHandler: (value) => { searchHandler: (keyword) => {
setKeyword(value); updateSearchParameters({
setPageIndex(1); keyword,
page: 1,
});
}, },
clearSearchHandler: () => { clearSearchHandler: () => {
setKeyword(''); updateSearchParameters({
setPageIndex(1); keyword: '',
page: 1,
});
}, },
}} }}
/> />

View file

@ -3,7 +3,7 @@ import { AppearanceMode } from '@logto/schemas';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Modal from 'react-modal'; import Modal from 'react-modal';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import useSWR from 'swr'; import useSWR from 'swr';
import ApiResourceDark from '@/assets/images/api-resource-dark.svg'; import ApiResourceDark from '@/assets/images/api-resource-dark.svg';
@ -15,8 +15,10 @@ import CopyToClipboard from '@/components/CopyToClipboard';
import ItemPreview from '@/components/ItemPreview'; import ItemPreview from '@/components/ItemPreview';
import Pagination from '@/components/Pagination'; import Pagination from '@/components/Pagination';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { defaultPageSize } from '@/consts';
import { ApiResourceDetailsTabs } from '@/consts/page-tabs'; import { ApiResourceDetailsTabs } from '@/consts/page-tabs';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { useTheme } from '@/hooks/use-theme'; import { useTheme } from '@/hooks/use-theme';
import * as modalStyles from '@/scss/modal.module.scss'; import * as modalStyles from '@/scss/modal.module.scss';
import * as resourcesStyles from '@/scss/resources.module.scss'; import * as resourcesStyles from '@/scss/resources.module.scss';
@ -25,23 +27,23 @@ import { buildUrl } from '@/utilities/url';
import CreateForm from './components/CreateForm'; import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
const apiResourcesPathname = '/api-resources'; const apiResourcesPathname = '/api-resources';
const createApiResourcePathname = `${apiResourcesPathname}/create`; const createApiResourcePathname = `${apiResourcesPathname}/create`;
const buildDetailsPathname = (id: string) => const buildDetailsPathname = (id: string) =>
`${apiResourcesPathname}/${id}/${ApiResourceDetailsTabs.Settings}`; `${apiResourcesPathname}/${id}/${ApiResourceDetailsTabs.Settings}`;
const pageSize = 20;
const ApiResources = () => { const ApiResources = () => {
const { pathname } = useLocation(); const { pathname, search } = useLocation();
const isCreateNew = pathname.endsWith('/create'); const isCreateNew = pathname.endsWith('/create');
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [query, setQuery] = useSearchParams();
const search = query.toString(); const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
const pageIndex = Number(query.get('page') ?? '1'); page: 1,
});
const url = buildUrl('/api/resources', { const url = buildUrl('/api/resources', {
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
}); });
@ -146,12 +148,12 @@ const ApiResources = () => {
onRetry={async () => mutate(undefined, true)} onRetry={async () => mutate(undefined, true)}
/> />
<Pagination <Pagination
pageIndex={pageIndex} page={page}
totalCount={totalCount} totalCount={totalCount}
pageSize={pageSize} pageSize={pageSize}
className={styles.pagination} className={styles.pagination}
onChange={(page) => { onChange={(page) => {
setQuery({ page: String(page) }); updateSearchParameters({ page });
}} }}
/> />
</div> </div>

View file

@ -2,7 +2,7 @@ import type { Application } from '@logto/schemas';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Modal from 'react-modal'; import Modal from 'react-modal';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import useSWR from 'swr'; import useSWR from 'swr';
import Plus from '@/assets/images/plus.svg'; import Plus from '@/assets/images/plus.svg';
@ -13,7 +13,9 @@ import CopyToClipboard from '@/components/CopyToClipboard';
import ItemPreview from '@/components/ItemPreview'; import ItemPreview from '@/components/ItemPreview';
import Pagination from '@/components/Pagination'; import Pagination from '@/components/Pagination';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as modalStyles from '@/scss/modal.module.scss'; import * as modalStyles from '@/scss/modal.module.scss';
import * as resourcesStyles from '@/scss/resources.module.scss'; import * as resourcesStyles from '@/scss/resources.module.scss';
import { applicationTypeI18nKey } from '@/types/applications'; import { applicationTypeI18nKey } from '@/types/applications';
@ -22,22 +24,23 @@ import { buildUrl } from '@/utilities/url';
import CreateForm from './components/CreateForm'; import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const pageSize = 20; const pageSize = defaultPageSize;
const applicationsPathname = '/applications'; const applicationsPathname = '/applications';
const createApplicationPathname = `${applicationsPathname}/create`; const createApplicationPathname = `${applicationsPathname}/create`;
const buildDetailsPathname = (id: string) => `${applicationsPathname}/${id}`; const buildDetailsPathname = (id: string) => `${applicationsPathname}/${id}`;
const Applications = () => { const Applications = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { pathname } = useLocation(); const { pathname, search } = useLocation();
const isCreateNew = pathname === createApplicationPathname; const isCreateNew = pathname === createApplicationPathname;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [query, setQuery] = useSearchParams();
const search = query.toString(); const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
const pageIndex = Number(query.get('page') ?? '1'); page: 1,
});
const url = buildUrl('/api/applications', { const url = buildUrl('/api/applications', {
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
}); });
@ -137,12 +140,12 @@ const Applications = () => {
onRetry={async () => mutate(undefined, true)} onRetry={async () => mutate(undefined, true)}
/> />
<Pagination <Pagination
pageIndex={pageIndex} page={page}
totalCount={totalCount} totalCount={totalCount}
pageSize={pageSize} pageSize={pageSize}
className={styles.pagination} className={styles.pagination}
onChange={(page) => { onChange={(page) => {
setQuery({ page: String(page) }); updateSearchParameters({ page });
}} }}
/> />
</div> </div>

View file

@ -8,14 +8,17 @@ import useSWR from 'swr';
import ConfirmModal from '@/components/ConfirmModal'; import ConfirmModal from '@/components/ConfirmModal';
import PermissionsTable from '@/components/PermissionsTable'; import PermissionsTable from '@/components/PermissionsTable';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import useTableSearchParams, { formatKeyword } from '@/hooks/use-table-search-params'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utilities/url'; import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import type { RoleDetailsOutletContext } from '../types'; import type { RoleDetailsOutletContext } from '../types';
import AssignPermissionsModal from './components/AssignPermissionsModal'; import AssignPermissionsModal from './components/AssignPermissionsModal';
const pageSize = defaultPageSize;
const RolePermissions = () => { const RolePermissions = () => {
const { const {
role: { id: roleId }, role: { id: roleId },
@ -23,17 +26,17 @@ const RolePermissions = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { const [{ page, keyword }, updateSearchParameters] = useSearchParametersWatcher({
pagination: { pageIndex, pageSize, setPageIndex }, page: 1,
search: { keyword, setKeyword }, keyword: '',
} = useTableSearchParams(); });
const { data, error, mutate } = useSWR<[ScopeResponse[], number], RequestError>( const { data, error, mutate } = useSWR<[ScopeResponse[], number], RequestError>(
roleId && roleId &&
buildUrl(`/api/roles/${roleId}/scopes`, { buildUrl(`/api/roles/${roleId}/scopes`, {
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
...conditional(keyword && { search: formatKeyword(keyword) }), ...conditional(keyword && { search: formatSearchKeyword(keyword) }),
}) })
); );
@ -80,20 +83,20 @@ const RolePermissions = () => {
errorMessage={error?.body?.message ?? error?.message} errorMessage={error?.body?.message ?? error?.message}
retryHandler={async () => mutate(undefined, true)} retryHandler={async () => mutate(undefined, true)}
pagination={{ pagination={{
pageIndex, page,
pageSize, pageSize,
totalCount, totalCount,
onChange: setPageIndex, onChange: (page) => {
updateSearchParameters({ page });
},
}} }}
search={{ search={{
keyword, keyword,
searchHandler: (value) => { searchHandler: (keyword) => {
setKeyword(value); updateSearchParameters({ keyword, page: 1 });
setPageIndex(1);
}, },
clearSearchHandler: () => { clearSearchHandler: () => {
setKeyword(''); updateSearchParameters({ keyword: '', page: 1 });
setPageIndex(1);
}, },
}} }}
/> />

View file

@ -18,15 +18,18 @@ import Search from '@/components/Search';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { Tooltip } from '@/components/Tip'; import { Tooltip } from '@/components/Tip';
import UserAvatar from '@/components/UserAvatar'; import UserAvatar from '@/components/UserAvatar';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import useTableSearchParams, { formatKeyword } from '@/hooks/use-table-search-params'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utilities/url'; import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import type { RoleDetailsOutletContext } from '../types'; import type { RoleDetailsOutletContext } from '../types';
import AssignUsersModal from './components/AssignUsersModal'; import AssignUsersModal from './components/AssignUsersModal';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
const RoleUsers = () => { const RoleUsers = () => {
const { const {
role: { id: roleId }, role: { id: roleId },
@ -34,17 +37,17 @@ const RoleUsers = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { const [{ page, keyword }, updateSearchParameters] = useSearchParametersWatcher({
pagination: { pageIndex, pageSize, setPageIndex }, page: 1,
search: { keyword, setKeyword }, keyword: '',
} = useTableSearchParams(); });
const { data, error, mutate } = useSWR<[User[], number], RequestError>( const { data, error, mutate } = useSWR<[User[], number], RequestError>(
roleId && roleId &&
buildUrl(`/api/roles/${roleId}/users`, { buildUrl(`/api/roles/${roleId}/users`, {
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
...conditional(keyword && { search: formatKeyword(keyword) }), ...conditional(keyword && { search: formatSearchKeyword(keyword) }),
}) })
); );
@ -132,13 +135,11 @@ const RoleUsers = () => {
defaultValue={keyword} defaultValue={keyword}
isClearable={Boolean(keyword)} isClearable={Boolean(keyword)}
placeholder={t('general.search_placeholder')} placeholder={t('general.search_placeholder')}
onSearch={(value) => { onSearch={(keyword) => {
setKeyword(value); updateSearchParameters({ keyword, page: 1 });
setPageIndex(1);
}} }}
onClearSearch={() => { onClearSearch={() => {
setKeyword(''); updateSearchParameters({ keyword: '', page: 1 });
setPageIndex(1);
}} }}
/> />
<Button <Button
@ -153,10 +154,12 @@ const RoleUsers = () => {
</div> </div>
} }
pagination={{ pagination={{
pageIndex, page,
pageSize, pageSize,
totalCount, totalCount,
onChange: setPageIndex, onChange: (page) => {
updateSearchParameters({ page });
},
}} }}
placeholder={{ placeholder={{
content: ( content: (

View file

@ -10,10 +10,11 @@ import CardTitle from '@/components/CardTitle';
import ItemPreview from '@/components/ItemPreview'; import ItemPreview from '@/components/ItemPreview';
import Search from '@/components/Search'; import Search from '@/components/Search';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useTableSearchParams, { formatKeyword } from '@/hooks/use-table-search-params'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as pageStyles from '@/scss/resources.module.scss'; import * as pageStyles from '@/scss/resources.module.scss';
import { buildUrl } from '@/utilities/url'; import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import AssignedUsers from './components/AssignedUsers'; import AssignedUsers from './components/AssignedUsers';
import CreateRoleModal from './components/CreateRoleModal'; import CreateRoleModal from './components/CreateRoleModal';
@ -23,21 +24,23 @@ const rolesPathname = '/roles';
const createRolePathname = `${rolesPathname}/create`; const createRolePathname = `${rolesPathname}/create`;
const buildDetailsPathname = (id: string) => `${rolesPathname}/${id}`; const buildDetailsPathname = (id: string) => `${rolesPathname}/${id}`;
const pageSize = defaultPageSize;
const Roles = () => { const Roles = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { pathname, search } = useLocation(); const { pathname, search } = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const isOnCreatePage = pathname === createRolePathname; const isOnCreatePage = pathname === createRolePathname;
const { const [{ page, keyword }, updateSearchParameters] = useSearchParametersWatcher({
pagination: { pageIndex, pageSize, setPageIndex }, page: 1,
search: { keyword, setKeyword }, keyword: '',
} = useTableSearchParams(); });
const url = buildUrl('/api/roles', { const url = buildUrl('/api/roles', {
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
...conditional(keyword && { search: formatKeyword(keyword) }), ...conditional(keyword && { search: formatSearchKeyword(keyword) }),
}); });
const { data, error, mutate } = useSWR<[RoleResponse[], number], RequestError>(url); const { data, error, mutate } = useSWR<[RoleResponse[], number], RequestError>(url);
@ -97,21 +100,21 @@ const Roles = () => {
placeholder={t('roles.search')} placeholder={t('roles.search')}
defaultValue={keyword} defaultValue={keyword}
isClearable={Boolean(keyword)} isClearable={Boolean(keyword)}
onSearch={(value) => { onSearch={(keyword) => {
setKeyword(value); updateSearchParameters({ keyword, page: 1 });
setPageIndex(1);
}} }}
onClearSearch={() => { onClearSearch={() => {
setKeyword(''); updateSearchParameters({ keyword: '', page: 1 });
setPageIndex(1);
}} }}
/> />
} }
pagination={{ pagination={{
pageIndex, page,
totalCount, totalCount,
pageSize, pageSize,
onChange: setPageIndex, onChange: (page) => {
updateSearchParameters({ page });
},
}} }}
placeholder={{ placeholder={{
content: ( content: (

View file

@ -15,31 +15,34 @@ import Search from '@/components/Search';
import Table from '@/components/Table'; import Table from '@/components/Table';
import TextLink from '@/components/TextLink'; import TextLink from '@/components/TextLink';
import { Tooltip } from '@/components/Tip'; import { Tooltip } from '@/components/Tip';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import useTableSearchParams, { formatKeyword } from '@/hooks/use-table-search-params'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utilities/url'; import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import type { UserDetailsOutletContext } from '../types'; import type { UserDetailsOutletContext } from '../types';
import AssignRolesModal from './components/AssignRolesModal'; import AssignRolesModal from './components/AssignRolesModal';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
const UserRoles = () => { const UserRoles = () => {
const { user } = useOutletContext<UserDetailsOutletContext>(); const { user } = useOutletContext<UserDetailsOutletContext>();
const { id: userId } = user; const { id: userId } = user;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { const [{ page, keyword }, updateSearchParameters] = useSearchParametersWatcher({
pagination: { pageIndex, pageSize, setPageIndex }, page: 1,
search: { keyword, setKeyword }, keyword: '',
} = useTableSearchParams(); });
const { data, error, mutate } = useSWR<[Role[], number], RequestError>( const { data, error, mutate } = useSWR<[Role[], number], RequestError>(
buildUrl(`/api/users/${userId}/roles`, { buildUrl(`/api/users/${userId}/roles`, {
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
...conditional(keyword && { search: formatKeyword(keyword) }), ...conditional(keyword && { search: formatSearchKeyword(keyword) }),
}) })
); );
@ -117,13 +120,11 @@ const UserRoles = () => {
defaultValue={keyword} defaultValue={keyword}
isClearable={Boolean(keyword)} isClearable={Boolean(keyword)}
placeholder={t('user_details.roles.search')} placeholder={t('user_details.roles.search')}
onSearch={(value) => { onSearch={(keyword) => {
setKeyword(value); updateSearchParameters({ keyword, page: 1 });
setPageIndex(1);
}} }}
onClearSearch={() => { onClearSearch={() => {
setKeyword(''); updateSearchParameters({ keyword: '', page: 1 });
setPageIndex(1);
}} }}
/> />
<Button <Button
@ -138,10 +139,12 @@ const UserRoles = () => {
</div> </div>
} }
pagination={{ pagination={{
pageIndex, page,
pageSize, pageSize,
totalCount, totalCount,
onChange: setPageIndex, onChange: (page) => {
updateSearchParameters({ page });
},
}} }}
placeholder={{ placeholder={{
content: ( content: (

View file

@ -13,15 +13,17 @@ import ItemPreview from '@/components/ItemPreview';
import Search from '@/components/Search'; import Search from '@/components/Search';
import Table from '@/components/Table'; import Table from '@/components/Table';
import UserAvatar from '@/components/UserAvatar'; import UserAvatar from '@/components/UserAvatar';
import { defaultPageSize } from '@/consts';
import { UserDetailsTabs } from '@/consts/page-tabs'; import { UserDetailsTabs } from '@/consts/page-tabs';
import type { RequestError } from '@/hooks/use-api'; import type { RequestError } from '@/hooks/use-api';
import useTableSearchParams from '@/hooks/use-table-search-params'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as resourcesStyles from '@/scss/resources.module.scss'; import * as resourcesStyles from '@/scss/resources.module.scss';
import { buildUrl } from '@/utilities/url'; import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import CreateForm from './components/CreateForm'; import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
const usersPathname = '/users'; const usersPathname = '/users';
const createUserPathname = `${usersPathname}/create`; const createUserPathname = `${usersPathname}/create`;
const buildDetailsPathname = (id: string) => `${usersPathname}/${id}/${UserDetailsTabs.Settings}`; const buildDetailsPathname = (id: string) => `${usersPathname}/${id}/${UserDetailsTabs.Settings}`;
@ -30,16 +32,17 @@ const Users = () => {
const { pathname, search } = useLocation(); const { pathname, search } = useLocation();
const isCreateNew = pathname === createUserPathname; const isCreateNew = pathname === createUserPathname;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
pagination: { pageIndex, pageSize, setPageIndex }, const [{ page, keyword }, updateSearchParameters] = useSearchParametersWatcher({
search: { keyword, setKeyword }, page: 1,
} = useTableSearchParams(); keyword: '',
});
const url = buildUrl('/api/users', { const url = buildUrl('/api/users', {
hideAdminUser: String(true), hideAdminUser: String(true),
page: String(pageIndex), page: String(page),
page_size: String(pageSize), page_size: String(pageSize),
...conditional(keyword && { search: `%${keyword}%` }), ...conditional(keyword && { search: formatSearchKeyword(keyword) }),
}); });
const { data, error, mutate } = useSWR<[User[], number], RequestError>(url); const { data, error, mutate } = useSWR<[User[], number], RequestError>(url);
@ -115,13 +118,11 @@ const Users = () => {
placeholder={t('users.search')} placeholder={t('users.search')}
defaultValue={keyword} defaultValue={keyword}
isClearable={Boolean(keyword)} isClearable={Boolean(keyword)}
onSearch={(value) => { onSearch={(keyword) => {
setKeyword(value); updateSearchParameters({ keyword, page: 1 });
setPageIndex(1);
}} }}
onClearSearch={() => { onClearSearch={() => {
setKeyword(''); updateSearchParameters({ keyword: '', page: 1 });
setPageIndex(1);
}} }}
/> />
} }
@ -143,10 +144,12 @@ const Users = () => {
navigate(buildDetailsPathname(id)); navigate(buildDetailsPathname(id));
}} }}
pagination={{ pagination={{
pageIndex, page,
pageSize, pageSize,
totalCount, totalCount,
onChange: setPageIndex, onChange: (page) => {
updateSearchParameters({ page });
},
}} }}
onRetry={async () => mutate(undefined, true)} onRetry={async () => mutate(undefined, true)}
/> />

View file

@ -1,2 +1,4 @@
export const buildUrl = (path: string, searchParameters: Record<string, string>) => export const buildUrl = (path: string, searchParameters: Record<string, string>) =>
`${path}?${new URLSearchParams(searchParameters).toString()}`; `${path}?${new URLSearchParams(searchParameters).toString()}`;
export const formatSearchKeyword = (keyword: string) => `%${keyword}%`;