Skip to content

Commit

Permalink
feat: Implement new "Add team member" flow (#1581)
Browse files Browse the repository at this point in the history
Commits:
* feat: 'Add Team Member' screen opens with new design and does all the validation at the same time, whenever the field entry changes
* feat: disable and positon Next button
* feat: Do backend validation on submit, and only do frontend validation on change
* fix: minor string update
* refactor: refactor onNext and backend validation
* feat: Implement user role selection screen
* feat: on 1st screen, set form to email keyboard and autocomplete + clean up comments
* refactor: Move AddTeamMemberForm to new file
* fix: 2nd screen app bar button is back arrow, and Cancel button pops navigation
* refactor: use UserFields type + resolve comments
* fix: Implement misc PR feedback
  - try/catch for backend call
  - don't autocapitalize email
  - navigate to appropriate screen tab instead of two pop() calls
* fix: need to dispatch to correctly specify the tab to navigate to
* fix: Update strings per design input
  • Loading branch information
knipec authored Jun 20, 2024
1 parent 19e5a5b commit 9ed5f96
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 172 deletions.
6 changes: 3 additions & 3 deletions dev-client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dev-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"react-native-screens": "^3.31.1",
"react-native-svg": "^15.3.0",
"react-native-tab-view": "^3.5.2",
"terraso-client-shared": "github:techmatters/terraso-client-shared#955095c8480485c00d3a8913a6adaa5a9b6847d1",
"terraso-client-shared": "github:techmatters/terraso-client-shared#6cf29e10f3b3914ae327ffc3c91608736bae2a6c",
"uuid": "^9.0.1",
"yup": "^1.4.0"
},
Expand Down
2 changes: 2 additions & 0 deletions dev-client/src/navigation/screenDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from 'terraso-mobile-client/navigation/types';
import {generateScreens} from 'terraso-mobile-client/navigation/utils/utils';
import {AddSiteNoteScreen} from 'terraso-mobile-client/screens/AddSiteNoteScreen';
import {AddUserToProjectRoleScreen} from 'terraso-mobile-client/screens/AddUserToProjectScreen/AddUserToProjectRoleScreen';
import {AddUserToProjectScreen} from 'terraso-mobile-client/screens/AddUserToProjectScreen/AddUserToProjectScreen';
import {BottomTabsScreen} from 'terraso-mobile-client/screens/BottomTabsScreen';
import {ColorAnalysisScreen} from 'terraso-mobile-client/screens/ColorAnalysisScreen/ColorAnalysisScreen';
Expand Down Expand Up @@ -74,6 +75,7 @@ export const screenDefinitions = {
SITE_SETTINGS: SiteSettingsScreen,
SITE_TEAM_SETTINGS: SiteTeamSettingsScreen,
ADD_USER_PROJECT: AddUserToProjectScreen,
ADD_USER_PROJECT_ROLE: AddUserToProjectRoleScreen,
MANAGE_TEAM_MEMBER: ManageTeamMemberScreen,
SLOPE_STEEPNESS: SlopeSteepnessScreen,
SLOPE_SHAPE: SlopeShapeScreen,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright © 2024 Technology Matters
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import {useCallback, useState} from 'react';
import {useTranslation} from 'react-i18next';

import {TabActions} from '@react-navigation/native';
import {Button} from 'native-base';

import {
addUserToProject,
ProjectRole,
} from 'terraso-client-shared/project/projectSlice';

import {ScreenContentSection} from 'terraso-mobile-client/components/content/ScreenContentSection';
import {
Box,
Column,
Row,
} from 'terraso-mobile-client/components/NativeBaseAdapters';
import {AppBar} from 'terraso-mobile-client/navigation/components/AppBar';
import {TabRoutes} from 'terraso-mobile-client/navigation/constants';
import {useNavigation} from 'terraso-mobile-client/navigation/hooks/useNavigation';
import {MinimalUserDisplay} from 'terraso-mobile-client/screens/AddUserToProjectScreen/components/MinimalUserDisplay';
import {ProjectRoleRadioBlock} from 'terraso-mobile-client/screens/AddUserToProjectScreen/components/ProjectRoleRadioBlock';
import {UserFields} from 'terraso-mobile-client/screens/AddUserToProjectScreen/components/UserDisplay';
import {ScreenScaffold} from 'terraso-mobile-client/screens/ScreenScaffold';
import {useDispatch, useSelector} from 'terraso-mobile-client/store';

type Props = {
projectId: string;
user: UserFields;
};

export const AddUserToProjectRoleScreen = ({projectId, user}: Props) => {
const {t} = useTranslation();
const dispatch = useDispatch();
const navigation = useNavigation();

const project = useSelector(state => state.project.projects[projectId]);

const [selectedRole, setSelectedRole] = useState<ProjectRole>('VIEWER');

const onCancel = useCallback(() => {
navigation.pop();
}, [navigation]);

const addUser = useCallback(async () => {
try {
dispatch(
addUserToProject({userId: user.id, role: selectedRole, projectId}),
);
} catch (e) {
console.error(e);
}
navigation.navigate('PROJECT_VIEW', {projectId: projectId});
navigation.dispatch(TabActions.jumpTo(TabRoutes.TEAM));
}, [dispatch, projectId, user, selectedRole, navigation]);

return (
<ScreenScaffold AppBar={<AppBar title={project?.name} />}>
<ScreenContentSection title={t('projects.add_user.heading')}>
<Column>
<Box ml="md" my="lg">
<MinimalUserDisplay user={user} />
</Box>

<ProjectRoleRadioBlock
onChange={setSelectedRole}
selectedRole={selectedRole}
/>

<Row
flex={0}
justifyContent="flex-end"
alignItems="center"
space="12px"
pt="md">
<Button
onPress={onCancel}
size="lg"
variant="outline"
borderColor="action.active"
_text={{textTransform: 'uppercase', color: 'action.active'}}>
{t('general.cancel')}
</Button>
{/* FYI: The 1px border is to visually match the size of the outline
variant, which appears to be 1px bigger than the solid variant due
to its border. */}
<Button
borderWidth="1px"
borderColor="primary.main"
onPress={addUser}
size="lg"
variant="solid"
_text={{textTransform: 'uppercase'}}>
{t('general.add')}
</Button>
</Row>
</Column>
</ScreenContentSection>
</ScreenScaffold>
);
};
141 changes: 12 additions & 129 deletions dev-client/src/screens/AddUserToProjectScreen/AddUserToProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,155 +15,38 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import {useCallback, useMemo, useState} from 'react';
import {useTranslation} from 'react-i18next';

import {Button} from 'native-base';

import {checkUserInProject} from 'terraso-client-shared/account/accountService';
import {
addUserToProject,
ProjectRole,
} from 'terraso-client-shared/project/projectSlice';

import {
Box,
Row,
Text,
} from 'terraso-mobile-client/components/NativeBaseAdapters';
import {ScreenContentSection} from 'terraso-mobile-client/components/content/ScreenContentSection';
import {Box, Text} from 'terraso-mobile-client/components/NativeBaseAdapters';
import {AppBar} from 'terraso-mobile-client/navigation/components/AppBar';
import {useNavigation} from 'terraso-mobile-client/navigation/hooks/useNavigation';
import {FreeformTextInput} from 'terraso-mobile-client/screens/AddUserToProjectScreen/components/FreeformTextInput';
import MembershipControlList, {
UserWithRole,
} from 'terraso-mobile-client/screens/AddUserToProjectScreen/components/MembershipControlList';
import {useKeyboardOpen} from 'terraso-mobile-client/screens/AddUserToProjectScreen/hooks/useKeyboardOpen';
import {AddTeamMemberForm} from 'terraso-mobile-client/screens/AddUserToProjectScreen/components/AddTeamMemberForm';
import {ScreenScaffold} from 'terraso-mobile-client/screens/ScreenScaffold';
import {useDispatch, useSelector} from 'terraso-mobile-client/store';
import {useSelector} from 'terraso-mobile-client/store';

type Props = {
projectId: string;
};

export const AddUserToProjectScreen = ({projectId}: Props) => {
const {t} = useTranslation();
const [userRecord, setUserRecord] = useState<Record<string, UserWithRole>>(
{},
);
const keyboardOpen = useKeyboardOpen();

const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const navigation = useNavigation();
const dispatch = useDispatch();

const projectName = useSelector(
state => state.project.projects[projectId]?.name,
);

const userList = useMemo(() => Object.values(userRecord), [userRecord]);
const disableSubmit = useMemo(
() => isSubmitting || Object.keys(userRecord).length === 0,
[userRecord, isSubmitting],
);

const validationFunc = async (email: string) => {
if (email === '') {
// TODO: current bug means empty string for email is considered existing :(
// Easier just to explicitly reject it here
return t('projects.add_user.empty_email');
}
const userExists = await checkUserInProject(projectId, email);
if ('type' in userExists) {
switch (userExists.type) {
case 'NoUser':
return t('projects.add_user.user_does_not_exist', {email: email});
case 'InProject':
return t('projects.add_user.user_in_project', {email: email});
}
}
if (userExists.id in userRecord) {
return t('projects.add_user.already_added', {email: email});
}

setUserRecord(users => {
return {
...users,
[userExists.id]: {user: userExists, role: 'VIEWER'},
};
});
return null;
};

const updateUserRole = useCallback((role: ProjectRole, userId: string) => {
setUserRecord(users => {
const newUsers = {...users};
newUsers[userId].role = role;
return newUsers;
});
}, []);

const removeUser = useCallback((userId: string) => {
setUserRecord(users => {
let newUsers = {...users};
delete newUsers[userId];
return newUsers;
});
}, []);

const submitUsers = async () => {
setIsSubmitting(true);
for (const {
user: {id: userId},
role,
} of Object.values(userRecord)) {
try {
dispatch(addUserToProject({userId, role, projectId}));
} catch (e) {
console.error(e);
}
}
navigation.pop();
setIsSubmitting(false);
};
// FYI: There was previously a mechanism to enter emails individually, but set roles at the same time.
// This was replaced, but we could refer back to `userRecord` in previous versions if we ever end up
// wanting to add multiple users at the same time.

return (
<ScreenScaffold AppBar={<AppBar title={projectName} />}>
<Box mx="5%" mb="15px" mt="22px">
<Text variant="body1" fontWeight="bold">
{t('projects.add_user.heading')}
</Text>
<ScreenContentSection title={t('projects.add_user.heading')}>
<Text variant="body1">{t('projects.add_user.help_text')}</Text>
</Box>
<Box mx="5%" mb="15px">
<FreeformTextInput
validationFunc={validationFunc}
placeholder={t('general.email_placeholder')}
inputProps={{
autoComplete: 'email',
autoCapitalize: 'none',
keyboardType: 'email-address',
label: t('general.email_label'),
}}
/>
</Box>
<MembershipControlList
users={userList}
updateUserRole={updateUserRole}
removeUser={removeUser}
/>
<Row
flexDirection="row-reverse"
my="20px"
ml="20px"
display={keyboardOpen ? 'none' : undefined}>
<Button
onPress={submitUsers}
isDisabled={disableSubmit}
isLoading={isSubmitting}
w="100px">
{t('general.save_fab')}
</Button>
</Row>
<Box mt="md">
<AddTeamMemberForm projectId={projectId} />
</Box>
</ScreenContentSection>
</ScreenScaffold>
);
};
Loading

0 comments on commit 9ed5f96

Please sign in to comment.