mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Store selected project ids separately (#15598)
GitOrigin-RevId: 56a833fb1524362ff6617afa9be4cb7b6c1984c7
This commit is contained in:
parent
0cde5be165
commit
49d2ca781e
14 changed files with 114 additions and 57 deletions
|
@ -34,7 +34,8 @@ function RenameProjectModal({
|
|||
const { t } = useTranslation()
|
||||
const [newProjectName, setNewProjectName] = useState(project.name)
|
||||
const { error, isError, isLoading, runAsync } = useAsync()
|
||||
const { updateProjectViewData } = useProjectListContext()
|
||||
const { toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
const newNotificationStyle = getMeta(
|
||||
'ol-newNotificationStyle',
|
||||
false
|
||||
|
@ -63,10 +64,10 @@ function RenameProjectModal({
|
|||
|
||||
runAsync(renameProject(project.id, newProjectName))
|
||||
.then(() => {
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
name: newProjectName,
|
||||
selected: false,
|
||||
})
|
||||
handleCloseModal()
|
||||
})
|
||||
|
@ -78,6 +79,7 @@ function RenameProjectModal({
|
|||
newProjectName,
|
||||
project,
|
||||
runAsync,
|
||||
toggleSelectedProject,
|
||||
updateProjectViewData,
|
||||
]
|
||||
)
|
||||
|
|
|
@ -17,7 +17,8 @@ function ArchiveProjectButton({
|
|||
project,
|
||||
children,
|
||||
}: ArchiveProjectButtonProps) {
|
||||
const { updateProjectViewData } = useProjectListContext()
|
||||
const { toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
const text = t('archive')
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
@ -35,13 +36,13 @@ function ArchiveProjectButton({
|
|||
|
||||
const handleArchiveProject = useCallback(async () => {
|
||||
await archiveProject(project.id)
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
archived: true,
|
||||
selected: false,
|
||||
trashed: false,
|
||||
})
|
||||
}, [project, updateProjectViewData])
|
||||
}, [project, toggleSelectedProject, updateProjectViewData])
|
||||
|
||||
if (project.archived) return null
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ function CopyProjectButton({ project, children }: CopyButtonProps) {
|
|||
const {
|
||||
addClonedProjectToViewData,
|
||||
addProjectToTagInView,
|
||||
toggleSelectedProject,
|
||||
updateProjectViewData,
|
||||
} = useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
|
@ -51,13 +52,15 @@ function CopyProjectButton({ project, children }: CopyButtonProps) {
|
|||
for (const tag of tags) {
|
||||
addProjectToTagInView(tag._id, clonedProject.project_id)
|
||||
}
|
||||
updateProjectViewData({ ...project, selected: false })
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({ ...project })
|
||||
setShowModal(false)
|
||||
},
|
||||
[
|
||||
addClonedProjectToViewData,
|
||||
addProjectToTagInView,
|
||||
project,
|
||||
toggleSelectedProject,
|
||||
updateProjectViewData,
|
||||
]
|
||||
)
|
||||
|
|
|
@ -14,7 +14,8 @@ type TrashProjectButtonProps = {
|
|||
}
|
||||
|
||||
function TrashProjectButton({ project, children }: TrashProjectButtonProps) {
|
||||
const { updateProjectViewData } = useProjectListContext()
|
||||
const { toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
const text = t('trash')
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
@ -32,13 +33,13 @@ function TrashProjectButton({ project, children }: TrashProjectButtonProps) {
|
|||
|
||||
const handleTrashProject = useCallback(async () => {
|
||||
await trashProject(project.id)
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
trashed: true,
|
||||
archived: false,
|
||||
selected: false,
|
||||
})
|
||||
}, [project, updateProjectViewData])
|
||||
}, [project, toggleSelectedProject, updateProjectViewData])
|
||||
|
||||
if (project.trashed) return null
|
||||
|
||||
|
|
|
@ -20,13 +20,14 @@ function UnarchiveProjectButton({
|
|||
}: UnarchiveProjectButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const text = t('unarchive')
|
||||
const { updateProjectViewData } = useProjectListContext()
|
||||
const { toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
|
||||
const handleUnarchiveProject = useCallback(async () => {
|
||||
await unarchiveProject(project.id)
|
||||
|
||||
updateProjectViewData({ ...project, archived: false, selected: false })
|
||||
}, [project, updateProjectViewData])
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({ ...project, archived: false })
|
||||
}, [project, toggleSelectedProject, updateProjectViewData])
|
||||
|
||||
if (!project.archived) return null
|
||||
|
||||
|
|
|
@ -20,12 +20,14 @@ function UntrashProjectButton({
|
|||
}: UntrashProjectButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const text = t('untrash')
|
||||
const { updateProjectViewData } = useProjectListContext()
|
||||
const { toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
|
||||
const handleUntrashProject = useCallback(async () => {
|
||||
await untrashProject(project.id)
|
||||
updateProjectViewData({ ...project, trashed: false, selected: false })
|
||||
}, [project, updateProjectViewData])
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({ ...project, trashed: false })
|
||||
}, [project, toggleSelectedProject, updateProjectViewData])
|
||||
|
||||
if (!project.trashed) return null
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { memo, useCallback } from 'react'
|
||||
import { useProjectListContext } from '@/features/project-list/context/project-list-context'
|
||||
|
||||
export const ProjectCheckbox = memo<{ projectId: string }>(({ projectId }) => {
|
||||
const { selectedProjectIds, toggleSelectedProject } = useProjectListContext()
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
event => {
|
||||
toggleSelectedProject(projectId, event.target.checked)
|
||||
},
|
||||
[projectId, toggleSelectedProject]
|
||||
)
|
||||
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`select-project-${projectId}`}
|
||||
checked={selectedProjectIds.has(projectId)}
|
||||
onChange={handleCheckboxChange}
|
||||
data-project-id={projectId}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
ProjectCheckbox.displayName = 'ProjectCheckbox'
|
|
@ -1,41 +1,25 @@
|
|||
import { useCallback } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InlineTags from './cells/inline-tags'
|
||||
import OwnerCell from './cells/owner-cell'
|
||||
import LastUpdatedCell from './cells/last-updated-cell'
|
||||
import ActionsCell from './cells/actions-cell'
|
||||
import ActionsDropdown from '../dropdown/actions-dropdown'
|
||||
import { useProjectListContext } from '../../context/project-list-context'
|
||||
import { getOwnerName } from '../../util/project'
|
||||
import { Project } from '../../../../../../types/project/dashboard/api'
|
||||
import { ProjectCheckbox } from './project-checkbox'
|
||||
|
||||
type ProjectListTableRowProps = {
|
||||
project: Project
|
||||
}
|
||||
export default function ProjectListTableRow({
|
||||
project,
|
||||
}: ProjectListTableRowProps) {
|
||||
function ProjectListTableRow({ project }: ProjectListTableRowProps) {
|
||||
const { t } = useTranslation()
|
||||
const ownerName = getOwnerName(project)
|
||||
const { updateProjectViewData } = useProjectListContext()
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateProjectViewData({ ...project, selected: event.target.checked })
|
||||
},
|
||||
[project, updateProjectViewData]
|
||||
)
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="dash-cell-checkbox hidden-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`select-project-${project.id}`}
|
||||
checked={project.selected === true}
|
||||
onChange={handleCheckboxChange}
|
||||
data-project-id={project.id}
|
||||
/>
|
||||
<ProjectCheckbox projectId={project.id} />
|
||||
<label htmlFor={`select-project-${project.id}`} className="sr-only">
|
||||
{t('select_project', { project: project.name })}
|
||||
</label>
|
||||
|
@ -68,3 +52,4 @@ export default function ProjectListTableRow({
|
|||
</tr>
|
||||
)
|
||||
}
|
||||
export default memo(ProjectListTableRow)
|
||||
|
|
|
@ -9,7 +9,8 @@ import { archiveProject } from '../../../../util/api'
|
|||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
|
||||
function ArchiveProjectsButton() {
|
||||
const { selectedProjects, updateProjectViewData } = useProjectListContext()
|
||||
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
const text = t('archive')
|
||||
|
||||
|
@ -29,10 +30,10 @@ function ArchiveProjectsButton() {
|
|||
const handleArchiveProject = async (project: Project) => {
|
||||
await archiveProject(project.id)
|
||||
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
archived: true,
|
||||
selected: false,
|
||||
trashed: false,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ import { trashProject } from '../../../../util/api'
|
|||
import { Project } from '../../../../../../../../types/project/dashboard/api'
|
||||
|
||||
function TrashProjectsButton() {
|
||||
const { selectedProjects, updateProjectViewData } = useProjectListContext()
|
||||
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
const text = t('trash')
|
||||
|
||||
|
@ -29,11 +30,11 @@ function TrashProjectsButton() {
|
|||
const handleTrashProject = async (project: Project) => {
|
||||
await trashProject(project.id)
|
||||
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({
|
||||
...project,
|
||||
trashed: true,
|
||||
archived: false,
|
||||
selected: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,15 @@ import { useProjectListContext } from '../../../../context/project-list-context'
|
|||
import { unarchiveProject } from '../../../../util/api'
|
||||
|
||||
function UnarchiveProjectsButton() {
|
||||
const { selectedProjects, updateProjectViewData } = useProjectListContext()
|
||||
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleUnarchiveProjects = async () => {
|
||||
for (const project of selectedProjects) {
|
||||
await unarchiveProject(project.id)
|
||||
updateProjectViewData({ ...project, archived: false, selected: false })
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({ ...project, archived: false })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,15 @@ import { useProjectListContext } from '../../../../context/project-list-context'
|
|||
import { untrashProject } from '../../../../util/api'
|
||||
|
||||
function UntrashProjectsButton() {
|
||||
const { selectedProjects, updateProjectViewData } = useProjectListContext()
|
||||
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
|
||||
useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleUntrashProjects = async () => {
|
||||
for (const project of selectedProjects) {
|
||||
await untrashProject(project.id)
|
||||
updateProjectViewData({ ...project, trashed: false, selected: false })
|
||||
toggleSelectedProject(project.id, false)
|
||||
updateProjectViewData({ ...project, trashed: false })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ function CopyProjectMenuItem() {
|
|||
const {
|
||||
addClonedProjectToViewData,
|
||||
addProjectToTagInView,
|
||||
updateProjectViewData,
|
||||
toggleSelectedProject,
|
||||
selectedProjects,
|
||||
} = useProjectListContext()
|
||||
const { t } = useTranslation()
|
||||
|
@ -43,7 +43,7 @@ function CopyProjectMenuItem() {
|
|||
for (const tag of tags) {
|
||||
addProjectToTagInView(tag._id, clonedProject.project_id)
|
||||
}
|
||||
updateProjectViewData({ ...project, selected: false })
|
||||
toggleSelectedProject(project.id, false)
|
||||
|
||||
if (isMounted.current) {
|
||||
setShowModal(false)
|
||||
|
@ -54,7 +54,7 @@ function CopyProjectMenuItem() {
|
|||
selectedProjects,
|
||||
addClonedProjectToViewData,
|
||||
addProjectToTagInView,
|
||||
updateProjectViewData,
|
||||
toggleSelectedProject,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -96,6 +96,9 @@ export type ProjectListContextValue = {
|
|||
searchText: string
|
||||
setSearchText: React.Dispatch<React.SetStateAction<string>>
|
||||
selectedProjects: Project[]
|
||||
selectedProjectIds: Set<string>
|
||||
setSelectedProjectIds: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||
toggleSelectedProject: (projectId: string, selected?: boolean) => void
|
||||
hiddenProjectsCount: number
|
||||
loadMoreCount: number
|
||||
showAllProjects: () => void
|
||||
|
@ -262,22 +265,44 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
|
|||
setMaxVisibleProjects(maxVisibleProjects + loadMoreCount)
|
||||
}, [maxVisibleProjects, loadMoreCount])
|
||||
|
||||
const [selectedProjectIds, setSelectedProjectIds] = useState(
|
||||
() => new Set<string>()
|
||||
)
|
||||
|
||||
const toggleSelectedProject = useCallback(
|
||||
(projectId: string, selected?: boolean) => {
|
||||
setSelectedProjectIds(selectedProjectIds => {
|
||||
if (selected === true) {
|
||||
selectedProjectIds.add(projectId)
|
||||
} else if (selected === false) {
|
||||
selectedProjectIds.delete(projectId)
|
||||
} else if (selectedProjectIds.has(projectId)) {
|
||||
selectedProjectIds.delete(projectId)
|
||||
} else {
|
||||
selectedProjectIds.add(projectId)
|
||||
}
|
||||
return new Set([...selectedProjectIds])
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const selectedProjects = useMemo(() => {
|
||||
return visibleProjects.filter(project => project.selected)
|
||||
}, [visibleProjects])
|
||||
return visibleProjects.filter(project => selectedProjectIds.has(project.id))
|
||||
}, [selectedProjectIds, visibleProjects])
|
||||
|
||||
const selectOrUnselectAllProjects = useCallback(
|
||||
checked => {
|
||||
const visibleProjectIds = visibleProjects.map(p => p.id)
|
||||
setLoadedProjects(loadedProjects =>
|
||||
loadedProjects.map(p => {
|
||||
if (visibleProjectIds.includes(p.id)) {
|
||||
return { ...p, selected: checked }
|
||||
setSelectedProjectIds(selectedProjectIds => {
|
||||
for (const project of visibleProjects) {
|
||||
if (checked) {
|
||||
selectedProjectIds.add(project.id)
|
||||
} else {
|
||||
return p
|
||||
selectedProjectIds.delete(project.id)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return new Set([...selectedProjectIds])
|
||||
})
|
||||
},
|
||||
[visibleProjects]
|
||||
)
|
||||
|
@ -448,16 +473,19 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
|
|||
selectedTagId,
|
||||
selectFilter,
|
||||
selectedProjects,
|
||||
selectedProjectIds,
|
||||
selectOrUnselectAllProjects,
|
||||
selectTag,
|
||||
searchText,
|
||||
setSearchText,
|
||||
setSelectedProjectIds,
|
||||
setShowCustomPicker,
|
||||
setSort,
|
||||
showAllProjects,
|
||||
showCustomPicker,
|
||||
sort,
|
||||
tags,
|
||||
toggleSelectedProject,
|
||||
totalProjectsCount,
|
||||
untaggedProjectsCount,
|
||||
updateProjectViewData,
|
||||
|
@ -483,17 +511,20 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) {
|
|||
removeProjectFromView,
|
||||
selectedTagId,
|
||||
selectFilter,
|
||||
selectedProjectIds,
|
||||
selectedProjects,
|
||||
selectOrUnselectAllProjects,
|
||||
selectTag,
|
||||
searchText,
|
||||
setSearchText,
|
||||
setSelectedProjectIds,
|
||||
setShowCustomPicker,
|
||||
setSort,
|
||||
showAllProjects,
|
||||
showCustomPicker,
|
||||
sort,
|
||||
tags,
|
||||
toggleSelectedProject,
|
||||
totalProjectsCount,
|
||||
untaggedProjectsCount,
|
||||
updateProjectViewData,
|
||||
|
|
Loading…
Reference in a new issue