mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
Admin tools: optimize user/project list performance
Add frontend pagination Add search debouncing Render modals conditionally Remove unnecessary sorting
This commit is contained in:
@@ -1442,6 +1442,7 @@
|
||||
"per_license": "",
|
||||
"per_month": "",
|
||||
"per_month_x_annually": "",
|
||||
"per_page": "",
|
||||
"per_year": "",
|
||||
"percent_is_the_percentage_of_the_line_width": "",
|
||||
"permanently_delete_accounts": "",
|
||||
@@ -1840,7 +1841,6 @@
|
||||
"sharing_permissions": "",
|
||||
"shortcut_to_open_advanced_reference_search": "",
|
||||
"show_all_projects": "",
|
||||
"show_all_users": "",
|
||||
"show_breadcrumbs": "",
|
||||
"show_document_preamble": "",
|
||||
"show_equation_preview": "",
|
||||
@@ -1854,7 +1854,6 @@
|
||||
"show_outline": "",
|
||||
"show_version_history": "",
|
||||
"show_x_more_projects": "",
|
||||
"show_x_more_users": "",
|
||||
"showing_1_result": "",
|
||||
"showing_1_result_of_total": "",
|
||||
"showing_pdf_preview_with_inverted_colors": "",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
onPageChange: (page: number) => void
|
||||
}
|
||||
|
||||
export default function Pagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}: PaginationProps) {
|
||||
const { t } = useTranslation()
|
||||
if (totalPages <= 1) return null
|
||||
|
||||
const pageNumbers: (number | "...")[] = []
|
||||
let startPage = Math.max(1, currentPage - 4)
|
||||
let endPage = Math.min(totalPages, currentPage + 4)
|
||||
|
||||
if (startPage > 1) {
|
||||
pageNumbers.push(1)
|
||||
if (startPage > 2) {
|
||||
pageNumbers.push("...")
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pageNumbers.push(i)
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
pageNumbers.push("...")
|
||||
}
|
||||
pageNumbers.push(totalPages)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav role="navigation" aria-label={t('pagination_navigation')}>
|
||||
<ul className="pagination">
|
||||
{/*
|
||||
{currentPage > 1 && (
|
||||
<li>
|
||||
<button aria-label={t('go_to_first_page')} onClick={() => onPageChange(1)}>
|
||||
<< {t('first')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
*/}
|
||||
{currentPage > 1 && (
|
||||
<li>
|
||||
<button aria-label={t('go_prev_page')} onClick={() => onPageChange(currentPage - 1)}>
|
||||
< {t('prev')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
{pageNumbers.map((page, index) => (
|
||||
<li key={index} className={page === currentPage ? "active" : ""}>
|
||||
{page === "..." ? (
|
||||
<span aria-hidden="true">{page}</span>
|
||||
) : page === currentPage ? (
|
||||
<span aria-label={t('page_current', { page })} aria-current="true">{page}</span>
|
||||
) : (
|
||||
<button aria-label={t('go_page', { page })} onClick={() => onPageChange(page)}>
|
||||
{page}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
{currentPage < totalPages && (
|
||||
<li>
|
||||
<button aria-label={t('go_next_page')} onClick={() => onPageChange(currentPage + 1)}>
|
||||
{t('next')} >
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
{/*
|
||||
{currentPage < totalPages && (
|
||||
<li>
|
||||
<button aria-label={t('go_to_last_page')} onClick={() => onPageChange(totalPages)}>
|
||||
{t('last')} >>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
*/}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1159,6 +1159,7 @@
|
||||
"pending": "Ausstehend",
|
||||
"pending_additional_licenses": "Dein Abonnement wird geändert, um <0>__pendingAdditionalLicenses__</0> zusätzliche Lizenz(en) für insgesamt <1>__pendingTotalLicenses__</1> Lizenzen einzuschließen.",
|
||||
"per_month": "pro Monat",
|
||||
"per_page": "pro Seite",
|
||||
"per_user_year": "pro Nutzer / Jahr",
|
||||
"per_year": "pro Jahr",
|
||||
"permanently_delete_accounts": "Konten endgültig löschen",
|
||||
@@ -1401,14 +1402,12 @@
|
||||
"shared_with_you": "Mit dir geteilt",
|
||||
"sharelatex_beta_program": "__appName__ Beta-Programm",
|
||||
"show_all_projects": "Alle Projekte anzeigen",
|
||||
"show_all_users": "Alle Benutzer anzeigen",
|
||||
"show_in_code": "Im Code anzeigen",
|
||||
"show_in_pdf": "Im PDF anzeigen",
|
||||
"show_less": "Weniger anzeigen",
|
||||
"show_live_equation_previews_while_typing": "Live-Gleichungsvorschau beim Tippen anzeigen",
|
||||
"show_outline": "Dateigliederung anzeigen",
|
||||
"show_x_more_projects": "__x__ weitere Projekte anzeigen",
|
||||
"show_x_more_users": "__x__ weitere Benutzer anzeigen",
|
||||
"showing_1_result": "1 Ergebnis wird angezeigt",
|
||||
"showing_1_result_of_total": "Zeige 1 Ergebnis von __total__",
|
||||
"showing_x_out_of_n_projects": "Es werden __x__ von __n__ Projekten angezeigt.",
|
||||
|
||||
@@ -1893,6 +1893,7 @@
|
||||
"per_month": "per month",
|
||||
"per_month_billed_annually": "per month, billed annually",
|
||||
"per_month_x_annually": "per month, __price__ annually",
|
||||
"per_page": "per page",
|
||||
"per_user_year": "per user / year",
|
||||
"per_year": "per year",
|
||||
"percent_is_the_percentage_of_the_line_width": "% is the percentage of the line width",
|
||||
@@ -2382,7 +2383,6 @@
|
||||
"sharing_permissions": "Sharing permissions",
|
||||
"shortcut_to_open_advanced_reference_search": "(<strong>__ctrlSpace__</strong> or <strong>__altSpace__</strong>)",
|
||||
"show_all_projects": "Show all projects",
|
||||
"show_all_users": "Show all users",
|
||||
"show_breadcrumbs": "Show breadcrumbs",
|
||||
"show_document_preamble": "Show document preamble",
|
||||
"show_equation_preview": "Show equation preview",
|
||||
@@ -2396,7 +2396,6 @@
|
||||
"show_outline": "Show File outline",
|
||||
"show_version_history": "Show version history",
|
||||
"show_x_more_projects": "Show __x__ more projects",
|
||||
"show_x_more_users": "Show __x__ more users",
|
||||
"showing_1_result": "Showing 1 result",
|
||||
"showing_1_result_of_total": "Showing 1 result of __total__",
|
||||
"showing_pdf_preview_with_inverted_colors": "Showing PDF preview with inverted colors",
|
||||
|
||||
@@ -325,6 +325,7 @@
|
||||
"password_reset_token_expired": "Ваш код восстановления пароля истёк. Пожалуйста, запросите восстановление пароля по почте ещё раз и перейдите по ссылке в письме.",
|
||||
"pdf_viewer": "Просмотрщик PDF",
|
||||
"pending": "В ожидании",
|
||||
"per_page": "на страницу",
|
||||
"permanently_delete_accounts": "Окончательно удалить аккаунты",
|
||||
"permanently_delete_projects": "Окончательно удалить проекты",
|
||||
"personal": "Личный",
|
||||
@@ -434,9 +435,7 @@
|
||||
"share_project": "Открыть доступ к проекту",
|
||||
"shared_with_you": "Доступные мне",
|
||||
"show_all_projects": "Показать все проекты",
|
||||
"show_all_users": "Показать всех пользователей",
|
||||
"show_x_more_projects": "Показать ещё __x__ проектов",
|
||||
"show_x_more_users": "Показать ещё __x__ пользователей",
|
||||
"showing_x_out_of_n_projects": "Показано __x__ из __n__ проектов",
|
||||
"showing_x_out_of_n_users": "Показано __x__ из __n__ пользователей",
|
||||
"signed_up": "Зарегистрирован",
|
||||
|
||||
@@ -323,8 +323,8 @@ function _matchesFilters(user, filters) {
|
||||
if (
|
||||
filters.search?.length &&
|
||||
user.email.toLowerCase().indexOf(filters.search.toLowerCase()) === -1 &&
|
||||
user.first_name.toLowerCase().indexOf(filters.search.toLowerCase()) === -1 &&
|
||||
user.last_name.toLowerCase().indexOf(filters.search.toLowerCase()) === -1
|
||||
user.firstName?.toLowerCase().indexOf(filters.search.toLowerCase()) === -1 &&
|
||||
user.lastName?.toLowerCase().indexOf(filters.search.toLowerCase()) === -1
|
||||
) { return false }
|
||||
// Deleted users only match the 'deleted' filter
|
||||
if (user.deleted) return Boolean(filters.deleted)
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProjectListContext } from '../context/project-list-context'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
|
||||
export default function LoadMore() {
|
||||
const {
|
||||
visibleProjects,
|
||||
hiddenProjectsCount,
|
||||
loadMoreCount,
|
||||
showAllProjects,
|
||||
loadMoreProjects,
|
||||
} = useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
{hiddenProjectsCount > 0 ? (
|
||||
<>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
className="project-list-load-more-button"
|
||||
onClick={() => loadMoreProjects()}
|
||||
>
|
||||
{t('show_x_more_projects', { x: loadMoreCount })}
|
||||
</OLButton>
|
||||
</>
|
||||
) : null}
|
||||
<p>
|
||||
{hiddenProjectsCount > 0 ? (
|
||||
<>
|
||||
<span aria-live="polite">
|
||||
{t('showing_x_out_of_n_projects', {
|
||||
x: visibleProjects.length,
|
||||
n: visibleProjects.length + hiddenProjectsCount,
|
||||
})}
|
||||
</span>{' '}
|
||||
<OLButton
|
||||
variant="link"
|
||||
onClick={() => showAllProjects()}
|
||||
className="btn-inline-link"
|
||||
>
|
||||
{t('show_all_projects')}
|
||||
</OLButton>
|
||||
</>
|
||||
) : (
|
||||
<span aria-live="polite">
|
||||
{t('showing_x_out_of_n_projects', {
|
||||
x: visibleProjects.length,
|
||||
n: visibleProjects.length,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import OLFormGroup from '@/shared/components/ol/ol-form-group'
|
||||
import OLFormLabel from '@/shared/components/ol/ol-form-label'
|
||||
import OLFormCheckbox from '@/shared/components/ol/ol-form-checkbox'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import sortUsers from '../../../user-list/util/sort-users'
|
||||
|
||||
type TransferProjectModalProps = Pick<
|
||||
React.ComponentProps<typeof ProjectsActionModal>,
|
||||
@@ -35,9 +34,8 @@ function TransferProjectModal({
|
||||
|
||||
const potentialOwners = useMemo(() => {
|
||||
if (!loadedUsers) return null;
|
||||
const sortedUsers = sortUsers(loadedUsers, { by: 'name', order: 'asc' })
|
||||
const result: UserRef[] = []
|
||||
for (const user of sortedUsers) {
|
||||
for (const user of loadedUsers) {
|
||||
if (!user.deleted && user.id !== projectsOwnerId) {
|
||||
result.push({
|
||||
id: user.id,
|
||||
|
||||
@@ -18,6 +18,8 @@ import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||
import { getUserName } from '../util/user'
|
||||
import { useProjectListContext } from '../context/project-list-context'
|
||||
import { useUserIdentityContext } from '../../user-list/context/user-identity-context'
|
||||
import Pagination from '@/shared/components/pagination-cep'
|
||||
import ProjectListSummary from './project-list-summary'
|
||||
|
||||
export function ProjectListDsNav() {
|
||||
|
||||
@@ -32,6 +34,9 @@ export function ProjectListDsNav() {
|
||||
selectedProjects,
|
||||
filter,
|
||||
projectsOwnerId,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
} = useProjectListContext()
|
||||
const { getUserNameById } = useUserIdentityContext()
|
||||
|
||||
@@ -100,7 +105,10 @@ export function ProjectListDsNav() {
|
||||
</TableContainer>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<LoadMore />
|
||||
<ProjectListSummary />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLFormSelect from '@/shared/components/ol/ol-form-select'
|
||||
import { useProjectListContext } from '../context/project-list-context'
|
||||
|
||||
export default function ProjectListSummary() {
|
||||
const {
|
||||
visibleProjects,
|
||||
hiddenProjectsCount,
|
||||
projectsPerPage,
|
||||
setProjectsPerPage,
|
||||
} = useProjectListContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<p>
|
||||
<span aria-live="polite">
|
||||
{t('showing_x_out_of_n_projects', {
|
||||
x: visibleProjects.length,
|
||||
n: visibleProjects.length + hiddenProjectsCount,
|
||||
})}
|
||||
</span>
|
||||
<span className="mx-2">·</span>
|
||||
<span className="d-inline-flex gap-1">
|
||||
<OLFormSelect
|
||||
name="projects_per_page"
|
||||
value={projectsPerPage}
|
||||
onChange={(e) => setProjectsPerPage(Number(e.target.value))}
|
||||
style={{
|
||||
width: 'auto',
|
||||
border: '1px solid #ccc',
|
||||
background: 'var(--green-10)',
|
||||
padding: '0 0.2rem',
|
||||
boxShadow: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value={20}>20</option>
|
||||
<option value={40}>40</option>
|
||||
<option value={80}>80</option>
|
||||
</OLFormSelect>
|
||||
<span>
|
||||
{t('per_page')}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import classnames from 'classnames'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import { isSmallDevice } from '@/infrastructure/event-tracking'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLForm from '@/shared/components/ol/ol-form'
|
||||
import OLFormGroup from '@/shared/components/ol/ol-form-group'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLFormControl from '@/shared/components/ol/ol-form-control'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
|
||||
type SearchFormOwnProps = {
|
||||
inputValue: string
|
||||
@@ -26,20 +27,39 @@ function SearchForm({
|
||||
...props
|
||||
}: SearchFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const placeholderMessage = t('search_projects')
|
||||
const placeholder = `${placeholderMessage}…`
|
||||
const placeholder = t('search_projects')+'…'
|
||||
|
||||
const [localValue, setLocalValue] = useState(inputValue)
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(inputValue)
|
||||
}, [inputValue])
|
||||
|
||||
const handleChange: React.ComponentProps<
|
||||
typeof OLFormControl
|
||||
>['onChange'] = e => {
|
||||
eventTracking.sendMB('admin-user-project-list-page-interaction', {
|
||||
eventTracking.sendMB('admin-project-list-page-interaction', {
|
||||
action: 'search',
|
||||
isSmallDevice,
|
||||
})
|
||||
setInputValue(e.target.value)
|
||||
|
||||
const value = e.target.value
|
||||
setLocalValue(value)
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current)
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setInputValue(value)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const handleClear = () => setInputValue('')
|
||||
const handleClear = () => {
|
||||
setLocalValue('')
|
||||
setInputValue('')
|
||||
}
|
||||
|
||||
return (
|
||||
<OLForm
|
||||
@@ -51,14 +71,15 @@ function SearchForm({
|
||||
<OLFormGroup>
|
||||
<OLCol>
|
||||
<OLFormControl
|
||||
name="search"
|
||||
type="text"
|
||||
value={inputValue}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
prepend={<MaterialIcon type="search" />}
|
||||
append={
|
||||
inputValue.length > 0 && (
|
||||
localValue.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="form-control-search-clear-btn"
|
||||
|
||||
@@ -47,12 +47,14 @@ function DeleteProjectButton({ project, children }: DeleteProjectButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<DeleteProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleDeleteProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<DeleteProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleDeleteProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,12 +42,14 @@ function PurgeProjectButton({ project, children }: PurgeProjectButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<PurgeProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handlePurgeProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<PurgeProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handlePurgeProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,12 +51,14 @@ function RestoreProjectButton({
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<RestoreProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleRestoreProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<RestoreProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleRestoreProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,12 +54,14 @@ function TransferProjectButton({ project, children }: TransferProjectButtonProps
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<TransferProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleTransferProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<TransferProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleTransferProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,12 +47,14 @@ function TrashProjectButton({ project, children }: TrashProjectButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<TrashProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleTrashProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<TrashProjectModal
|
||||
projects={[project]}
|
||||
actionHandler={handleTrashProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const ProjectCheckbox = memo<{ projectId: string; projectName: string }>(
|
||||
|
||||
return (
|
||||
<OLFormCheckbox
|
||||
id={`select_project_${projectId}`}
|
||||
autoComplete="off"
|
||||
onChange={handleCheckboxChange}
|
||||
checked={selectedProjectIds.has(projectId)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useEffect } from 'react'
|
||||
import { useCallback, useRef, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ProjectListTableRow from './project-list-table-row'
|
||||
import { useProjectListContext } from '../../context/project-list-context'
|
||||
@@ -28,9 +28,12 @@ function ProjectListTable() {
|
||||
const {
|
||||
visibleProjects,
|
||||
sort,
|
||||
searchText,
|
||||
selectedProjects,
|
||||
selectOrUnselectAllProjects,
|
||||
filter,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
} = useProjectListContext()
|
||||
const { handleSort } = useSort()
|
||||
const checkAllRef = useRef<HTMLInputElement>(null)
|
||||
@@ -49,6 +52,10 @@ function ProjectListTable() {
|
||||
selectedProjects.length !== visibleProjects.length
|
||||
}
|
||||
}, [selectedProjects, visibleProjects])
|
||||
|
||||
const [lastNonSearchPage, setLastNonSearchPage] = useState(1)
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
|
||||
return (
|
||||
<OLTable className="project-dash-table" container={false} hover>
|
||||
<caption className="visually-hidden">{t('projects_list')}</caption>
|
||||
@@ -59,6 +66,7 @@ function ProjectListTable() {
|
||||
aria-label={t('select_projects')}
|
||||
>
|
||||
<OLFormCheckbox
|
||||
name="select_all_projects"
|
||||
autoComplete="off"
|
||||
onChange={handleAllProjectsCheckboxChange}
|
||||
checked={
|
||||
|
||||
@@ -43,12 +43,14 @@ function DeleteProjectsButton() {
|
||||
<OLButton variant="danger" onClick={handleOpenModal}>
|
||||
{t('delete')}
|
||||
</OLButton>
|
||||
<DeleteProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleDeleteProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<DeleteProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleDeleteProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,12 +36,14 @@ function PurgeProjectsButton() {
|
||||
<OLButton variant="danger" onClick={handleOpenModal}>
|
||||
{t('purge')}
|
||||
</OLButton>
|
||||
<PurgeProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handlePurgeProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<PurgeProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handlePurgeProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,12 +47,14 @@ function RestoreProjectsButton() {
|
||||
<OLButton variant="primary" onClick={handleOpenModal}>
|
||||
{t('restore')}
|
||||
</OLButton>
|
||||
<RestoreProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleRestoreProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<RestoreProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleRestoreProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,12 +60,14 @@ function TransferProjectsButton() {
|
||||
icon="swap_horiz"
|
||||
/>
|
||||
</OLTooltip>
|
||||
<TransferProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleTransferProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<TransferProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleTransferProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,12 +52,14 @@ function TrashProjectsButton() {
|
||||
icon="delete"
|
||||
/>
|
||||
</OLTooltip>
|
||||
<TrashProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleTrashProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<TrashProjectModal
|
||||
projects={selectedProjects}
|
||||
actionHandler={handleTrashProject}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import { getProjects } from '../util/api'
|
||||
import { useUserIdentityContext } from '../../user-list/context/user-identity-context'
|
||||
import sortProjects from '../util/sort-projects'
|
||||
|
||||
const MAX_PROJECT_PER_PAGE = 20
|
||||
|
||||
export type Filter = 'owned' | 'trashed' | 'deleted' | 'inactive'
|
||||
|
||||
type FilterMap = {
|
||||
@@ -54,8 +52,6 @@ export type ProjectListContextValue = {
|
||||
filter: Filter
|
||||
hiddenProjectsCount: number
|
||||
isLoading: ReturnType<typeof useAsync>['isLoading']
|
||||
loadMoreCount: number
|
||||
loadMoreProjects: () => void
|
||||
loadProgress: number
|
||||
removeProjectFromView: (project: Project) => void
|
||||
selectFilter: (filter: Filter) => void
|
||||
@@ -66,13 +62,17 @@ export type ProjectListContextValue = {
|
||||
setSearchText: React.Dispatch<React.SetStateAction<string>>
|
||||
setSelectedProjectIds: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||
setSort: React.Dispatch<React.SetStateAction<Sort>>
|
||||
showAllProjects: () => void
|
||||
sort: Sort
|
||||
toggleSelectedProject: (projectId: string, selected?: boolean) => void
|
||||
totalProjectsCount: number
|
||||
projectsOwnerId: string | null
|
||||
updateProjectViewData: (newProjectData: Project) => void
|
||||
visibleProjects: Project[]
|
||||
currentPage: number
|
||||
setCurrentPage: React.Dispatch<React.SetStateAction<number>>
|
||||
totalPages: number
|
||||
projectsPerPage: number,
|
||||
setProjectsPerPage: React.Dispatch<React.SetStateAction<number>>,
|
||||
}
|
||||
|
||||
export const ProjectListContext = createContext<
|
||||
@@ -92,9 +92,6 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
prefetchedProjectsBlob?.projects ?? []
|
||||
)
|
||||
|
||||
const [maxVisibleProjects, setMaxVisibleProjects] =
|
||||
useState(MAX_PROJECT_PER_PAGE)
|
||||
|
||||
const [loadProgress, setLoadProgress] = useState(
|
||||
prefetchedProjectsBlob ? 100 : 20
|
||||
)
|
||||
@@ -113,7 +110,32 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
})
|
||||
const prevSortRef = useRef<Sort>(sort)
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [searchText, setSearchTextState] = useState('')
|
||||
const lastNonSearchPageRef = useRef(1)
|
||||
const isSearchingRef = useRef(false)
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
||||
const setSearchText: React.Dispatch<React.SetStateAction<string>> = value => {
|
||||
setSearchTextState(prev => {
|
||||
const nextValue =
|
||||
typeof value === 'function' ? value(prev) : value
|
||||
|
||||
const wasSearching = isSearchingRef.current
|
||||
const willSearch = nextValue.length > 0
|
||||
|
||||
if (!wasSearching && willSearch) {
|
||||
lastNonSearchPageRef.current = currentPage
|
||||
isSearchingRef.current = true
|
||||
setCurrentPage(1)
|
||||
} else if (wasSearching && !willSearch) {
|
||||
isSearchingRef.current = false
|
||||
setCurrentPage(lastNonSearchPageRef.current)
|
||||
}
|
||||
|
||||
return nextValue
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
isLoading: loading,
|
||||
@@ -131,7 +153,6 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
if (prefetchedProjectsBlob) return
|
||||
|
||||
setLoadProgress(40)
|
||||
|
||||
runAsync(getProjects({ userId: projectsOwnerId, by: 'lastUpdated', order: 'desc' }))
|
||||
.then(data => {
|
||||
setLoadedProjects(data.projects)
|
||||
@@ -144,7 +165,12 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
}, [projectsOwnerId, runAsync, prefetchedProjectsBlob])
|
||||
|
||||
const sortedProjects = useMemo(() => {
|
||||
if (prevSortRef.current === sort) return loadedProjects
|
||||
if (
|
||||
prevSortRef.current.by === sort.by &&
|
||||
prevSortRef.current.order === sort.order
|
||||
) {
|
||||
return loadedProjects
|
||||
}
|
||||
|
||||
const sorted = sortProjects(loadedProjects, sort, getUserById)
|
||||
prevSortRef.current = sort
|
||||
@@ -152,40 +178,49 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
}, [loadedProjects, sort, getUserById])
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
let result = sortedProjects
|
||||
const predicate = filters[filter]
|
||||
const hasSearch = searchText.length > 0
|
||||
const lower = hasSearch ? searchText.toLowerCase() : null
|
||||
|
||||
if (searchText.length) {
|
||||
const lower = searchText.toLowerCase()
|
||||
result = result.filter(project =>
|
||||
project.name.toLowerCase().includes(lower)
|
||||
)
|
||||
}
|
||||
return sortedProjects.filter(project => {
|
||||
if (hasSearch) {
|
||||
if (!project.name.toLowerCase().includes(lower!)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return result.filter(filters[filter])
|
||||
return typeof predicate === 'function'
|
||||
? predicate(project)
|
||||
: true
|
||||
})
|
||||
}, [sortedProjects, searchText, filter])
|
||||
|
||||
const [projectsPerPage, setProjectsPerPage] = useState(20)
|
||||
const previousProjectsPerPageRef = useRef(projectsPerPage)
|
||||
|
||||
useEffect(() => {
|
||||
const previousProjectsPerPage = previousProjectsPerPageRef.current
|
||||
|
||||
if (previousProjectsPerPage !== projectsPerPage) {
|
||||
const oldStartIndex = (currentPage - 1) * previousProjectsPerPage
|
||||
const newPage = Math.floor(oldStartIndex / projectsPerPage) + 1
|
||||
setCurrentPage(newPage)
|
||||
previousProjectsPerPageRef.current = projectsPerPage
|
||||
}
|
||||
}, [projectsPerPage])
|
||||
|
||||
const totalPages = Math.ceil(filteredProjects.length / projectsPerPage)
|
||||
const startIndex = (currentPage - 1) * projectsPerPage
|
||||
|
||||
const visibleProjects = useMemo(() => {
|
||||
return filteredProjects.slice(0, maxVisibleProjects)
|
||||
}, [filteredProjects, maxVisibleProjects])
|
||||
return filteredProjects.slice(startIndex, startIndex + projectsPerPage)
|
||||
}, [filteredProjects, startIndex, projectsPerPage])
|
||||
|
||||
const hiddenProjectsCount = Math.max(
|
||||
filteredProjects.length - visibleProjects.length,
|
||||
0
|
||||
)
|
||||
|
||||
const loadMoreCount = Math.min(
|
||||
hiddenProjectsCount,
|
||||
MAX_PROJECT_PER_PAGE
|
||||
)
|
||||
|
||||
const showAllProjects = useCallback(() => {
|
||||
setMaxVisibleProjects(v => v + hiddenProjectsCount)
|
||||
}, [hiddenProjectsCount])
|
||||
|
||||
const loadMoreProjects = useCallback(() => {
|
||||
setMaxVisibleProjects(v => v + loadMoreCount)
|
||||
}, [loadMoreCount])
|
||||
|
||||
const [selectedProjectIds, setSelectedProjectIds] = useState(
|
||||
() => new Set<string>()
|
||||
)
|
||||
@@ -233,6 +268,8 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
const selectFilter = useCallback(
|
||||
(filter: Filter) => {
|
||||
setFilter(filter)
|
||||
selectOrUnselectAllProjects(false)
|
||||
setCurrentPage(1)
|
||||
|
||||
setSort(prev => {
|
||||
if (filter === 'deleted' && prev.by === 'lastUpdated') {
|
||||
@@ -243,8 +280,6 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
}
|
||||
return prev
|
||||
})
|
||||
|
||||
selectOrUnselectAllProjects(false)
|
||||
},
|
||||
[selectOrUnselectAllProjects]
|
||||
)
|
||||
@@ -269,8 +304,6 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
filter,
|
||||
hiddenProjectsCount,
|
||||
isLoading,
|
||||
loadMoreCount,
|
||||
loadMoreProjects,
|
||||
loadProgress,
|
||||
removeProjectFromView,
|
||||
selectFilter,
|
||||
@@ -281,21 +314,23 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
setSearchText,
|
||||
setSelectedProjectIds,
|
||||
setSort,
|
||||
showAllProjects,
|
||||
sort,
|
||||
toggleSelectedProject,
|
||||
totalProjectsCount,
|
||||
updateProjectViewData,
|
||||
projectsOwnerId,
|
||||
visibleProjects,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
projectsPerPage,
|
||||
setProjectsPerPage
|
||||
}),
|
||||
[
|
||||
error,
|
||||
filter,
|
||||
hiddenProjectsCount,
|
||||
isLoading,
|
||||
loadMoreCount,
|
||||
loadMoreProjects,
|
||||
loadProgress,
|
||||
removeProjectFromView,
|
||||
selectFilter,
|
||||
@@ -306,13 +341,17 @@ export function ProjectListProvider({ projectsOwnerId, children }: ProjectListPr
|
||||
setSearchText,
|
||||
setSelectedProjectIds,
|
||||
setSort,
|
||||
showAllProjects,
|
||||
sort,
|
||||
toggleSelectedProject,
|
||||
totalProjectsCount,
|
||||
projectsOwnerId,
|
||||
updateProjectViewData,
|
||||
visibleProjects,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
projectsPerPage,
|
||||
setProjectsPerPage
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ const toggleSort = (order: SortingOrder): SortingOrder =>
|
||||
order === 'asc' ? 'desc' : 'asc'
|
||||
|
||||
function useSort() {
|
||||
const { filter, sort, setSort } = useProjectListContext()
|
||||
const { filter, sort, setSort, setCurrentPage } = useProjectListContext()
|
||||
|
||||
const handleSort = (by: Sort['by']) => {
|
||||
setCurrentPage(1)
|
||||
setSort(prev => ({
|
||||
by,
|
||||
order: prev.by === by ? toggleSort(prev.order) : prev.order,
|
||||
@@ -18,13 +19,15 @@ function useSort() {
|
||||
|
||||
useEffect(() => {
|
||||
if (filter === 'deleted' && sort.by === 'lastUpdated') {
|
||||
setCurrentPage(1)
|
||||
setSort(prev => ({ ...prev, by: 'deletedAt' }))
|
||||
}
|
||||
|
||||
if (filter !== 'deleted' && sort.by === 'deletedAt') {
|
||||
setCurrentPage(1)
|
||||
setSort(prev => ({ ...prev, by: 'lastUpdated' }))
|
||||
}
|
||||
}, [filter, sort.by, setSort])
|
||||
}, [filter, sort.by, setSort, setCurrentPage])
|
||||
|
||||
return { handleSort }
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUserListContext } from '../context/user-list-context'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
|
||||
export default function LoadMore() {
|
||||
const {
|
||||
visibleUsers,
|
||||
hiddenUsersCount,
|
||||
loadMoreCount,
|
||||
showAllUsers,
|
||||
loadMoreUsers,
|
||||
} = useUserListContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
{hiddenUsersCount > 0 ? (
|
||||
<>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
className="user-list-load-more-button"
|
||||
onClick={() => loadMoreUsers()}
|
||||
>
|
||||
{t('show_x_more_users', { x: loadMoreCount })}
|
||||
</OLButton>
|
||||
</>
|
||||
) : null}
|
||||
<p>
|
||||
{hiddenUsersCount > 0 ? (
|
||||
<>
|
||||
<span aria-live="polite">
|
||||
{t('showing_x_out_of_n_users', {
|
||||
x: visibleUsers.length,
|
||||
n: visibleUsers.length + hiddenUsersCount,
|
||||
})}
|
||||
</span>{' '}
|
||||
<OLButton
|
||||
variant="link"
|
||||
onClick={() => showAllUsers()}
|
||||
className="btn-inline-link"
|
||||
>
|
||||
{t('show_all_users')}
|
||||
</OLButton>
|
||||
</>
|
||||
) : (
|
||||
<span aria-live="polite">
|
||||
{t('showing_x_out_of_n_users', {
|
||||
x: visibleUsers.length,
|
||||
n: visibleUsers.length,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import OLForm from '@/shared/components/ol/ol-form'
|
||||
import SelectOwnerForm from '../../../project-list/components/select-owner-form'
|
||||
import { useUserListContext } from '../../../user-list/context/user-list-context'
|
||||
import { UserRef } from '../../../../../types/project/api'
|
||||
import sortUsers from '../../../user-list/util/sort-users'
|
||||
|
||||
type DeleteUserModalProps = Pick<
|
||||
React.ComponentProps<typeof UsersActionModal>,
|
||||
@@ -34,12 +33,10 @@ function DeleteUserModal({
|
||||
|
||||
const potentialOwners = useMemo(() => {
|
||||
if (!loadedUsers) return []
|
||||
|
||||
const excludeIds = new Set(users.map(u => u.id))
|
||||
const possibleUsers = loadedUsers.filter(
|
||||
return loadedUsers.filter(
|
||||
user => !user.deleted && !excludeIds.has(user.id)
|
||||
)
|
||||
return sortUsers(possibleUsers, { by: 'name', order: 'asc' })
|
||||
}, [loadedUsers, users])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -95,6 +95,7 @@ function ShowUserInfoModal({
|
||||
<>
|
||||
<Card.Header>{t('Account')}</Card.Header>
|
||||
<Body>
|
||||
<InfoRow label={'ID'} value={user.id} />
|
||||
<InfoRow label={t('email_address')} value={user.email} />
|
||||
<InfoRow label={t('first_name')} value={user.firstName || '—'} />
|
||||
<InfoRow label={t('last_name')} value={user.lastName || '—'} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classnames from 'classnames'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
@@ -8,12 +9,10 @@ import OLFormGroup from '@/shared/components/ol/ol-form-group'
|
||||
import OLFormControl from '@/shared/components/ol/ol-form-control'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import { Filter } from '../context/user-list-context'
|
||||
|
||||
type SearchFormOwnProps = {
|
||||
inputValue: string
|
||||
setInputValue: (input: string) => void
|
||||
filter: Filter
|
||||
}
|
||||
|
||||
type SearchFormProps = MergeAndOverride<
|
||||
@@ -24,13 +23,19 @@ type SearchFormProps = MergeAndOverride<
|
||||
function SearchForm({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
filter,
|
||||
className,
|
||||
...props
|
||||
}: SearchFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const placeholder = t('search')+'…'
|
||||
|
||||
const [localValue, setLocalValue] = useState(inputValue)
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(inputValue)
|
||||
}, [inputValue])
|
||||
|
||||
const handleChange: React.ComponentProps<
|
||||
typeof OLFormControl
|
||||
>['onChange'] = e => {
|
||||
@@ -38,10 +43,23 @@ function SearchForm({
|
||||
action: 'search',
|
||||
isSmallDevice,
|
||||
})
|
||||
setInputValue(e.target.value)
|
||||
|
||||
const value = e.target.value
|
||||
setLocalValue(value)
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current)
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setInputValue(value)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const handleClear = () => setInputValue('')
|
||||
const handleClear = () => {
|
||||
setLocalValue('')
|
||||
setInputValue('')
|
||||
}
|
||||
|
||||
return (
|
||||
<OLForm
|
||||
@@ -55,13 +73,13 @@ function SearchForm({
|
||||
<OLFormControl
|
||||
name="search"
|
||||
type="text"
|
||||
value={inputValue}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
prepend={<MaterialIcon type="search" />}
|
||||
append={
|
||||
inputValue.length > 0 && (
|
||||
localValue.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="form-control-search-clear-btn"
|
||||
|
||||
@@ -40,12 +40,14 @@ function DeleteUserButton({ user, children }: DeleteUserButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<DeleteUserModal
|
||||
users={[user]}
|
||||
actionHandler={handleDeleteUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<DeleteUserModal
|
||||
users={[user]}
|
||||
actionHandler={handleDeleteUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,13 +43,15 @@ function FlagUserButton({ user, action, children }: FlagUserButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<FlagUserModal
|
||||
users={[user]}
|
||||
action={action}
|
||||
actionHandler={handleFlagUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<FlagUserModal
|
||||
users={[user]}
|
||||
action={action}
|
||||
actionHandler={handleFlagUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,12 +40,14 @@ function PurgeUserButton({ user, children }: PurgeUserButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<PurgeUserModal
|
||||
users={[user]}
|
||||
actionHandler={handlePurgeUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<PurgeUserModal
|
||||
users={[user]}
|
||||
actionHandler={handlePurgeUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,12 +40,14 @@ function RestoreUserButton({ user, children }: RestoreUserButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<RestoreUserModal
|
||||
users={[user]}
|
||||
actionHandler={handleRestoreUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<RestoreUserModal
|
||||
users={[user]}
|
||||
actionHandler={handleRestoreUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,12 +42,14 @@ function SendRegEmailButton({ user, children }: SendRegEmailButtonProps) {
|
||||
return (
|
||||
<span style={isHidden ? { visibility: 'hidden' } : undefined }>
|
||||
{children(text, handleOpenModal)}
|
||||
<SendRegEmailModal
|
||||
users={[user]}
|
||||
actionHandler={handleSendRegEmail}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<SendRegEmailModal
|
||||
users={[user]}
|
||||
actionHandler={handleSendRegEmail}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,11 +38,13 @@ function ShowUserInfoButton({ user, children }: ShowUserInfoButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<ShowUserInfoModal
|
||||
users={[user]}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<ShowUserInfoModal
|
||||
users={[user]}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,12 +40,14 @@ function UpdateUserButton({ user, children }: UpdateUserButtonProps) {
|
||||
return (
|
||||
<>
|
||||
{children(text, handleOpenModal)}
|
||||
<UpdateUserModal
|
||||
users={[user]}
|
||||
actionHandler={handleUpdateUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<UpdateUserModal
|
||||
users={[user]}
|
||||
actionHandler={handleUpdateUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,10 +28,13 @@ function UserListTable() {
|
||||
const {
|
||||
visibleUsers,
|
||||
sort,
|
||||
searchText,
|
||||
selectedUsers,
|
||||
selectOrUnselectAllUsers,
|
||||
selfVisibleCount,
|
||||
filter,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
} = useUserListContext()
|
||||
const { handleSort } = useSort()
|
||||
const checkAllRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -39,12 +39,14 @@ function DeleteUsersButton() {
|
||||
<OLButton variant="danger" onClick={handleOpenModal}>
|
||||
{t('delete')}
|
||||
</OLButton>
|
||||
<DeleteUserModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handleDeleteUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<DeleteUserModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handleDeleteUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,13 +70,15 @@ function FlagUsersButton({ action }: { action: string }) {
|
||||
unfilled={unfilled}
|
||||
/>
|
||||
</OLTooltip>
|
||||
<FlagUserModal
|
||||
users={selectedUsers}
|
||||
action={action}
|
||||
actionHandler={handleFlagUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<FlagUserModal
|
||||
users={selectedUsers}
|
||||
action={action}
|
||||
actionHandler={handleFlagUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,13 +33,14 @@ function PurgeUsersButton() {
|
||||
<OLButton variant="danger" onClick={handleOpenModal}>
|
||||
{t('purge')}
|
||||
</OLButton>
|
||||
|
||||
<PurgeUserModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handlePurgeUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<PurgeUserModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handlePurgeUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,13 +33,14 @@ function RestoreUsersButton() {
|
||||
<OLButton variant="primary" onClick={handleOpenModal}>
|
||||
{t('restore')}
|
||||
</OLButton>
|
||||
|
||||
<RestoreUserModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handleRestoreUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<RestoreUserModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handleRestoreUser}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,12 +47,14 @@ function SendRegEmailsButton({ action }: { action: string }) {
|
||||
unfilled={true}
|
||||
/>
|
||||
</OLTooltip>
|
||||
<SendRegEmailModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handleSendRegEmail}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
{showModal && (
|
||||
<SendRegEmailModal
|
||||
users={selectedUsers}
|
||||
actionHandler={handleSendRegEmail}
|
||||
showModal={showModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import UsersDropdown from './dropdown/users-dropdown'
|
||||
import SortByDropdown from './dropdown/sort-by-dropdown'
|
||||
import UserTools from './table/user-tools/user-tools'
|
||||
import UserListTitle from './title/user-list-title'
|
||||
import LoadMore from './load-more'
|
||||
import CountUsers from './count-users'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import { TableContainer } from '@/shared/components/table'
|
||||
@@ -18,6 +18,8 @@ import Footer from '@/shared/components/footer/footer'
|
||||
import SidebarDsNav from './sidebar/sidebar-ds-nav'
|
||||
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||
import CookieBanner from '@/shared/components/cookie-banner'
|
||||
import Pagination from '@/shared/components/pagination-cep'
|
||||
import UserListSummary from './user-list-summary'
|
||||
|
||||
export function UserListDsNav() {
|
||||
const navbarProps = getMeta('ol-navbar')
|
||||
@@ -30,6 +32,9 @@ export function UserListDsNav() {
|
||||
setSearchText,
|
||||
selectedUsers,
|
||||
filter,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
} = useUserListContext()
|
||||
|
||||
const tableTopArea = (
|
||||
@@ -40,7 +45,6 @@ export function UserListDsNav() {
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
filter={filter}
|
||||
className="overflow-hidden flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
@@ -77,7 +81,6 @@ export function UserListDsNav() {
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
filter={filter}
|
||||
/>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
@@ -98,7 +101,10 @@ export function UserListDsNav() {
|
||||
</TableContainer>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<LoadMore />
|
||||
<UserListSummary />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLFormSelect from '@/shared/components/ol/ol-form-select'
|
||||
import { useUserListContext } from '../context/user-list-context'
|
||||
|
||||
export default function UserListSummary() {
|
||||
const {
|
||||
visibleUsers,
|
||||
hiddenUsersCount,
|
||||
usersPerPage,
|
||||
setUsersPerPage,
|
||||
} = useUserListContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<p>
|
||||
<span aria-live="polite">
|
||||
{t('showing_x_out_of_n_users', {
|
||||
x: visibleUsers.length,
|
||||
n: visibleUsers.length + hiddenUsersCount,
|
||||
})}
|
||||
</span>
|
||||
<span className="mx-2">·</span>
|
||||
<span className="d-inline-flex gap-1">
|
||||
<OLFormSelect
|
||||
name="users_per_page"
|
||||
value={usersPerPage}
|
||||
onChange={(e) => setUsersPerPage(Number(e.target.value))}
|
||||
style={{
|
||||
width: 'auto',
|
||||
border: '1px solid #ccc',
|
||||
background: 'var(--green-10)',
|
||||
padding: '0 0.2rem',
|
||||
boxShadow: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value={20}>20</option>
|
||||
<option value={40}>40</option>
|
||||
<option value={80}>80</option>
|
||||
</OLFormSelect>
|
||||
<span>
|
||||
{t('per_page')}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
filter as arrayFilter,
|
||||
@@ -25,8 +26,6 @@ import sortUsers from '../util/sort-users'
|
||||
|
||||
import { UserIdentityProvider } from './user-identity-context'
|
||||
|
||||
const MAX_USER_PER_PAGE = 10
|
||||
|
||||
type AuthMethods = 'local' | 'ldap' | 'saml' | 'oidc'
|
||||
export type Filter = 'all' | 'admin' | 'suspended' | 'inactive' | AuthMethods | 'deleted'
|
||||
|
||||
@@ -68,8 +67,6 @@ export type UserListContextValue = {
|
||||
filterTranslations: Map<Filter, string>
|
||||
hiddenUsersCount: number
|
||||
isLoading: ReturnType<typeof useAsync>['isLoading']
|
||||
loadMoreCount: number
|
||||
loadMoreUsers: () => void
|
||||
loadProgress: number
|
||||
removeUserFromView: (user: User) => void
|
||||
searchText: string
|
||||
@@ -81,13 +78,17 @@ export type UserListContextValue = {
|
||||
setSearchText: React.Dispatch<React.SetStateAction<string>>
|
||||
setSelectedUserIds: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||
setSort: React.Dispatch<React.SetStateAction<Sort>>
|
||||
showAllUsers: () => void
|
||||
sort: Sort
|
||||
toggleSelectedUser: (userId: string, selected?: boolean) => void
|
||||
totalUsersCount: number
|
||||
updateUserViewData: (newUserData: User) => void
|
||||
loadedUsers: User[]
|
||||
visibleUsers: User[]
|
||||
currentPage: number
|
||||
setCurrentPage: React.Dispatch<React.SetStateAction<number>>
|
||||
totalPages: number
|
||||
usersPerPage: number,
|
||||
setUsersPerPage: React.Dispatch<React.SetStateAction<number>>,
|
||||
}
|
||||
|
||||
export const UserListContext = createContext<
|
||||
@@ -104,9 +105,6 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
prefetchedUsersBlob?.users ?? []
|
||||
)
|
||||
|
||||
const [maxVisibleUsers, setMaxVisibleUsers] =
|
||||
useState(MAX_USER_PER_PAGE)
|
||||
|
||||
const [loadProgress, setLoadProgress] = useState(
|
||||
prefetchedUsersBlob ? 100 : 20
|
||||
)
|
||||
@@ -130,7 +128,31 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
'all'
|
||||
)
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [searchText, setSearchTextState] = useState('')
|
||||
const lastNonSearchPageRef = useRef(1)
|
||||
const isSearchingRef = useRef(false)
|
||||
|
||||
const setSearchText: React.Dispatch<React.SetStateAction<string>> = value => {
|
||||
setSearchTextState(prev => {
|
||||
const nextValue =
|
||||
typeof value === 'function' ? value(prev) : value
|
||||
|
||||
const wasSearching = isSearchingRef.current
|
||||
const willSearch = nextValue.length > 0
|
||||
|
||||
if (!wasSearching && willSearch) {
|
||||
lastNonSearchPageRef.current = currentPage
|
||||
isSearchingRef.current = true
|
||||
setCurrentPage(1)
|
||||
} else if (wasSearching && !willSearch) {
|
||||
isSearchingRef.current = false
|
||||
setCurrentPage(lastNonSearchPageRef.current)
|
||||
}
|
||||
|
||||
return nextValue
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
isLoading: loading,
|
||||
@@ -143,10 +165,11 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
})
|
||||
const isLoading = isIdle ? true : loading
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (prefetchedUsersBlob) return
|
||||
setLoadProgress(40)
|
||||
runAsync(getUsers({ by: 'signUpDate', order: 'desc' }))
|
||||
runAsync(getUsers({ by: 'name', order: 'asc' }))
|
||||
.then(data => {
|
||||
setLoadedUsers(data.users)
|
||||
setTotalUsersCount(data.totalSize)
|
||||
@@ -161,7 +184,10 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
setLoadedUsers(prev => [newUser as User, ...prev])
|
||||
}, [])
|
||||
|
||||
const processedUsers = useMemo(() => {
|
||||
const isDefaultSort =
|
||||
sort.by === 'name' && sort.order === 'asc'
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
let users = loadedUsers
|
||||
|
||||
if (searchText.length) {
|
||||
@@ -175,35 +201,50 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
|
||||
users = arrayFilter(users, filters[filter])
|
||||
|
||||
return sortUsers(users, sort)
|
||||
}, [loadedUsers, searchText, filter, sort])
|
||||
return users
|
||||
}, [loadedUsers, searchText, filter])
|
||||
|
||||
const processedUsers = useMemo(() => {
|
||||
if (isDefaultSort) {
|
||||
return filteredUsers
|
||||
}
|
||||
|
||||
return sortUsers(filteredUsers, sort)
|
||||
}, [filteredUsers, sort, isDefaultSort])
|
||||
|
||||
const [usersPerPage, setUsersPerPage] = useState(20)
|
||||
const previousUsersPerPageRef = useRef(usersPerPage)
|
||||
|
||||
useEffect(() => {
|
||||
const previousUsersPerPage = previousUsersPerPageRef.current
|
||||
|
||||
if (previousUsersPerPage !== usersPerPage) {
|
||||
const oldStartIndex = (currentPage - 1) * previousUsersPerPage
|
||||
const newPage = Math.floor(oldStartIndex / usersPerPage) + 1
|
||||
setCurrentPage(newPage)
|
||||
previousUsersPerPageRef.current = usersPerPage
|
||||
}
|
||||
}, [usersPerPage])
|
||||
|
||||
const totalPages = useMemo(
|
||||
() => Math.ceil(processedUsers.length / usersPerPage),
|
||||
[processedUsers.length, usersPerPage]
|
||||
)
|
||||
const startIndex = (currentPage - 1) * usersPerPage
|
||||
|
||||
const visibleUsers = useMemo(() => {
|
||||
return processedUsers.slice(0, maxVisibleUsers)
|
||||
}, [processedUsers, maxVisibleUsers])
|
||||
return processedUsers.slice(startIndex, startIndex + usersPerPage)
|
||||
}, [processedUsers, startIndex, usersPerPage])
|
||||
|
||||
const hiddenUsersCount = Math.max(
|
||||
processedUsers.length - visibleUsers.length,
|
||||
0
|
||||
)
|
||||
|
||||
const loadMoreCount = Math.min(
|
||||
hiddenUsersCount,
|
||||
MAX_USER_PER_PAGE
|
||||
)
|
||||
|
||||
const selfVisibleCount = useMemo(() => {
|
||||
return visibleUsers.some(u => u.id === selfId) ? 1 : 0
|
||||
}, [visibleUsers])
|
||||
|
||||
const showAllUsers = useCallback(() => {
|
||||
setMaxVisibleUsers(maxVisibleUsers + hiddenUsersCount)
|
||||
}, [hiddenUsersCount, maxVisibleUsers])
|
||||
|
||||
const loadMoreUsers = useCallback(() => {
|
||||
setMaxVisibleUsers(maxVisibleUsers + loadMoreCount)
|
||||
}, [maxVisibleUsers, loadMoreCount])
|
||||
|
||||
const [selectedUserIds, setSelectedUserIds] = useState(
|
||||
() => new Set<string>()
|
||||
)
|
||||
@@ -266,10 +307,10 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
return prev
|
||||
})
|
||||
|
||||
const selected = false
|
||||
selectOrUnselectAllUsers(selected)
|
||||
selectOrUnselectAllUsers(false)
|
||||
setCurrentPage(1)
|
||||
},
|
||||
[selectOrUnselectAllUsers, setFilter]
|
||||
[selectOrUnselectAllUsers]
|
||||
)
|
||||
|
||||
const updateUserViewData = useCallback((newUserData: User) => {
|
||||
@@ -294,8 +335,6 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
filterTranslations,
|
||||
hiddenUsersCount,
|
||||
isLoading,
|
||||
loadMoreCount,
|
||||
loadMoreUsers,
|
||||
loadProgress,
|
||||
removeUserFromView,
|
||||
searchText,
|
||||
@@ -307,13 +346,17 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
setSearchText,
|
||||
setSelectedUserIds,
|
||||
setSort,
|
||||
showAllUsers,
|
||||
sort,
|
||||
toggleSelectedUser,
|
||||
totalUsersCount,
|
||||
updateUserViewData,
|
||||
loadedUsers,
|
||||
visibleUsers,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
usersPerPage,
|
||||
setUsersPerPage,
|
||||
}),
|
||||
[
|
||||
addUserToView,
|
||||
@@ -322,8 +365,6 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
filterTranslations,
|
||||
hiddenUsersCount,
|
||||
isLoading,
|
||||
loadMoreCount,
|
||||
loadMoreUsers,
|
||||
loadProgress,
|
||||
removeUserFromView,
|
||||
searchText,
|
||||
@@ -335,19 +376,21 @@ export function UserListProvider({ children }: UserListProviderProps) {
|
||||
setSearchText,
|
||||
setSelectedUserIds,
|
||||
setSort,
|
||||
showAllUsers,
|
||||
sort,
|
||||
toggleSelectedUser,
|
||||
totalUsersCount,
|
||||
updateUserViewData,
|
||||
loadedUsers,
|
||||
visibleUsers,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
usersPerPage,
|
||||
setUsersPerPage,
|
||||
]
|
||||
)
|
||||
|
||||
if (!loadedUsers || loadedUsers.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (isLoading) return null
|
||||
|
||||
return (
|
||||
<UserIdentityProvider users={loadedUsers}>
|
||||
|
||||
@@ -8,9 +8,10 @@ const toggleSort = (order: SortingOrder): SortingOrder => {
|
||||
}
|
||||
|
||||
function useSort() {
|
||||
const { filter, sort, setSort } = useUserListContext()
|
||||
const { filter, sort, setSort, setCurrentPage } = useUserListContext()
|
||||
|
||||
const handleSort = (by: Sort['by']) => {
|
||||
setCurrentPage(1)
|
||||
setSort(prev => ({
|
||||
by,
|
||||
order: prev.by === by ? toggleSort(sort.order) : sort.order,
|
||||
@@ -19,13 +20,15 @@ function useSort() {
|
||||
|
||||
useEffect(() => {
|
||||
if (filter === 'deleted' && sort.by === 'signUpDate') {
|
||||
setCurrentPage(1)
|
||||
setSort(prev => ({ ...prev, by: 'deletedAt' }))
|
||||
}
|
||||
|
||||
if (filter !== 'deleted' && sort.by === 'deletedAt') {
|
||||
setCurrentPage(1)
|
||||
setSort(prev => ({ ...prev, by: 'signUpDate' }))
|
||||
}
|
||||
}, [filter, sort.by, setSort])
|
||||
}, [filter, sort.by, setSort, setCurrentPage])
|
||||
|
||||
return { handleSort }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user