MM-42835_Invite People - add email+user invites

This commit is contained in:
Julian Mondragon
2023-01-19 17:18:51 -05:00
parent a8da27d5e9
commit 29a66bbc19
7 changed files with 57 additions and 96 deletions

View File

@@ -508,19 +508,13 @@ export async function getTeamMembersByIds(serverUrl: string, teamId: string, use
if (!fetchOnly) { if (!fetchOnly) {
setTeamLoading(serverUrl, true); setTeamLoading(serverUrl, true);
const teamMemberships: TeamMembership[] = []; const roles = [];
const roles: Record<string, boolean> = {};
for (const member of members) { for (const {roles: memberRoles} of members) {
teamMemberships.push(member); roles.push(...memberRoles.split(' '));
member.roles.split(' ').forEach((role) => {
if (!roles[role]) {
roles[role] = true;
}
});
} }
fetchRolesIfNeeded(serverUrl, Object.getOwnPropertyNames(roles)); fetchRolesIfNeeded(serverUrl, Array.from(new Set(roles)));
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
@@ -529,7 +523,7 @@ export async function getTeamMembersByIds(serverUrl: string, teamId: string, use
const models: Model[] = (await Promise.all([ const models: Model[] = (await Promise.all([
operator.handleTeam({teams: [team], prepareRecordsOnly: true}), operator.handleTeam({teams: [team], prepareRecordsOnly: true}),
operator.handleTeamMemberships({teamMemberships, prepareRecordsOnly: true}), operator.handleTeamMemberships({teamMemberships: members, prepareRecordsOnly: true}),
])).flat(); ])).flat();
await operator.batchRecords(models); await operator.batchRecords(models);

View File

@@ -115,6 +115,7 @@ export default function Invite({
const modalPosition = useModalPosition(mainView); const modalPosition = useModalPosition(mainView);
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null); const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
const retryTimeoutId = useRef<NodeJS.Timeout | null>(null);
const [term, setTerm] = useState(''); const [term, setTerm] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]); const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
@@ -137,7 +138,7 @@ export default function Invite({
return; return;
} }
const {data} = await searchProfiles(serverUrl, searchTerm.toLowerCase(), {allow_inactive: true}); const {data} = await searchProfiles(serverUrl, searchTerm.toLowerCase());
const results: SearchResult[] = data ?? []; const results: SearchResult[] = data ?? [];
if (!results.length && isEmail(searchTerm.trim())) { if (!results.length && isEmail(searchTerm.trim())) {
@@ -193,7 +194,7 @@ export default function Invite({
setSendError(''); setSendError('');
setStage(Stage.LOADING); setStage(Stage.LOADING);
setTimeout(() => { retryTimeoutId.current = setTimeout(() => {
handleSend(); handleSend();
}, TIMEOUT_MILLISECONDS); }, TIMEOUT_MILLISECONDS);
}; };
@@ -243,9 +244,9 @@ export default function Invite({
for (const userId of userIds) { for (const userId of userIds) {
if (isGuest((selectedIds[userId] as UserProfile).roles)) { if (isGuest((selectedIds[userId] as UserProfile).roles)) {
notSent.push({userId, reason: formatMessage({id: 'invite.members.user-is-guest', defaultMessage: 'Contact your admin to make this guest a full member'})}); notSent.push({userId, reason: formatMessage({id: 'invite.members.user_is_guest', defaultMessage: 'Contact your admin to make this guest a full member'})});
} else if (currentMemberIds[userId]) { } else if (currentMemberIds[userId]) {
notSent.push({userId, reason: formatMessage({id: 'invite.members.already-member', defaultMessage: 'This person is already a team member'})}); notSent.push({userId, reason: formatMessage({id: 'invite.members.already_member', defaultMessage: 'This person is already a team member'})});
} else { } else {
usersToAdd.push(userId); usersToAdd.push(userId);
} }
@@ -338,6 +339,18 @@ export default function Invite({
}); });
}, [componentId, locale, theme, stage]); }, [componentId, locale, theme, stage]);
useEffect(() => {
return () => {
if (searchTimeoutId.current) {
clearTimeout(searchTimeoutId.current);
}
if (retryTimeoutId.current) {
clearTimeout(retryTimeoutId.current);
}
};
}, []);
const handleRemoveItem = useCallback((id: string) => { const handleRemoveItem = useCallback((id: string) => {
const newSelectedIds = Object.assign({}, selectedIds); const newSelectedIds = Object.assign({}, selectedIds);

View File

@@ -23,7 +23,6 @@ import {useTheme} from '@context/theme';
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete'; import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
import {useIsTablet, useKeyboardHeight} from '@hooks/device'; import {useIsTablet, useKeyboardHeight} from '@hooks/device';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
import {SearchResult} from './invite'; import {SearchResult} from './invite';
import SelectedEmail from './selected_email'; import SelectedEmail from './selected_email';
@@ -56,54 +55,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
display: 'flex', display: 'flex',
flex: 1, flex: 1,
}, },
teamContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: '100%',
paddingVertical: 16,
paddingHorizontal: 20,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
},
iconContainer: {
width: 40,
height: 40,
},
textContainer: {
display: 'flex',
flexDirection: 'column',
},
teamText: {
color: theme.centerChannelColor,
marginLeft: 12,
...typography('Body', 200, 'SemiBold'),
},
serverText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
marginLeft: 12,
...typography('Body', 75, 'Regular'),
},
shareLink: {
display: 'flex',
marginLeft: 'auto',
},
shareLinkButton: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: 40,
paddingHorizontal: 20,
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
borderRadius: 4,
},
shareLinkText: {
color: theme.buttonBg,
...typography('Body', 100, 'SemiBold'),
paddingLeft: 7,
},
shareLinkIcon: {
color: theme.buttonBg,
},
searchList: { searchList: {
left: 20, left: 20,
right: 20, right: 20,

View File

@@ -262,18 +262,22 @@ export default function Summary({
</Text> </Text>
) : ( ) : (
<> <>
<SummaryReport {notSent.length > 0 && (
type={SummaryReportType.NOT_SENT} <SummaryReport
invites={notSent} type={SummaryReportType.NOT_SENT}
selectedIds={selectedIds} invites={notSent}
testID='invite.summary_report' selectedIds={selectedIds}
/> testID='invite.summary_report'
<SummaryReport />
type={SummaryReportType.SENT} )}
invites={sent} {sent.length > 0 && (
selectedIds={selectedIds} <SummaryReport
testID='invite.summary_report' type={SummaryReportType.SENT}
/> invites={sent}
selectedIds={selectedIds}
testID='invite.summary_report'
/>
)}
</> </>
)} )}
</View> </View>

View File

@@ -14,9 +14,12 @@ import {typography} from '@utils/typography';
import {SearchResult, InviteResult} from './invite'; import {SearchResult, InviteResult} from './invite';
import TextItem, {TextItemType} from './text_item'; import TextItem, {TextItemType} from './text_item';
const COLOR_SUCCESS = '#3db887';
const COLOR_ERROR = '#d24b4e';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return { return {
summaryInvitationsContainer: { container: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
borderWidth: 1, borderWidth: 1,
@@ -25,29 +28,29 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
marginBottom: 16, marginBottom: 16,
paddingVertical: 8, paddingVertical: 8,
}, },
summaryInvitationsTitle: { title: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 12, paddingVertical: 12,
}, },
summaryInvitationsTitleText: { titleText: {
marginLeft: 12, marginLeft: 12,
...typography('Heading', 300, 'SemiBold'), ...typography('Heading', 300, 'SemiBold'),
color: theme.centerChannelColor, color: theme.centerChannelColor,
}, },
summaryInvitationsItem: { item: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
paddingVertical: 12, paddingVertical: 12,
}, },
summaryInvitationsUser: { user: {
paddingTop: 0, paddingTop: 0,
paddingBottom: 0, paddingBottom: 0,
height: 'auto', height: 'auto',
}, },
summaryInvitationsReason: { reason: {
paddingLeft: 56, paddingLeft: 56,
paddingRight: 20, paddingRight: 20,
...typography('Body', 75, 'Regular'), ...typography('Body', 75, 'Regular'),
@@ -80,10 +83,6 @@ export default function SummaryReport({
const count = invites.length; const count = invites.length;
if (!count) {
return null;
}
const sent = type === SummaryReportType.SENT; const sent = type === SummaryReportType.SENT;
const message = sent ? ( const message = sent ? (
formatMessage( formatMessage(
@@ -105,16 +104,16 @@ export default function SummaryReport({
return ( return (
<View <View
style={styles.summaryInvitationsContainer} style={styles.container}
testID={`${testID}.${type}`} testID={`${testID}.${type}`}
> >
<View style={styles.summaryInvitationsTitle}> <View style={styles.title}>
<CompassIcon <CompassIcon
name={sent ? 'check-circle' : 'close-circle'} name={sent ? 'check-circle' : 'close-circle'}
size={24} size={24}
style={{color: sent ? '#3db887' : '#d24b4e'}} style={{color: sent ? COLOR_SUCCESS : COLOR_ERROR}}
/> />
<Text style={styles.summaryInvitationsTitleText}> <Text style={styles.titleText}>
{message} {message}
</Text> </Text>
</View> </View>
@@ -124,7 +123,7 @@ export default function SummaryReport({
return ( return (
<View <View
key={userId} key={userId}
style={styles.summaryInvitationsItem} style={styles.item}
> >
{typeof item === 'string' ? ( {typeof item === 'string' ? (
<TextItem <TextItem
@@ -135,11 +134,11 @@ export default function SummaryReport({
) : ( ) : (
<UserItem <UserItem
user={item} user={item}
containerStyle={styles.summaryInvitationsUser} containerStyle={styles.user}
testID={`${testID}.user_item`} testID={`${testID}.user_item`}
/> />
)} )}
<Text style={styles.summaryInvitationsReason}> <Text style={styles.reason}>
{reason} {reason}
</Text> </Text>
</View> </View>

View File

@@ -703,7 +703,7 @@ export function setButtons(componentId: string, buttons: NavButtons = {leftButto
mergeNavigationOptions(componentId, options); mergeNavigationOptions(componentId, options);
} }
export function showOverlay(name: string, passProps = {}, options = {}) { export function showOverlay(name: string, passProps = {}, options: Options = {}) {
if (!isScreenRegistered(name)) { if (!isScreenRegistered(name)) {
return; return;
} }

View File

@@ -327,8 +327,8 @@
"intro.welcome.public": "Add some more team members to the channel or start a conversation below.", "intro.welcome.public": "Add some more team members to the channel or start a conversation below.",
"invite_people_to_team.message": "Heres a link to collaborate and communicate with us on Mattermost.", "invite_people_to_team.message": "Heres a link to collaborate and communicate with us on Mattermost.",
"invite_people_to_team.title": "Join the {team} team", "invite_people_to_team.title": "Join the {team} team",
"invite.members.already-member": "This person is already a team member", "invite.members.already_member": "This person is already a team member",
"invite.members.user-is-guest": "Contact your admin to make this guest a full member", "invite.members.user_is_guest": "Contact your admin to make this guest a full member",
"invite.search.email_invite": "invite", "invite.search.email_invite": "invite",
"invite.search.no_results": "No one found matching", "invite.search.no_results": "No one found matching",
"invite.searchPlaceholder": "Type a name or email address…", "invite.searchPlaceholder": "Type a name or email address…",