From e72e14b7ea9c328554099387f9091987e5cf97bd Mon Sep 17 00:00:00 2001 From: kage1414 Date: Sat, 17 Aug 2024 11:45:34 -0400 Subject: [PATCH 01/12] feat: added user search field (#3771) --- src/components/UserList/index.tsx | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index b37751f4f3..2e82c33620 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -20,6 +20,7 @@ import { ChevronLeftIcon, ChevronRightIcon, InboxArrowDownIcon, + MagnifyingGlassIcon, PencilIcon, UserPlusIcon, } from '@heroicons/react/24/solid'; @@ -76,6 +77,7 @@ const messages = defineMessages({ sortRequests: 'Request Count', localLoginDisabled: 'The Enable Local Sign-In setting is currently disabled.', + searchUsersPlaceholder: 'Search Users', }); type Sort = 'created' | 'updated' | 'requests' | 'displayname'; @@ -88,6 +90,7 @@ const UserList = () => { const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const [currentSort, setCurrentSort] = useState('displayname'); const [currentPageSize, setCurrentPageSize] = useState(10); + const [searchString, setSearchString] = useState(''); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; @@ -526,6 +529,32 @@ const UserList = () => { +
+
+
+
+
+ +
+ 0 ? '1.75rem' : '', + }} + className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base" + autoComplete="off" + placeholder={intl.formatMessage( + messages.searchUsersPlaceholder + )} + onChange={(e) => { + setSearchString(e.target.value); + }} + /> +
+
+
+
From 52e8a2cbb23539b6b5bb75b86e534cad96d8f122 Mon Sep 17 00:00:00 2001 From: kage1414 Date: Sat, 17 Aug 2024 14:06:42 -0400 Subject: [PATCH 02/12] feat: add search query (#3771) --- overseerr-api.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/overseerr-api.yml b/overseerr-api.yml index c4c1e97b74..7075c2dc27 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -3460,6 +3460,12 @@ paths: type: string enum: [created, updated, requests, displayname] default: created + - in: query + name: searchQuery + schema: + type: string + nullable: true + example: 'steve@gmail.com' responses: '200': description: A JSON array of all users From f51a5fb1e1e19f70268138a177ccc2cb748a03e4 Mon Sep 17 00:00:00 2001 From: kage1414 Date: Sat, 17 Aug 2024 14:38:14 -0400 Subject: [PATCH 03/12] feat: search from db (#3771) --- server/routes/user/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 94784df51a..c58d0f1c46 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -30,8 +30,18 @@ router.get('/', async (req, res, next) => { try { const pageSize = req.query.take ? Number(req.query.take) : 10; const skip = req.query.skip ? Number(req.query.skip) : 0; + const searchQuery = + req.query.searchQuery === 'null' || req.query.searchQuery === null + ? null + : req.query.searchQuery; let query = getRepository(User).createQueryBuilder('user'); + if (searchQuery) { + await query.where('user.email like :query OR user.username like :query', { + query: `%${searchQuery}%`, + }); + } + switch (req.query.sort) { case 'updated': query = query.orderBy('user.updatedAt', 'DESC'); From 9aff74e41fc1ba766173e2fa434a82f7c81d526c Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sat, 17 Aug 2024 15:44:00 -0400 Subject: [PATCH 04/12] refactor(userlist): cleanup request payload and BE handling --- server/routes/user/index.ts | 5 +- src/components/UserList/index.tsx | 81 +++++++++++++++++++------------ 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index c58d0f1c46..42754d6882 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -30,10 +30,7 @@ router.get('/', async (req, res, next) => { try { const pageSize = req.query.take ? Number(req.query.take) : 10; const skip = req.query.skip ? Number(req.query.skip) : 0; - const searchQuery = - req.query.searchQuery === 'null' || req.query.searchQuery === null - ? null - : req.query.searchQuery; + const searchQuery = req.query.searchQuery; let query = getRepository(User).createQueryBuilder('user'); if (searchQuery) { diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 2e82c33620..07ec82014f 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -28,8 +28,10 @@ import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces' import { hasPermission } from '@server/lib/permissions'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; +import { debounce } from 'lodash'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import type React from 'react'; import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; @@ -82,7 +84,52 @@ const messages = defineMessages({ type Sort = 'created' | 'updated' | 'requests' | 'displayname'; -const UserList = () => { +const UserListContainer = () => { + const intl = useIntl(); + const [searchString, setSearchString] = useState(''); + + const debounceSetSearchString = debounce((str: string) => { + setSearchString(str); + }, 200); + + return ( + <> +
+
+
+
+
+ +
+ 0 ? '1.75rem' : '', + }} + className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base" + autoComplete="off" + placeholder={intl.formatMessage( + messages.searchUsersPlaceholder + )} + onChange={(e) => { + debounceSetSearchString(e.target.value); + }} + /> +
+
+
+
+ + + ); +}; + +interface UserListProps { + searchString?: string; +} + +const UserList = ({ searchString }: UserListProps) => { const intl = useIntl(); const router = useRouter(); const settings = useSettings(); @@ -90,7 +137,6 @@ const UserList = () => { const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const [currentSort, setCurrentSort] = useState('displayname'); const [currentPageSize, setCurrentPageSize] = useState(10); - const [searchString, setSearchString] = useState(''); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; @@ -103,7 +149,7 @@ const UserList = () => { } = useSWR( `/api/v1/user?take=${currentPageSize}&skip=${ pageIndex * currentPageSize - }&sort=${currentSort}` + }&searchQuery=${searchString ? searchString : '%00'}&sort=${currentSort}` ); const [isDeleting, setDeleting] = useState(false); @@ -529,32 +575,7 @@ const UserList = () => { -
-
-
-
-
- -
- 0 ? '1.75rem' : '', - }} - className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base" - autoComplete="off" - placeholder={intl.formatMessage( - messages.searchUsersPlaceholder - )} - onChange={(e) => { - setSearchString(e.target.value); - }} - /> -
-
-
-
+
@@ -779,4 +800,4 @@ const UserList = () => { ); }; -export default UserList; +export default UserListContainer; From 8e65598da9ee2e8fe643a63f325690f588008f3b Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sun, 18 Aug 2024 13:25:31 -0400 Subject: [PATCH 05/12] refactor(user list): move user list back to one component 3771 --- src/components/UserList/index.tsx | 509 ++++++++++++++---------------- 1 file changed, 245 insertions(+), 264 deletions(-) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 07ec82014f..6e124aebe3 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -84,52 +84,7 @@ const messages = defineMessages({ type Sort = 'created' | 'updated' | 'requests' | 'displayname'; -const UserListContainer = () => { - const intl = useIntl(); - const [searchString, setSearchString] = useState(''); - - const debounceSetSearchString = debounce((str: string) => { - setSearchString(str); - }, 200); - - return ( - <> -
-
-
-
-
- -
- 0 ? '1.75rem' : '', - }} - className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base" - autoComplete="off" - placeholder={intl.formatMessage( - messages.searchUsersPlaceholder - )} - onChange={(e) => { - debounceSetSearchString(e.target.value); - }} - /> -
-
-
-
- - - ); -}; - -interface UserListProps { - searchString?: string; -} - -const UserList = ({ searchString }: UserListProps) => { +const UserList = () => { const intl = useIntl(); const router = useRouter(); const settings = useSettings(); @@ -137,16 +92,17 @@ const UserList = ({ searchString }: UserListProps) => { const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const [currentSort, setCurrentSort] = useState('displayname'); const [currentPageSize, setCurrentPageSize] = useState(10); + const [searchString, setSearchString] = useState(''); + + const debounceSetSearchString = debounce((str: string) => { + setSearchString(str); + }, 200); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); - const { - data, - error, - mutate: revalidate, - } = useSWR( + const { data, mutate: revalidate } = useSWR( `/api/v1/user?take=${currentPageSize}&skip=${ pageIndex * currentPageSize }&searchQuery=${searchString ? searchString : '%00'}&sort=${currentSort}` @@ -243,10 +199,6 @@ const UserList = ({ searchString }: UserListProps) => { } }; - if (!data && !error) { - return ; - } - const CreateUserSchema = Yup.object().shape({ email: Yup.string() .required(intl.formatMessage(messages.validationEmail)) @@ -261,11 +213,7 @@ const UserList = ({ searchString }: UserListProps) => { ), }); - if (!data) { - return ; - } - - const hasNextPage = data.pageInfo.pages > pageIndex + 1; + const hasNextPage = (data?.pageInfo.pages ?? 0) > pageIndex + 1; const hasPrevPage = pageIndex > 0; const passwordGenerationEnabled = @@ -504,7 +452,7 @@ const UserList = ({ searchString }: UserListProps) => { revalidate(); }} selectedUserIds={selectedUsers} - users={data.results} + users={data?.results ?? []} /> @@ -535,6 +483,7 @@ const UserList = ({ searchString }: UserListProps) => { className="mb-2 flex-grow sm:mb-0 sm:mr-2" buttonType="primary" onClick={() => setCreateModal({ isOpen: true })} + disabled={!data} > {intl.formatMessage(messages.createlocaluser)} @@ -543,6 +492,7 @@ const UserList = ({ searchString }: UserListProps) => { className="flex-grow lg:mr-2" buttonType="primary" onClick={() => setShowImportModal(true)} + disabled={!data} > {intl.formatMessage(messages.importfromplex)} @@ -561,6 +511,7 @@ const UserList = ({ searchString }: UserListProps) => { }} value={currentSort} className="rounded-r-only" + disabled={!data} >
- - - - {(data.results ?? []).length > 1 && ( - { - toggleAllUsers(); - }} - /> - )} - - {intl.formatMessage(messages.user)} - {intl.formatMessage(messages.totalrequests)} - {intl.formatMessage(messages.accounttype)} - {intl.formatMessage(messages.role)} - {intl.formatMessage(messages.created)} - - {(data.results ?? []).length > 1 && ( - - )} - - - - - {data?.results.map((user) => ( - - - {isUserPermsEditable(user.id) && ( +
+
+
+
+
+ +
+ 0 ? '1.75rem' : '', + }} + className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base" + autoComplete="off" + placeholder={intl.formatMessage( + messages.searchUsersPlaceholder + )} + onChange={(e) => { + debounceSetSearchString(e.target.value); + }} + /> +
+
+
+
+ {data ? ( +
+ + + + {(data.results ?? []).length > 1 && ( { - toggleUser(user.id); + toggleAllUsers(); }} /> )} - - -
- - - - - -
+ + {intl.formatMessage(messages.user)} + {intl.formatMessage(messages.totalrequests)} + {intl.formatMessage(messages.accounttype)} + {intl.formatMessage(messages.role)} + {intl.formatMessage(messages.created)} + + {(data.results ?? []).length > 1 && ( + + )} + +
+ + + {data?.results.map((user) => ( + + + {isUserPermsEditable(user.id) && ( + { + toggleUser(user.id); + }} + /> + )} + + +
- - {user.displayName} + + - {user.displayName.toLowerCase() !== user.email && ( -
- {user.email} -
- )} -
- -
- - {user.id === currentUser?.id || - currentHasPermission( - [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], - { type: 'or' } - ) ? ( - - - {user.requestCount} - - - ) : ( - user.requestCount - )} - - - {user.userType === UserType.PLEX ? ( - - {intl.formatMessage(messages.plexuser)} - - ) : ( - - {intl.formatMessage(messages.localuser)} - - )} - - - {user.id === 1 - ? intl.formatMessage(messages.owner) - : hasPermission(Permission.ADMIN, user.permissions) - ? intl.formatMessage(messages.admin) - : intl.formatMessage(messages.user)} - - - {intl.formatDate(user.createdAt, { - year: 'numeric', - month: 'long', - day: 'numeric', - })} - - - - - - - ))} - - - - - - -
+ + + ))} + + + + + + + + ) : ( + + )} ); }; -export default UserListContainer; +export default UserList; From c720ffea4eb16885cfbd6fb507137505927e0dd6 Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sun, 18 Aug 2024 13:32:12 -0400 Subject: [PATCH 06/12] refactor(loading indicator): show loading indicator if no data and if loading 3771 --- src/components/UserList/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 6e124aebe3..c57233b81e 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -102,7 +102,11 @@ const UserList = () => { const pageIndex = page - 1; const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); - const { data, mutate: revalidate } = useSWR( + const { + data, + mutate: revalidate, + isLoading, + } = useSWR( `/api/v1/user?take=${currentPageSize}&skip=${ pageIndex * currentPageSize }&searchQuery=${searchString ? searchString : '%00'}&sort=${currentSort}` @@ -552,7 +556,7 @@ const UserList = () => { - {data ? ( + {data && !isLoading ? ( From 037ee45e9d16c4262dcd5b2fbf6ffce2cf935a6b Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sun, 18 Aug 2024 13:38:32 -0400 Subject: [PATCH 07/12] style(search bar width): cleanup excess containers. adjust width removes extra containers surrounding search bar. Limits width to 25% 3771 --- src/components/UserList/index.tsx | 38 +++++++++++++------------------ 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index c57233b81e..9fe30fb3d3 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -531,29 +531,23 @@ const UserList = () => {
-
-
-
-
- -
- 0 ? '1.75rem' : '', - }} - className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base" - autoComplete="off" - placeholder={intl.formatMessage( - messages.searchUsersPlaceholder - )} - onChange={(e) => { - debounceSetSearchString(e.target.value); - }} - /> -
+
+
+
+ 0 ? '1.75rem' : '', + }} + className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base" + autoComplete="off" + placeholder={intl.formatMessage(messages.searchUsersPlaceholder)} + onChange={(e) => { + debounceSetSearchString(e.target.value); + }} + />
{data && !isLoading ? ( From 34829fe1417a77f33a8116e1eb4066112d742e04 Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sun, 18 Aug 2024 13:47:31 -0400 Subject: [PATCH 08/12] style(search bar dimensions): cleans up styling on user search bar --- src/components/UserList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 9fe30fb3d3..0705c2f112 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -531,7 +531,7 @@ const UserList = () => {
-
+
From 776f14bf5420fa11587e90dad73aa1abf5c19372 Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sun, 18 Aug 2024 13:58:12 -0400 Subject: [PATCH 09/12] fix(loading state & url encode): only show loading indicator if data undefined AND isLoading Shows empty list if data is undefined. Should not show loading if not loading --- src/components/UserList/index.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 0705c2f112..ca8fb465e0 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -109,7 +109,9 @@ const UserList = () => { } = useSWR( `/api/v1/user?take=${currentPageSize}&skip=${ pageIndex * currentPageSize - }&searchQuery=${searchString ? searchString : '%00'}&sort=${currentSort}` + }&searchQuery=${ + searchString ? encodeURIComponent(searchString) : '%00' + }&sort=${currentSort}` ); const [isDeleting, setDeleting] = useState(false); @@ -550,12 +552,14 @@ const UserList = () => { />
- {data && !isLoading ? ( + {!data && isLoading ? ( + + ) : (
- {(data.results ?? []).length > 1 && ( + {(data?.results ?? []).length > 1 && ( { {intl.formatMessage(messages.role)} {intl.formatMessage(messages.created)} - {(data.results ?? []).length > 1 && ( + {(data?.results ?? []).length > 1 && (
- ) : ( - )} ); From 52729b6fd515e99aa97920cb63cbf8f64b0fd4cd Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sun, 18 Aug 2024 14:06:48 -0400 Subject: [PATCH 10/12] feat(update i18n): adds placeholder to i18n --- src/i18n/locale/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 10165c9e15..76840da002 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1058,6 +1058,7 @@ "components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.", "components.UserList.plexuser": "Plex User", "components.UserList.role": "Role", + "components.UserList.searchUsersPlaceholder": "Search Users", "components.UserList.sortCreated": "Join Date", "components.UserList.sortDisplayName": "Display Name", "components.UserList.sortRequests": "Request Count", From 945a1782ae7dd7140edb43e346ad129fd4d27667 Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Sat, 5 Oct 2024 20:25:36 -0400 Subject: [PATCH 11/12] refactor(user list): add custom hook for fetching users 3771 --- src/components/UserList/hooks.tsx | 36 ++++++++++++++++++++ src/components/UserList/index.tsx | 56 ++++++++++++------------------- 2 files changed, 58 insertions(+), 34 deletions(-) create mode 100644 src/components/UserList/hooks.tsx diff --git a/src/components/UserList/hooks.tsx b/src/components/UserList/hooks.tsx new file mode 100644 index 0000000000..b41af4ea53 --- /dev/null +++ b/src/components/UserList/hooks.tsx @@ -0,0 +1,36 @@ +import type { Sort } from '@app/components/UserList'; +import type { User } from '@app/hooks/useUser'; +import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; +import { isEqual } from 'lodash'; +import { useEffect, useState } from 'react'; +import useSWR from 'swr'; + +export const useUsers = ({ + pageIndex, + currentPageSize, + searchString, + currentSort, +}: { + pageIndex: number; + currentPageSize: number; + searchString: string; + currentSort: Sort; +}) => { + const [users, setUsers] = useState([]); + + const { data, mutate, isLoading } = useSWR( + `/api/v1/user?take=${currentPageSize}&skip=${ + pageIndex * currentPageSize + }&searchQuery=${ + searchString ? encodeURIComponent(searchString) : '%00' + }&sort=${currentSort}` + ); + + useEffect(() => { + if (!isLoading && data?.results && !isEqual(data.results, users)) { + setUsers(data.results); + } + }, [isLoading, data?.results, users]); + + return { users, mutate, isLoading, pageInfo: data?.pageInfo }; +}; diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index ca8fb465e0..20116280ad 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -8,6 +8,7 @@ import PageTitle from '@app/components/Common/PageTitle'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import Table from '@app/components/Common/Table'; import BulkEditModal from '@app/components/UserList/BulkEditModal'; +import { useUsers } from '@app/components/UserList/hooks'; import PlexImportModal from '@app/components/UserList/PlexImportModal'; import useSettings from '@app/hooks/useSettings'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; @@ -24,7 +25,6 @@ import { PencilIcon, UserPlusIcon, } from '@heroicons/react/24/solid'; -import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import { hasPermission } from '@server/lib/permissions'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; @@ -35,7 +35,7 @@ import type React from 'react'; import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; + import * as Yup from 'yup'; const messages = defineMessages({ @@ -82,7 +82,7 @@ const messages = defineMessages({ searchUsersPlaceholder: 'Search Users', }); -type Sort = 'created' | 'updated' | 'requests' | 'displayname'; +export type Sort = 'created' | 'updated' | 'requests' | 'displayname'; const UserList = () => { const intl = useIntl(); @@ -103,16 +103,11 @@ const UserList = () => { const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); const { - data, mutate: revalidate, isLoading, - } = useSWR( - `/api/v1/user?take=${currentPageSize}&skip=${ - pageIndex * currentPageSize - }&searchQuery=${ - searchString ? encodeURIComponent(searchString) : '%00' - }&sort=${currentSort}` - ); + users, + pageInfo, + } = useUsers({ currentPageSize, pageIndex, searchString, currentSort }); const [isDeleting, setDeleting] = useState(false); const [showImportModal, setShowImportModal] = useState(false); @@ -156,20 +151,14 @@ const UserList = () => { const isAllUsersSelected = () => { return ( selectedUsers.length === - data?.results.filter((user) => user.id !== currentUser?.id).length + users.filter((user) => user.id !== currentUser?.id).length ); }; const isUserSelected = (userId: number) => selectedUsers.includes(userId); const toggleAllUsers = () => { - if ( - data && - selectedUsers.length >= 0 && - selectedUsers.length < data?.results.length - 1 - ) { + if (selectedUsers.length >= 0 && selectedUsers.length < users.length - 1) { setSelectedUsers( - data.results - .filter((user) => isUserPermsEditable(user.id)) - .map((u) => u.id) + users.filter((user) => isUserPermsEditable(user.id)).map((u) => u.id) ); } else { setSelectedUsers([]); @@ -219,7 +208,7 @@ const UserList = () => { ), }); - const hasNextPage = (data?.pageInfo.pages ?? 0) > pageIndex + 1; + const hasNextPage = (pageInfo?.pages ?? 0) > pageIndex + 1; const hasPrevPage = pageIndex > 0; const passwordGenerationEnabled = @@ -458,7 +447,7 @@ const UserList = () => { revalidate(); }} selectedUserIds={selectedUsers} - users={data?.results ?? []} + users={users} /> @@ -489,7 +478,7 @@ const UserList = () => { className="mb-2 flex-grow sm:mb-0 sm:mr-2" buttonType="primary" onClick={() => setCreateModal({ isOpen: true })} - disabled={!data} + disabled={!users.length} > {intl.formatMessage(messages.createlocaluser)} @@ -498,7 +487,7 @@ const UserList = () => { className="flex-grow lg:mr-2" buttonType="primary" onClick={() => setShowImportModal(true)} - disabled={!data} + disabled={!users.length} > {intl.formatMessage(messages.importfromplex)} @@ -517,7 +506,7 @@ const UserList = () => { }} value={currentSort} className="rounded-r-only" - disabled={!data} + disabled={!users.length} >