From 49d2ca781e68659d9a90beca520f9cb4e5264afc Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Fri, 26 Jan 2024 09:24:10 +0000 Subject: [PATCH] Store selected project ids separately (#15598) GitOrigin-RevId: 56a833fb1524362ff6617afa9be4cb7b6c1984c7 --- .../modals/rename-project-modal.tsx | 6 ++- .../action-buttons/archive-project-button.tsx | 7 +-- .../action-buttons/copy-project-button.tsx | 5 +- .../action-buttons/trash-project-button.tsx | 7 +-- .../unarchive-project-button.tsx | 9 ++-- .../action-buttons/untrash-project-button.tsx | 8 +-- .../components/table/project-checkbox.tsx | 25 +++++++++ .../table/project-list-table-row.tsx | 25 ++------- .../buttons/archive-projects-button.tsx | 5 +- .../buttons/trash-projects-button.tsx | 5 +- .../buttons/unarchive-projects-button.tsx | 6 ++- .../buttons/untrash-projects-button.tsx | 6 ++- .../menu-items/copy-project-menu-item.tsx | 6 +-- .../context/project-list-context.tsx | 51 +++++++++++++++---- 14 files changed, 114 insertions(+), 57 deletions(-) create mode 100644 services/web/frontend/js/features/project-list/components/table/project-checkbox.tsx diff --git a/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx index c7a24d6fc7..d76f4e4af6 100644 --- a/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/modals/rename-project-modal.tsx @@ -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, ] ) diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/archive-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/archive-project-button.tsx index 6c079c48a6..7639473b4d 100644 --- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/archive-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/archive-project-button.tsx @@ -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 diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx index ce27689a5e..5d4b167dd2 100644 --- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx @@ -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, ] ) diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/trash-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/trash-project-button.tsx index 05ba35f9a4..616810dbf2 100644 --- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/trash-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/trash-project-button.tsx @@ -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 diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/unarchive-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/unarchive-project-button.tsx index 8797a69299..2d1fba5af5 100644 --- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/unarchive-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/unarchive-project-button.tsx @@ -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 diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/untrash-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/untrash-project-button.tsx index 02c375f48f..5513bc5cc3 100644 --- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/untrash-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/untrash-project-button.tsx @@ -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 diff --git a/services/web/frontend/js/features/project-list/components/table/project-checkbox.tsx b/services/web/frontend/js/features/project-list/components/table/project-checkbox.tsx new file mode 100644 index 0000000000..e63cd5a42e --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/table/project-checkbox.tsx @@ -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 ( + + ) +}) + +ProjectCheckbox.displayName = 'ProjectCheckbox' diff --git a/services/web/frontend/js/features/project-list/components/table/project-list-table-row.tsx b/services/web/frontend/js/features/project-list/components/table/project-list-table-row.tsx index bb8bda76ae..235c839ccd 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-list-table-row.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-list-table-row.tsx @@ -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) => { - updateProjectViewData({ ...project, selected: event.target.checked }) - }, - [project, updateProjectViewData] - ) return ( - + @@ -68,3 +52,4 @@ export default function ProjectListTableRow({ ) } +export default memo(ProjectListTableRow) diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx index 14897b2a8b..43779469f0 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx @@ -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, }) } diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx index b57d5cb120..959b252288 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx @@ -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, }) } diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/unarchive-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/unarchive-projects-button.tsx index 8662478bc1..d56e078821 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/unarchive-projects-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/unarchive-projects-button.tsx @@ -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 }) } } diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx index 775eaef30d..ee2081f766 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx @@ -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 }) } } diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx index 6515b7bead..934eb2e637 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx @@ -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, ] ) diff --git a/services/web/frontend/js/features/project-list/context/project-list-context.tsx b/services/web/frontend/js/features/project-list/context/project-list-context.tsx index e11f502894..b7859e5e22 100644 --- a/services/web/frontend/js/features/project-list/context/project-list-context.tsx +++ b/services/web/frontend/js/features/project-list/context/project-list-context.tsx @@ -96,6 +96,9 @@ export type ProjectListContextValue = { searchText: string setSearchText: React.Dispatch> selectedProjects: Project[] + selectedProjectIds: Set + setSelectedProjectIds: React.Dispatch>> + 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() + ) + + 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,