From dff4f2a62ab5fe7aed6c60ad4d25d5b48a409632 Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Tue, 24 Oct 2023 14:29:07 -0400 Subject: [PATCH 01/13] build: Bump client-shared --- dev-client/package-lock.json | 4 +- dev-client/package.json | 2 +- .../components/common/SelectAllCheckboxes.tsx | 50 -------- .../src/screens/SiteTransferProject.tsx | 120 ++++++------------ 4 files changed, 42 insertions(+), 134 deletions(-) delete mode 100644 dev-client/src/components/common/SelectAllCheckboxes.tsx diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index e4f3ad8c1..58c0583a1 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -34,7 +34,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#67a5d10", + "terraso-client-shared": "github:techmatters/terraso-client-shared#07f71bd", "uuid": "^9.0.1", "yup": "^1.3.2" }, @@ -18683,7 +18683,7 @@ }, "node_modules/terraso-client-shared": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#67a5d10da956de739c6ea5dc1533bbf5a12a31df", + "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#07f71bd2236469b5c5ae720546d4d2da58197421", "dependencies": { "@reduxjs/toolkit": "^1.9.7", "jwt-decode": "^3.1.2", diff --git a/dev-client/package.json b/dev-client/package.json index df3060fea..6e797e2a3 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -39,7 +39,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#67a5d10", + "terraso-client-shared": "github:techmatters/terraso-client-shared#07f71bd", "uuid": "^9.0.1", "yup": "^1.3.2" }, diff --git a/dev-client/src/components/common/SelectAllCheckboxes.tsx b/dev-client/src/components/common/SelectAllCheckboxes.tsx deleted file mode 100644 index b4f299c73..000000000 --- a/dev-client/src/components/common/SelectAllCheckboxes.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import {Checkbox, Text, VStack} from 'native-base'; -import {useState} from 'react'; -import {useTranslation} from 'react-i18next'; -import {LogBox} from 'react-native'; - -type CheckboxItem = {key: string; value: string; label: string}; - -type Props = { - items: CheckboxItem[]; - onUpdate: (items: string[]) => void; -}; - -export default function SelectAllCheckboxes({items, onUpdate}: Props) { - const {t} = useTranslation(); - // TODO: see https://github.com/techmatters/terraso-mobile-client/issues/82 - LogBox.ignoreLogs([ - 'We can not support a function callback. See Github Issues for details https://github.com/adobe/react-spectrum/issues/2320', - ]); - const [selected, setSelected] = useState([] as string[]); - return ( - - { - if (selected.length < items.length) { - let all = items.map(({value}) => String(value)); - setSelected(all); - } else { - setSelected([]); - } - onUpdate(selected); - }}> - - {t('general.select_all', '')} - - - - { - setSelected(values ?? []); - }}> - {items.map(({value, key, label}) => ( - - {label} - - ))} - - - - ); -} diff --git a/dev-client/src/screens/SiteTransferProject.tsx b/dev-client/src/screens/SiteTransferProject.tsx index 27ac10186..109316faa 100644 --- a/dev-client/src/screens/SiteTransferProject.tsx +++ b/dev-client/src/screens/SiteTransferProject.tsx @@ -1,8 +1,5 @@ import {HStack, Heading, Text, VStack} from 'native-base'; import {SearchBar} from 'terraso-mobile-client/components/common/search/SearchBar'; -import {useTranslation} from 'react-i18next'; -import {useCallback, useMemo} from 'react'; -import SelectAllCheckboxes from 'terraso-mobile-client/components/common/SelectAllCheckboxes'; import {Accordion} from 'terraso-mobile-client/components/common/Accordion'; import {useSelector} from 'terraso-mobile-client/model/store'; import { @@ -10,35 +7,10 @@ import { ScreenScaffold, } from 'terraso-mobile-client/screens/ScreenScaffold'; import {useTextSearch} from 'terraso-mobile-client/components/common/search/search'; -import {Site} from 'terraso-client-shared/site/siteSlice'; - -type ItemProps = { - projectName: string; - sites: Site[]; -}; - -const SiteTransferItem = ({projectName, sites}: ItemProps) => { - const items = sites.map(site => ({ - value: site.id, - label: site.name, - key: site.id, - })); - const updateSelected = useCallback((currentItems: string[]) => { - console.debug(currentItems); - }, []); - - const head = ( - - {projectName} - ({items.length}) - - ); - return ( - - - - ); -}; +import {selectProjectsWithTransferrableSites} from 'terraso-client-shared/selectors'; +import {useTranslation} from 'react-i18next'; +import {groupBy} from 'terraso-mobile-client/util'; +import {useEffect, useMemo} from 'react'; type Props = {projectId: string}; @@ -47,50 +19,36 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const projects = useSelector(state => state.project.projects); const project = projects[projectId]; - const sites = useSelector(state => state.site.sites); - const siteList = useMemo(() => Object.values(sites), [sites]); + const sites = useSelector(state => + selectProjectsWithTransferrableSites(state, 'manager'), + ); + const { results: searchedSites, query, setQuery, - } = useTextSearch({data: siteList, keys: ['name']}); - - const sortedSitesByProject = useMemo(() => { - const nonProjectSites = searchedSites.filter( - site => site.projectId === undefined, - ); - const projectSites = searchedSites.filter( - site => site.projectId !== undefined && site.projectId !== project.id, - ); + } = useTextSearch({data: sites, keys: ['siteName']}); - const sitesByProject = projectSites.reduce( - (projectMap, site) => - Object.assign(projectMap, { - [site.projectId!]: {...projectMap[site.projectId!], [site.id]: site}, - }), - {} as Record>, - ); + const groupedByProject = useMemo(() => { + const clusters = new Map(); + for (const site of searchedSites) { + const key = [site.projectId, site.projectName]; + let current = null; + if (!clusters.has(key)) { + current = []; + } else { + current = clusters.get(key); + } + current.push(site); + clusters.set(key, current); + } + return Array.from(clusters) as [ + [string, string], + (typeof searchedSites)[number][], + ][]; + }, [searchedSites]); - return [ - [undefined, nonProjectSites] as const, - ...Object.entries(sitesByProject) - .sort(([projectIdA], [projectIdB]) => - projects[projectIdA].name.localeCompare(projects[projectIdB].name), - ) - .map( - ([projIds, projSites]) => - [projects[projIds], Object.values(projSites)] as const, - ), - ].map( - ([pId, projSites]) => - [ - pId, - projSites.sort((siteA, siteB) => - siteA.name.localeCompare(siteB.name), - ), - ] as const, - ); - }, [projects, project.id, searchedSites]); + useEffect(() => console.debug(groupedByProject), [groupedByProject]); return ( { setQuery={setQuery} placeholder={t('site.search.placeholder')} /> - {sortedSitesByProject.map(([itemProject, itemSites]) => { - return ( - - ); - })} + {groupedByProject.map(([[projectId, projectName], cluster]) => ( + + {projectName} {cluster.length} + + }> + World + + ))} ); From 41c367e716e3287a3e01e86f68ff1e531ef2a66d Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Wed, 25 Oct 2023 17:44:15 -0400 Subject: [PATCH 02/13] feat: Add Checkbox Group --- dev-client/package-lock.json | 16 +++ dev-client/package.json | 1 + .../src/components/common/CheckboxGroup.tsx | 103 ++++++++++++++++++ .../src/screens/SiteTransferProject.tsx | 64 +++++++---- 4 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 dev-client/src/components/common/CheckboxGroup.tsx diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index 58c0583a1..8b5dd47dd 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@gorhom/bottom-sheet": "^4.5.1", + "@react-native-community/checkbox": "^0.5.16", "@react-native/metro-config": "^0.74.0", "@react-navigation/material-top-tabs": "^6.6.5", "@react-navigation/native": "^6.1.7", @@ -4258,6 +4259,21 @@ "react-native": "*" } }, + "node_modules/@react-native-community/checkbox": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@react-native-community/checkbox/-/checkbox-0.5.16.tgz", + "integrity": "sha512-j4fmWe77EAayGnKJ52BljlN8apLT3xjxG/pJOA6HZ4ew63FiXmnY7VtxTzmvDKgSPrETdQc2lmx5mdXTAufJnw==", + "peerDependencies": { + "react": "*", + "react-native": ">= 0.62", + "react-native-windows": ">=0.62" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, "node_modules/@react-native-community/cli": { "version": "11.3.7", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-11.3.7.tgz", diff --git a/dev-client/package.json b/dev-client/package.json index 6e797e2a3..01db377a1 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@gorhom/bottom-sheet": "^4.5.1", + "@react-native-community/checkbox": "^0.5.16", "@react-native/metro-config": "^0.74.0", "@react-navigation/material-top-tabs": "^6.6.5", "@react-navigation/native": "^6.1.7", diff --git a/dev-client/src/components/common/CheckboxGroup.tsx b/dev-client/src/components/common/CheckboxGroup.tsx new file mode 100644 index 000000000..2b6ea58b0 --- /dev/null +++ b/dev-client/src/components/common/CheckboxGroup.tsx @@ -0,0 +1,103 @@ +import {Box, FormControl, HStack} from 'native-base'; +import {useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import CheckBox from '@react-native-community/checkbox'; + +export const useCheckboxHandlers = (groups: Record) => { + const [allChecked, setAllChecked] = useState>( + Object.keys(groups).reduce((x, y) => ({...x, [y]: false}), {}), + ); + const [checkedValues, setCheckedValues] = useState>( + Object.entries(groups).reduce( + (x, [label, length]) => ({...x, [label]: Array(length).fill(false)}), + {}, + ), + ); + + const setGroupChecked = (key: string, checked: boolean) => { + setAllChecked(state => { + const newState = {...state}; + newState[key] = checked; + return newState; + }); + }; + + const onValueChanged = (key: string, n: number) => (checked: boolean) => + setCheckedValues(state => { + let newState = {...state}; + newState[key][n] = checked; + if (allChecked[key] && !checked) { + setGroupChecked(key, false); + } else if (!allChecked[key] && newState[key].every(Boolean)) { + setGroupChecked(key, true); + } + return newState; + }); + + const onAllChecked = (key: string) => (checked: boolean) => { + setGroupChecked(key, checked); + setCheckedValues(state => { + let newState = {...state}; + newState[key] = newState[key].fill(checked); + return newState; + }); + }; + + return { + allChecked, + checkedValues, + onValueChanged, + onAllChecked, + }; +}; + +type CheckboxProps = { + label: string; + id: string; + onValue: (checked: boolean) => void; + checked: boolean; +}; + +type Props = { + checkboxes: CheckboxProps[]; + allChecked: boolean; + onCheckAll: (checked: boolean) => void; + groupName: string; +}; + +const CheckboxGroup = ({ + checkboxes, + groupName, + allChecked, + onCheckAll, +}: Props) => { + const {t} = useTranslation(); + return ( + + + + + {t('general.select_all')} + + + {checkboxes.map(({label, id, onValue, checked}) => ( + + + + {label} + + + ))} + + ); +}; + +export default CheckboxGroup; diff --git a/dev-client/src/screens/SiteTransferProject.tsx b/dev-client/src/screens/SiteTransferProject.tsx index 109316faa..46b3b2862 100644 --- a/dev-client/src/screens/SiteTransferProject.tsx +++ b/dev-client/src/screens/SiteTransferProject.tsx @@ -1,4 +1,4 @@ -import {HStack, Heading, Text, VStack} from 'native-base'; +import {Heading, Text, VStack} from 'native-base'; import {SearchBar} from 'terraso-mobile-client/components/common/search/SearchBar'; import {Accordion} from 'terraso-mobile-client/components/common/Accordion'; import {useSelector} from 'terraso-mobile-client/model/store'; @@ -9,8 +9,10 @@ import { import {useTextSearch} from 'terraso-mobile-client/components/common/search/search'; import {selectProjectsWithTransferrableSites} from 'terraso-client-shared/selectors'; import {useTranslation} from 'react-i18next'; -import {groupBy} from 'terraso-mobile-client/util'; import {useEffect, useMemo} from 'react'; +import CheckboxGroup, { + useCheckboxHandlers, +} from 'terraso-mobile-client/components/common/CheckboxGroup'; type Props = {projectId: string}; @@ -32,7 +34,7 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const groupedByProject = useMemo(() => { const clusters = new Map(); for (const site of searchedSites) { - const key = [site.projectId, site.projectName]; + const key = projectId; let current = null; if (!clusters.has(key)) { current = []; @@ -42,13 +44,23 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { current.push(site); clusters.set(key, current); } - return Array.from(clusters) as [ - [string, string], - (typeof searchedSites)[number][], - ][]; + return Object.fromEntries(clusters) as Record< + string, + (typeof searchedSites)[number][] + >; }, [searchedSites]); - useEffect(() => console.debug(groupedByProject), [groupedByProject]); + const checkboxHandlers = useCheckboxHandlers( + Object.entries(groupedByProject).reduce( + (x, [projectId, fields]) => ({ + ...x, + [projectId]: fields.length, + }), + {}, + ), + ); + + // useEffect(() => console.debug(groupedByProject)); return ( { setQuery={setQuery} placeholder={t('site.search.placeholder')} /> - {groupedByProject.map(([[projectId, projectName], cluster]) => ( - - {projectName} {cluster.length} - - }> - World - - ))} + {Object.entries(groupedByProject).map( + ([projectId, cluster]) => + cluster && + cluster.length && ( + + {cluster[0].projectName} {cluster.length} + + }> + ({ + label: site.siteName, + id: site.siteId, + onValue: checkboxHandlers.onValueChanged(site.projectId, i), + checked: checkboxHandlers.checkedValues[site.projectId][i], + }))} + allChecked={checkboxHandlers.allChecked[projectId]} + onCheckAll={checkboxHandlers.onAllChecked(projectId)} + groupName={projectId} + /> + + ), + )} ); From 3c39db9419ecfb8d117847dd83c058d069a87089 Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Thu, 26 Oct 2023 14:27:14 -0400 Subject: [PATCH 03/13] feat: Add proper logic to site transfer --- dev-client/package-lock.json | 9 +- dev-client/package.json | 3 +- .../src/components/common/CheckboxGroup.tsx | 78 +++----- .../src/screens/SiteTransferProject.tsx | 170 ++++++++++++------ 4 files changed, 143 insertions(+), 117 deletions(-) diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index 8b5dd47dd..543cbc73a 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -17,6 +17,7 @@ "@rnmapbox/maps": "^10.0.12", "formik": "^2.4.3", "i18next": "^23.4.2", + "lodash": "^4.17.21", "native-base": "^3.4.28", "react": "18.2.0", "react-i18next": "^13.3.0", @@ -35,7 +36,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#07f71bd", + "terraso-client-shared": "github:techmatters/terraso-client-shared#64d456b", "uuid": "^9.0.1", "yup": "^1.3.2" }, @@ -18699,14 +18700,14 @@ }, "node_modules/terraso-client-shared": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#07f71bd2236469b5c5ae720546d4d2da58197421", + "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#64d456bfff142502b973f0e2d691e18e333f85e9", "dependencies": { "@reduxjs/toolkit": "^1.9.7", "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "react": "^18.2.0", - "react-redux": "^8.1.2", - "terraso-backend": "github:techmatters/terraso-backend#dc15c3a", + "react-redux": "^8.1.3", + "terraso-backend": "github:techmatters/terraso-backend#3d323b5", "uuid": "^9.0.1" } }, diff --git a/dev-client/package.json b/dev-client/package.json index 01db377a1..64d79d837 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -22,6 +22,7 @@ "@rnmapbox/maps": "^10.0.12", "formik": "^2.4.3", "i18next": "^23.4.2", + "lodash": "^4.17.21", "native-base": "^3.4.28", "react": "18.2.0", "react-i18next": "^13.3.0", @@ -40,7 +41,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#07f71bd", + "terraso-client-shared": "github:techmatters/terraso-client-shared#64d456b", "uuid": "^9.0.1", "yup": "^1.3.2" }, diff --git a/dev-client/src/components/common/CheckboxGroup.tsx b/dev-client/src/components/common/CheckboxGroup.tsx index 2b6ea58b0..c6a288c0e 100644 --- a/dev-client/src/components/common/CheckboxGroup.tsx +++ b/dev-client/src/components/common/CheckboxGroup.tsx @@ -1,94 +1,56 @@ import {Box, FormControl, HStack} from 'native-base'; -import {useState} from 'react'; +import {useCallback, useMemo} from 'react'; import {useTranslation} from 'react-i18next'; import CheckBox from '@react-native-community/checkbox'; -export const useCheckboxHandlers = (groups: Record) => { - const [allChecked, setAllChecked] = useState>( - Object.keys(groups).reduce((x, y) => ({...x, [y]: false}), {}), - ); - const [checkedValues, setCheckedValues] = useState>( - Object.entries(groups).reduce( - (x, [label, length]) => ({...x, [label]: Array(length).fill(false)}), - {}, - ), - ); - - const setGroupChecked = (key: string, checked: boolean) => { - setAllChecked(state => { - const newState = {...state}; - newState[key] = checked; - return newState; - }); - }; - - const onValueChanged = (key: string, n: number) => (checked: boolean) => - setCheckedValues(state => { - let newState = {...state}; - newState[key][n] = checked; - if (allChecked[key] && !checked) { - setGroupChecked(key, false); - } else if (!allChecked[key] && newState[key].every(Boolean)) { - setGroupChecked(key, true); - } - return newState; - }); - - const onAllChecked = (key: string) => (checked: boolean) => { - setGroupChecked(key, checked); - setCheckedValues(state => { - let newState = {...state}; - newState[key] = newState[key].fill(checked); - return newState; - }); - }; - - return { - allChecked, - checkedValues, - onValueChanged, - onAllChecked, - }; -}; - type CheckboxProps = { label: string; id: string; - onValue: (checked: boolean) => void; checked: boolean; }; type Props = { checkboxes: CheckboxProps[]; - allChecked: boolean; - onCheckAll: (checked: boolean) => void; groupName: string; + groupId: string; + onChangeValue: ( + groupId: string, + checkboxId: string, + ) => (checked: boolean) => void; }; const CheckboxGroup = ({ checkboxes, groupName, - allChecked, - onCheckAll, + groupId, + onChangeValue, }: Props) => { const {t} = useTranslation(); + const selectAllChecked = useMemo(() => { + return checkboxes.every(({checked}) => checked); + }, [checkboxes]); + const onSelectAll = useCallback(() => { + checkboxes.forEach(({id: checkboxId}) => + onChangeValue(groupId, checkboxId)(!selectAllChecked), + ); + }, [checkboxes, selectAllChecked, onChangeValue, groupId]); return ( {t('general.select_all')} - {checkboxes.map(({label, id, onValue, checked}) => ( + {checkboxes.map(({label, id, checked}) => ( diff --git a/dev-client/src/screens/SiteTransferProject.tsx b/dev-client/src/screens/SiteTransferProject.tsx index 46b3b2862..c35add27b 100644 --- a/dev-client/src/screens/SiteTransferProject.tsx +++ b/dev-client/src/screens/SiteTransferProject.tsx @@ -9,58 +9,118 @@ import { import {useTextSearch} from 'terraso-mobile-client/components/common/search/search'; import {selectProjectsWithTransferrableSites} from 'terraso-client-shared/selectors'; import {useTranslation} from 'react-i18next'; -import {useEffect, useMemo} from 'react'; -import CheckboxGroup, { - useCheckboxHandlers, -} from 'terraso-mobile-client/components/common/CheckboxGroup'; +import {useCallback, useEffect, useMemo, useState} from 'react'; +import CheckboxGroup from 'terraso-mobile-client/components/common/CheckboxGroup'; type Props = {projectId: string}; export const SiteTransferProjectScreen = ({projectId}: Props) => { const {t} = useTranslation(); - const projects = useSelector(state => state.project.projects); - const project = projects[projectId]; - const sites = useSelector(state => + const project = useSelector(state => state.project.projects[projectId]); + const {projects, sites} = useSelector(state => selectProjectsWithTransferrableSites(state, 'manager'), ); + const sitesExcludingCurrent = useMemo(() => { + return sites.filter(site => site.projectId !== projectId); + }, [sites, projectId]); + + const projectsExcludingCurrent = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let {[projectId]: _, ...rest} = projects; + return rest; + }, [projects, projectId]); const { results: searchedSites, query, setQuery, - } = useTextSearch({data: sites, keys: ['siteName']}); + } = useTextSearch({data: sitesExcludingCurrent, keys: ['siteName']}); - const groupedByProject = useMemo(() => { - const clusters = new Map(); - for (const site of searchedSites) { - const key = projectId; - let current = null; - if (!clusters.has(key)) { - current = []; - } else { - current = clusters.get(key); + const displayedProjects = useMemo(() => { + const displayed: Record< + string, + {projectName: string; sites: typeof searchedSites} + > = {}; + for (let site of searchedSites) { + if (!(site.projectId in displayed)) { + displayed[site.projectId] = { + projectName: site.projectName, + sites: [], + }; } - current.push(site); - clusters.set(key, current); + displayed[site.projectId].sites.push(site); } - return Object.fromEntries(clusters) as Record< - string, - (typeof searchedSites)[number][] - >; - }, [searchedSites]); + if (!query) { + // want to display empty projects if not query! + for (const {projectId: projId, projectName} of Object.values( + projectsExcludingCurrent, + )) { + if (!(projId in displayed)) { + displayed[projId] = {projectName, sites: []}; + } + } + } + return displayed; + }, [projectsExcludingCurrent, searchedSites, query]); + + const projectRecord = useMemo(() => { + const record: Record> = {}; + for (const site of sitesExcludingCurrent) { + if (!(site.projectId in record)) { + record[site.projectId] = {}; + } + record[site.projectId][site.siteId] = false; + } + return record; + }, [sitesExcludingCurrent]); + + const [projState, setProjState] = useState({}); + + const removeKeys = (a: any, b: any) => { + const remove = [a, b]; + let currA, currB; + while (remove.length) { + currA = remove.pop(); + currB = remove.pop(); + for (const keyA of Object.keys(currA)) { + if (!(keyA in currB)) { + delete currA[keyA]; + continue; + } + const valA = currA[keyA]; + const valB = currB[keyA]; + if (typeof valA !== typeof valB) { + delete currA[keyA]; + continue; + } + if (typeof valA === 'object' && !Array.isArray(valA) && valA !== null) { + remove.push(valA, valB); + } + } + } + }; + + useEffect(() => { + setProjState(latestState => { + const newState = Object.assign({...latestState}, projectRecord); + removeKeys(newState, projectRecord); + return newState; + }); + }, [projectRecord, setProjState]); - const checkboxHandlers = useCheckboxHandlers( - Object.entries(groupedByProject).reduce( - (x, [projectId, fields]) => ({ - ...x, - [projectId]: fields.length, - }), - {}, - ), + const onCheckboxChange = useCallback( + (groupId: string, checkboxId: string) => (checked: boolean) => { + setProjState(currState => { + const newState = {...currState}; + newState[groupId][checkboxId] = checked; + return newState; + }); + }, + [setProjState], ); - // useEffect(() => console.debug(groupedByProject)); + // useEffect(() => console.debug(displayedProjects)); return ( { setQuery={setQuery} placeholder={t('site.search.placeholder')} /> - {Object.entries(groupedByProject).map( - ([projectId, cluster]) => - cluster && - cluster.length && ( - - {cluster[0].projectName} {cluster.length} - - }> + {Object.entries(displayedProjects).map( + ([projId, {projectName, sites: projectSites}]) => ( + + {projectName} - {projectSites.length} + + }> + {projectSites.length > 0 ? ( ({ - label: site.siteName, - id: site.siteId, - onValue: checkboxHandlers.onValueChanged(site.projectId, i), - checked: checkboxHandlers.checkedValues[site.projectId][i], + groupName={projectName} + groupId={projId} + checkboxes={projectSites.map(({siteId, siteName}) => ({ + label: siteName, + id: siteId, + checked: + projState && projState[projId] + ? projState[projId][siteId] + : false, }))} - allChecked={checkboxHandlers.allChecked[projectId]} - onCheckAll={checkboxHandlers.onAllChecked(projectId)} - groupName={projectId} + onChangeValue={onCheckboxChange} /> - - ), + ) : undefined} + + ), )} From 83da222d9a5a4bbf8b73c3e9a97433a8d8be763d Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Thu, 26 Oct 2023 15:34:25 -0400 Subject: [PATCH 04/13] feat: Dispatch transfer mutation --- dev-client/package-lock.json | 6 ++--- dev-client/package.json | 2 +- .../src/screens/SiteTransferProject.tsx | 24 ++++++++++++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index 543cbc73a..5b037ba32 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -36,7 +36,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#64d456b", + "terraso-client-shared": "github:techmatters/terraso-client-shared#2e4c775", "uuid": "^9.0.1", "yup": "^1.3.2" }, @@ -18700,14 +18700,14 @@ }, "node_modules/terraso-client-shared": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#64d456bfff142502b973f0e2d691e18e333f85e9", + "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#2e4c775c32b93a54337abe8f048ad3417a64b6c8", "dependencies": { "@reduxjs/toolkit": "^1.9.7", "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "react": "^18.2.0", "react-redux": "^8.1.3", - "terraso-backend": "github:techmatters/terraso-backend#3d323b5", + "terraso-backend": "github:techmatters/terraso-backend#f671497", "uuid": "^9.0.1" } }, diff --git a/dev-client/package.json b/dev-client/package.json index 64d79d837..dd74caa7d 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -41,7 +41,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#64d456b", + "terraso-client-shared": "github:techmatters/terraso-client-shared#2e4c775", "uuid": "^9.0.1", "yup": "^1.3.2" }, diff --git a/dev-client/src/screens/SiteTransferProject.tsx b/dev-client/src/screens/SiteTransferProject.tsx index c35add27b..d6854df25 100644 --- a/dev-client/src/screens/SiteTransferProject.tsx +++ b/dev-client/src/screens/SiteTransferProject.tsx @@ -1,7 +1,7 @@ -import {Heading, Text, VStack} from 'native-base'; +import {Fab, Heading, Text, VStack} from 'native-base'; import {SearchBar} from 'terraso-mobile-client/components/common/search/SearchBar'; import {Accordion} from 'terraso-mobile-client/components/common/Accordion'; -import {useSelector} from 'terraso-mobile-client/model/store'; +import {useDispatch, useSelector} from 'terraso-mobile-client/model/store'; import { AppBar, ScreenScaffold, @@ -11,11 +11,13 @@ import {selectProjectsWithTransferrableSites} from 'terraso-client-shared/select import {useTranslation} from 'react-i18next'; import {useCallback, useEffect, useMemo, useState} from 'react'; import CheckboxGroup from 'terraso-mobile-client/components/common/CheckboxGroup'; +import {transferSites} from 'terraso-client-shared/site/siteSlice'; type Props = {projectId: string}; export const SiteTransferProjectScreen = ({projectId}: Props) => { const {t} = useTranslation(); + const dispatch = useDispatch(); const project = useSelector(state => state.project.projects[projectId]); const {projects, sites} = useSelector(state => @@ -120,7 +122,15 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { [setProjState], ); - // useEffect(() => console.debug(displayedProjects)); + const onSubmit = useCallback(() => { + const siteIds = Object.values(projState).flatMap(projSites => + Object.entries(projSites) + .filter(([_, checked]) => checked) + .map(([siteId, _]) => siteId), + ); + const payload = {projectId, siteIds}; + return dispatch(transferSites(payload)); + }, [projState]); return ( { ), )} + + Transfer sites + + } + onPress={onSubmit} + /> ); }; From 2c6795008b17d300d685bbf33fbc2f2ab8fb9662 Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Thu, 26 Oct 2023 16:11:26 -0400 Subject: [PATCH 05/13] fix: Render checkboxes in FlatList --- dev-client/src/screens/AppScaffold.tsx | 2 +- ...oject.tsx => SiteTransferProjectScreen.tsx} | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) rename dev-client/src/screens/{SiteTransferProject.tsx => SiteTransferProjectScreen.tsx} (94%) diff --git a/dev-client/src/screens/AppScaffold.tsx b/dev-client/src/screens/AppScaffold.tsx index 3abd5f44a..36279e2a0 100644 --- a/dev-client/src/screens/AppScaffold.tsx +++ b/dev-client/src/screens/AppScaffold.tsx @@ -15,7 +15,7 @@ import {ProjectListScreen} from 'terraso-mobile-client/screens/ProjectListScreen import {ProjectViewScreen} from 'terraso-mobile-client/screens/ProjectViewScreen'; import {CreateProjectScreen} from 'terraso-mobile-client/screens/CreateProjectScreen'; import {HomeScreen} from 'terraso-mobile-client/screens/HomeScreen'; -import {SiteTransferProjectScreen} from 'terraso-mobile-client/screens/SiteTransferProject'; +import {SiteTransferProjectScreen} from 'terraso-mobile-client/screens/SiteTransferProjectScreen'; import {CreateSiteScreen} from 'terraso-mobile-client/screens/CreateSiteScreen'; import {useNavigation as useNavigationNative} from '@react-navigation/native'; import {LocationDashboardScreen} from 'terraso-mobile-client/components/sites/LocationDashboardScreen'; diff --git a/dev-client/src/screens/SiteTransferProject.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx similarity index 94% rename from dev-client/src/screens/SiteTransferProject.tsx rename to dev-client/src/screens/SiteTransferProjectScreen.tsx index d6854df25..20016974d 100644 --- a/dev-client/src/screens/SiteTransferProject.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -1,4 +1,4 @@ -import {Fab, Heading, Text, VStack} from 'native-base'; +import {Fab, FlatList, Heading, Text, VStack} from 'native-base'; import {SearchBar} from 'terraso-mobile-client/components/common/search/SearchBar'; import {Accordion} from 'terraso-mobile-client/components/common/Accordion'; import {useDispatch, useSelector} from 'terraso-mobile-client/model/store'; @@ -66,6 +66,11 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { return displayed; }, [projectsExcludingCurrent, searchedSites, query]); + const listData = useMemo( + () => Object.entries(displayedProjects), + [displayedProjects], + ); + const projectRecord = useMemo(() => { const record: Record> = {}; for (const site of sitesExcludingCurrent) { @@ -144,8 +149,11 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { setQuery={setQuery} placeholder={t('site.search.placeholder')} /> - {Object.entries(displayedProjects).map( - ([projId, {projectName, sites: projectSites}]) => ( + ( { /> ) : undefined} - ), - )} + )} + /> Date: Thu, 26 Oct 2023 16:20:19 -0400 Subject: [PATCH 06/13] fix: Don't display sites of empty projects --- .../src/components/common/Accordion.tsx | 20 +++++++++++++------ .../src/screens/SiteTransferProjectScreen.tsx | 3 ++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/dev-client/src/components/common/Accordion.tsx b/dev-client/src/components/common/Accordion.tsx index 18e2e7d7a..5521d3634 100644 --- a/dev-client/src/components/common/Accordion.tsx +++ b/dev-client/src/components/common/Accordion.tsx @@ -6,9 +6,15 @@ type Props = { Head: ReactNode; children: ReactNode; initiallyOpen?: boolean; + disableOpen?: boolean; }; -export function Accordion({Head, children, initiallyOpen = false}: Props) { +export function Accordion({ + Head, + children, + initiallyOpen = false, + disableOpen = false, +}: Props) { const [open, setOpen] = useState(initiallyOpen); const onPress = useCallback(() => { setOpen(!open); @@ -23,11 +29,13 @@ export function Accordion({Head, children, initiallyOpen = false}: Props) { justifyContent="space-between" px="16px"> {Head} - + {!disableOpen && ( + + )} {open && children} diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index 20016974d..912e97182 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -160,7 +160,8 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { {projectName} - {projectSites.length} - }> + } + disableOpen={projectSites.length === 0}> {projectSites.length > 0 ? ( Date: Thu, 26 Oct 2023 16:22:33 -0400 Subject: [PATCH 07/13] fix: Do some sorting --- .../src/screens/SiteTransferProjectScreen.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index 912e97182..afc16d27d 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -67,7 +67,10 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { }, [projectsExcludingCurrent, searchedSites, query]); const listData = useMemo( - () => Object.entries(displayedProjects), + () => + Object.entries(displayedProjects).sort((a, b) => + a[1].projectName.localeCompare(b[1].projectName), + ), [displayedProjects], ); @@ -166,14 +169,16 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { ({ - label: siteName, - id: siteId, - checked: - projState && projState[projId] - ? projState[projId][siteId] - : false, - }))} + checkboxes={projectSites + .map(({siteId, siteName}) => ({ + label: siteName, + id: siteId, + checked: + projState && projState[projId] + ? projState[projId][siteId] + : false, + })) + .sort((a, b) => a.label.localeCompare(b.label))} onChangeValue={onCheckboxChange} /> ) : undefined} From cd694041ae3a1828c82fc76873ab7f266959bc1d Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Thu, 26 Oct 2023 16:25:01 -0400 Subject: [PATCH 08/13] feat: Navigate back after transfer --- dev-client/src/screens/SiteTransferProjectScreen.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index afc16d27d..5143f7bf7 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -12,12 +12,14 @@ import {useTranslation} from 'react-i18next'; import {useCallback, useEffect, useMemo, useState} from 'react'; import CheckboxGroup from 'terraso-mobile-client/components/common/CheckboxGroup'; import {transferSites} from 'terraso-client-shared/site/siteSlice'; +import {useNavigation} from 'terraso-mobile-client/screens/AppScaffold'; type Props = {projectId: string}; export const SiteTransferProjectScreen = ({projectId}: Props) => { const {t} = useTranslation(); const dispatch = useDispatch(); + const navigation = useNavigation(); const project = useSelector(state => state.project.projects[projectId]); const {projects, sites} = useSelector(state => @@ -130,14 +132,15 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { [setProjState], ); - const onSubmit = useCallback(() => { + const onSubmit = useCallback(async () => { const siteIds = Object.values(projState).flatMap(projSites => Object.entries(projSites) .filter(([_, checked]) => checked) .map(([siteId, _]) => siteId), ); const payload = {projectId, siteIds}; - return dispatch(transferSites(payload)); + await dispatch(transferSites(payload)); + return navigation.pop(); }, [projState]); return ( From d78cb4b8ed422aca7b4915bd2f4ded3c217dcc44 Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Thu, 26 Oct 2023 17:31:02 -0400 Subject: [PATCH 09/13] fix: Styling changes --- .../src/components/common/CheckboxGroup.tsx | 30 +++--- .../src/screens/SiteTransferProjectScreen.tsx | 92 +++++++++---------- dev-client/src/theme.ts | 8 ++ dev-client/src/translations/en.json | 3 +- dev-client/src/util.ts | 28 +++++- 5 files changed, 94 insertions(+), 67 deletions(-) diff --git a/dev-client/src/components/common/CheckboxGroup.tsx b/dev-client/src/components/common/CheckboxGroup.tsx index c6a288c0e..ce567f4f8 100644 --- a/dev-client/src/components/common/CheckboxGroup.tsx +++ b/dev-client/src/components/common/CheckboxGroup.tsx @@ -1,4 +1,4 @@ -import {Box, FormControl, HStack} from 'native-base'; +import {Box, FormControl, HStack, VStack} from 'native-base'; import {useCallback, useMemo} from 'react'; import {useTranslation} from 'react-i18next'; import CheckBox from '@react-native-community/checkbox'; @@ -42,22 +42,24 @@ const CheckboxGroup = ({ onValueChange={onSelectAll} value={selectAllChecked} /> - + {t('general.select_all')} - {checkboxes.map(({label, id, checked}) => ( - - - - {label} - - - ))} + + {checkboxes.map(({label, id, checked}) => ( + + + + {label} + + + ))} + ); }; diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index 5143f7bf7..dee1704a9 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -1,4 +1,4 @@ -import {Fab, FlatList, Heading, Text, VStack} from 'native-base'; +import {Box, Fab, FlatList, HStack, Heading, Text, VStack} from 'native-base'; import {SearchBar} from 'terraso-mobile-client/components/common/search/SearchBar'; import {Accordion} from 'terraso-mobile-client/components/common/Accordion'; import {useDispatch, useSelector} from 'terraso-mobile-client/model/store'; @@ -13,6 +13,8 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; import CheckboxGroup from 'terraso-mobile-client/components/common/CheckboxGroup'; import {transferSites} from 'terraso-client-shared/site/siteSlice'; import {useNavigation} from 'terraso-mobile-client/screens/AppScaffold'; +import {removeKeys} from 'terraso-mobile-client/util'; +import {FormTooltip} from 'terraso-mobile-client/components/common/Form'; type Props = {projectId: string}; @@ -89,30 +91,6 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const [projState, setProjState] = useState({}); - const removeKeys = (a: any, b: any) => { - const remove = [a, b]; - let currA, currB; - while (remove.length) { - currA = remove.pop(); - currB = remove.pop(); - for (const keyA of Object.keys(currA)) { - if (!(keyA in currB)) { - delete currA[keyA]; - continue; - } - const valA = currA[keyA]; - const valB = currB[keyA]; - if (typeof valA !== typeof valB) { - delete currA[keyA]; - continue; - } - if (typeof valA === 'object' && !Array.isArray(valA) && valA !== null) { - remove.push(valA, valB); - } - } - } - }; - useEffect(() => { setProjState(latestState => { const newState = Object.assign({...latestState}, projectRecord); @@ -147,14 +125,21 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { }> - - {t('projects.transfer_sites.heading', '')} - {t('projects.transfer_sites.description', '')} - + + + + {t('projects.transfer_sites.heading', '')} + + {t('projects.transfer_sites.tooltip')} + + + {t('projects.transfer_sites.description', '')} + + { - {projectName} - {projectSites.length} + + {projectName} ({projectSites.length}) } + initiallyOpen={projectSites.length > 0} disableOpen={projectSites.length === 0}> {projectSites.length > 0 ? ( - ({ - label: siteName, - id: siteId, - checked: - projState && projState[projId] - ? projState[projId][siteId] - : false, - })) - .sort((a, b) => a.label.localeCompare(b.label))} - onChangeValue={onCheckboxChange} - /> + + ({ + label: siteName, + id: siteId, + checked: + projState && projState[projId] + ? projState[projId][siteId] + : false, + })) + .sort((a, b) => a.label.localeCompare(b.label))} + onChangeValue={onCheckboxChange} + /> + ) : undefined} )} diff --git a/dev-client/src/theme.ts b/dev-client/src/theme.ts index a91a32a90..3f4f7e883 100644 --- a/dev-client/src/theme.ts +++ b/dev-client/src/theme.ts @@ -182,6 +182,14 @@ export const theme = extendTheme({ letterSpacing: '0.15px', }, }, + body1: { + _text: { + color: 'text.primary', + fontWeight: 400, + fontSize: '14px', + lineHeight: '24px', + }, + }, }, defaultProps: { _text: { diff --git a/dev-client/src/translations/en.json b/dev-client/src/translations/en.json index a4c1bfbb0..f2b1f9154 100644 --- a/dev-client/src/translations/en.json +++ b/dev-client/src/translations/en.json @@ -190,7 +190,8 @@ "transfer_sites": { "heading": "Transfer existing sites", "description": "Choose existing LandPKS sites you manage to transfer to this project. Note that only those who have access to the project will be able to access the sites in the project.", - "unaffiliated": "Unaffiliated" + "unaffiliated": "Unaffiliated", + "tooltip": "Sites you transfer to this project will inherit this project’s data settings, input requirements, and team members. Learn more..." }, "form": { "name_min_length_error": "Project name must be at least {{min}} characters", diff --git a/dev-client/src/util.ts b/dev-client/src/util.ts index 2dd7ecbfe..cfce073c1 100644 --- a/dev-client/src/util.ts +++ b/dev-client/src/util.ts @@ -1,3 +1,27 @@ -export function formatName(firstName: string, lastName?: string) { +export const formatName = (firstName: string, lastName?: string) => { return [lastName, firstName].filter(Boolean).join(', '); -} +}; + +export const removeKeys = (a: any, b: any) => { + const remove = [a, b]; + let currA, currB; + while (remove.length) { + currA = remove.pop(); + currB = remove.pop(); + for (const keyA of Object.keys(currA)) { + if (!(keyA in currB)) { + delete currA[keyA]; + continue; + } + const valA = currA[keyA]; + const valB = currB[keyA]; + if (typeof valA !== typeof valB) { + delete currA[keyA]; + continue; + } + if (typeof valA === 'object' && !Array.isArray(valA) && valA !== null) { + remove.push(valA, valB); + } + } + } +}; From 8529501d4a50d6fdec76449c159a10ac110f2a32 Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Fri, 27 Oct 2023 10:25:27 -0400 Subject: [PATCH 10/13] feat: Add unaffiliated sites to site transfer --- dev-client/package-lock.json | 4 +- dev-client/package.json | 2 +- .../src/screens/SiteTransferProjectScreen.tsx | 58 ++++++++++++++----- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index 5b037ba32..3b90addbf 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -36,7 +36,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#2e4c775", + "terraso-client-shared": "github:techmatters/terraso-client-shared#2bbb0d0", "uuid": "^9.0.1", "yup": "^1.3.2" }, @@ -18700,7 +18700,7 @@ }, "node_modules/terraso-client-shared": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#2e4c775c32b93a54337abe8f048ad3417a64b6c8", + "resolved": "git+ssh://git@github.com/techmatters/terraso-client-shared.git#2bbb0d017832442b6516826736fdbd4f4d168104", "dependencies": { "@reduxjs/toolkit": "^1.9.7", "jwt-decode": "^3.1.2", diff --git a/dev-client/package.json b/dev-client/package.json index dd74caa7d..6b567261a 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -41,7 +41,7 @@ "react-native-svg": "^13.13.0", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^10.0.0", - "terraso-client-shared": "github:techmatters/terraso-client-shared#2e4c775", + "terraso-client-shared": "github:techmatters/terraso-client-shared#2bbb0d0", "uuid": "^9.0.1", "yup": "^1.3.2" }, diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index dee1704a9..e7b9f4cab 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -23,18 +23,32 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const dispatch = useDispatch(); const navigation = useNavigation(); + const UNAFFILIATED = { + projectId: Symbol('unaffiliated'), + projectName: t('projects.transfer_sites.unaffiliated'), + }; + const project = useSelector(state => state.project.projects[projectId]); - const {projects, sites} = useSelector(state => + const {projects, sites, unaffiliatedSites} = useSelector(state => selectProjectsWithTransferrableSites(state, 'manager'), ); const sitesExcludingCurrent = useMemo(() => { - return sites.filter(site => site.projectId !== projectId); + const prospectiveSites = sites.filter(site => site.projectId !== projectId); + const unaffiliated = unaffiliatedSites.map(site => ({ + ...site, + projectId: UNAFFILIATED.projectId, + projectName: UNAFFILIATED.projectName, + })); + return [...prospectiveSites, ...unaffiliated]; }, [sites, projectId]); - const projectsExcludingCurrent = useMemo(() => { + const projectsExcludingCurrent: Record< + string | symbol, + {projectId: string | symbol; projectName: string} + > = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars let {[projectId]: _, ...rest} = projects; - return rest; + return {...rest, [UNAFFILIATED.projectId]: UNAFFILIATED}; }, [projects, projectId]); const { @@ -45,13 +59,18 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const displayedProjects = useMemo(() => { const displayed: Record< - string, - {projectName: string; sites: typeof searchedSites} + string | symbol, + { + projectName: string; + projId: string | symbol; + sites: typeof searchedSites; + } > = {}; for (let site of searchedSites) { if (!(site.projectId in displayed)) { displayed[site.projectId] = { projectName: site.projectName, + projId: site.projectId, sites: [], }; } @@ -63,23 +82,32 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { projectsExcludingCurrent, )) { if (!(projId in displayed)) { - displayed[projId] = {projectName, sites: []}; + displayed[projId] = {projectName, projId, sites: []}; } } } return displayed; }, [projectsExcludingCurrent, searchedSites, query]); - const listData = useMemo( - () => - Object.entries(displayedProjects).sort((a, b) => - a[1].projectName.localeCompare(b[1].projectName), - ), - [displayedProjects], - ); + const listData = useMemo(() => { + let pool = Object.entries(displayedProjects); + if (UNAFFILIATED.projectId in displayedProjects) { + pool.push([ + String(UNAFFILIATED.projectId), + displayedProjects[UNAFFILIATED.projectId], + ]); + } + const projects = pool.sort((a, b) => { + if (a[1].projId === UNAFFILIATED.projectId) { + return -1; + } + return a[1].projectName.localeCompare(b[1].projectName); + }); + return projects; + }, [displayedProjects]); const projectRecord = useMemo(() => { - const record: Record> = {}; + const record: Record> = {}; for (const site of sitesExcludingCurrent) { if (!(site.projectId in record)) { record[site.projectId] = {}; From 7c12522ddbd4974962caac70a77bccf3e8900aa3 Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Fri, 27 Oct 2023 11:16:26 -0400 Subject: [PATCH 11/13] fix: Fix unaffiliated search, allow scrolling --- .../src/screens/SiteTransferProjectScreen.tsx | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index e7b9f4cab..f226a3271 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -23,10 +23,15 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const dispatch = useDispatch(); const navigation = useNavigation(); - const UNAFFILIATED = { - projectId: Symbol('unaffiliated'), - projectName: t('projects.transfer_sites.unaffiliated'), - }; + // Don't want this re-rendering + // Otherwise it declares multiple symbols that are *not* equal + const UNAFFILIATED = useMemo( + () => ({ + projectId: Symbol('unaffiliated'), + projectName: t('projects.transfer_sites.unaffiliated'), + }), + [], + ); const project = useSelector(state => state.project.projects[projectId]); const {projects, sites, unaffiliatedSites} = useSelector(state => @@ -91,7 +96,11 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const listData = useMemo(() => { let pool = Object.entries(displayedProjects); - if (UNAFFILIATED.projectId in displayedProjects) { + if ( + Object.getOwnPropertySymbols(displayedProjects).find( + symb => symb === UNAFFILIATED.projectId, + ) !== undefined + ) { pool.push([ String(UNAFFILIATED.projectId), displayedProjects[UNAFFILIATED.projectId], @@ -149,26 +158,30 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { return navigation.pop(); }, [projState]); + const ListHeader = ( + + + {t('projects.transfer_sites.heading', '')} + + {t('projects.transfer_sites.tooltip')} + + + {t('projects.transfer_sites.description', '')} + + + ); + return ( }> - - - {t('projects.transfer_sites.heading', '')} - - {t('projects.transfer_sites.tooltip')} - - - {t('projects.transfer_sites.description', '')} - - Date: Fri, 27 Oct 2023 11:46:45 -0400 Subject: [PATCH 12/13] fix: Allow transferring unaffiliated sites --- .../src/components/common/CheckboxGroup.tsx | 4 +-- .../src/screens/SiteTransferProjectScreen.tsx | 32 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/dev-client/src/components/common/CheckboxGroup.tsx b/dev-client/src/components/common/CheckboxGroup.tsx index ce567f4f8..b6add73cb 100644 --- a/dev-client/src/components/common/CheckboxGroup.tsx +++ b/dev-client/src/components/common/CheckboxGroup.tsx @@ -12,9 +12,9 @@ type CheckboxProps = { type Props = { checkboxes: CheckboxProps[]; groupName: string; - groupId: string; + groupId: string | symbol; onChangeValue: ( - groupId: string, + groupId: string | symbol, checkboxId: string, ) => (checked: boolean) => void; }; diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index f226a3271..93c0a3d29 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -33,6 +33,11 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { [], ); + const hasUnaffiliatedProject = (o: object) => + Object.getOwnPropertySymbols(o).find( + symb => symb === UNAFFILIATED.projectId, + ) !== undefined; + const project = useSelector(state => state.project.projects[projectId]); const {projects, sites, unaffiliatedSites} = useSelector(state => selectProjectsWithTransferrableSites(state, 'manager'), @@ -95,14 +100,11 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { }, [projectsExcludingCurrent, searchedSites, query]); const listData = useMemo(() => { - let pool = Object.entries(displayedProjects); - if ( - Object.getOwnPropertySymbols(displayedProjects).find( - symb => symb === UNAFFILIATED.projectId, - ) !== undefined - ) { + let pool: [string | symbol, (typeof displayedProjects)[string]][] = + Object.entries(displayedProjects); + if (hasUnaffiliatedProject(displayedProjects)) { pool.push([ - String(UNAFFILIATED.projectId), + UNAFFILIATED.projectId, displayedProjects[UNAFFILIATED.projectId], ]); } @@ -118,7 +120,11 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const projectRecord = useMemo(() => { const record: Record> = {}; for (const site of sitesExcludingCurrent) { - if (!(site.projectId in record)) { + if ( + !(site.projectId in record) || + (site.projectId === UNAFFILIATED.projectId && + !hasUnaffiliatedProject(record)) + ) { record[site.projectId] = {}; } record[site.projectId][site.siteId] = false; @@ -137,7 +143,7 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { }, [projectRecord, setProjState]); const onCheckboxChange = useCallback( - (groupId: string, checkboxId: string) => (checked: boolean) => { + (groupId: string | symbol, checkboxId: string) => (checked: boolean) => { setProjState(currState => { const newState = {...currState}; newState[groupId][checkboxId] = checked; @@ -148,7 +154,11 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { ); const onSubmit = useCallback(async () => { - const siteIds = Object.values(projState).flatMap(projSites => + const pool = Object.values(projState); + if (hasUnaffiliatedProject(projState)) { + pool.push(projState[UNAFFILIATED.projectId]); + } + const siteIds = pool.flatMap(projSites => Object.entries(projSites) .filter(([_, checked]) => checked) .map(([siteId, _]) => siteId), @@ -187,7 +197,7 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { item: [projId, {projectName, sites: projectSites}], }) => ( Date: Mon, 30 Oct 2023 10:09:25 -0400 Subject: [PATCH 13/13] fix(lint): Fix linting errors --- dev-client/package-lock.json | 1 - dev-client/package.json | 1 - .../src/screens/SiteTransferProjectScreen.tsx | 23 ++++++++----------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index b157fe632..a560d7e20 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -17,7 +17,6 @@ "@rnmapbox/maps": "^10.0.15", "formik": "^2.4.5", "i18next": "^23.6.0", - "lodash": "^4.17.21", "native-base": "^3.4.28", "react": "18.2.0", "react-i18next": "^13.3.1", diff --git a/dev-client/package.json b/dev-client/package.json index 60c516d35..308cbf01f 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -22,7 +22,6 @@ "@rnmapbox/maps": "^10.0.15", "formik": "^2.4.5", "i18next": "^23.6.0", - "lodash": "^4.17.21", "native-base": "^3.4.28", "react": "18.2.0", "react-i18next": "^13.3.1", diff --git a/dev-client/src/screens/SiteTransferProjectScreen.tsx b/dev-client/src/screens/SiteTransferProjectScreen.tsx index 93c0a3d29..e0a8fe106 100644 --- a/dev-client/src/screens/SiteTransferProjectScreen.tsx +++ b/dev-client/src/screens/SiteTransferProjectScreen.tsx @@ -18,20 +18,17 @@ import {FormTooltip} from 'terraso-mobile-client/components/common/Form'; type Props = {projectId: string}; +const UNAFFILIATED = { + projectId: Symbol('unaffiliated'), + projectName: '', +}; + export const SiteTransferProjectScreen = ({projectId}: Props) => { const {t} = useTranslation(); const dispatch = useDispatch(); const navigation = useNavigation(); - // Don't want this re-rendering - // Otherwise it declares multiple symbols that are *not* equal - const UNAFFILIATED = useMemo( - () => ({ - projectId: Symbol('unaffiliated'), - projectName: t('projects.transfer_sites.unaffiliated'), - }), - [], - ); + UNAFFILIATED.projectName = t('projects.transfer_sites.unaffiliated'); const hasUnaffiliatedProject = (o: object) => Object.getOwnPropertySymbols(o).find( @@ -50,7 +47,7 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { projectName: UNAFFILIATED.projectName, })); return [...prospectiveSites, ...unaffiliated]; - }, [sites, projectId]); + }, [sites, projectId, unaffiliatedSites]); const projectsExcludingCurrent: Record< string | symbol, @@ -108,13 +105,13 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { displayedProjects[UNAFFILIATED.projectId], ]); } - const projects = pool.sort((a, b) => { + const sortedProjects = pool.sort((a, b) => { if (a[1].projId === UNAFFILIATED.projectId) { return -1; } return a[1].projectName.localeCompare(b[1].projectName); }); - return projects; + return sortedProjects; }, [displayedProjects]); const projectRecord = useMemo(() => { @@ -166,7 +163,7 @@ export const SiteTransferProjectScreen = ({projectId}: Props) => { const payload = {projectId, siteIds}; await dispatch(transferSites(payload)); return navigation.pop(); - }, [projState]); + }, [projState, dispatch, navigation, projectId]); const ListHeader = (