Store selected project ids separately (#15598)

GitOrigin-RevId: 56a833fb1524362ff6617afa9be4cb7b6c1984c7
This commit is contained in:
Alf Eaton 2024-01-26 09:24:10 +00:00 committed by Copybot
parent 0cde5be165
commit 49d2ca781e
14 changed files with 114 additions and 57 deletions

View file

@ -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,
]
)

View file

@ -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

View file

@ -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,
]
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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)

View file

@ -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,
})
}

View file

@ -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,
})
}

View file

@ -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 })
}
}

View file

@ -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 })
}
}

View file

@ -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,
]
)

View file

@ -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,