forked from Ivasoft/mattermost-mobile
MM-39720_Invite People - phase 1
This commit is contained in:
@@ -21,6 +21,7 @@ type SlideUpPanelProps = {
|
||||
textStyles?: TextStyle;
|
||||
testID?: string;
|
||||
text: string;
|
||||
topDivider?: boolean;
|
||||
}
|
||||
|
||||
export const ITEM_HEIGHT = 48;
|
||||
@@ -61,10 +62,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
divider: {
|
||||
borderTopWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text, textStyles, rightIcon = false}: SlideUpPanelProps) => {
|
||||
const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text, textStyles, rightIcon = false, topDivider}: SlideUpPanelProps) => {
|
||||
const theme = useTheme();
|
||||
const handleOnPress = useCallback(preventDoubleTap(onPress, 500), []);
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -99,10 +105,12 @@ const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text
|
||||
}
|
||||
}
|
||||
|
||||
const divider = topDivider ? style.divider : {};
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={handleOnPress}
|
||||
style={style.container}
|
||||
style={{...style.container, ...divider}}
|
||||
testID={testID}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
>
|
||||
|
||||
@@ -11,10 +11,22 @@ import {MM_TABLES} from '@constants/database';
|
||||
const {SERVER: {
|
||||
GROUP,
|
||||
MY_CHANNEL,
|
||||
TEAM,
|
||||
THREAD,
|
||||
}} = MM_TABLES;
|
||||
|
||||
export default schemaMigrations({migrations: [
|
||||
{
|
||||
toVersion: 4,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: TEAM,
|
||||
columns: [
|
||||
{name: 'invite_id', type: 'string'},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
toVersion: 3,
|
||||
steps: [
|
||||
|
||||
@@ -87,6 +87,9 @@ export default class TeamModel extends Model implements TeamModelInterface {
|
||||
/** allowed_domains : List of domains that can join this team */
|
||||
@field('allowed_domains') allowedDomains!: string;
|
||||
|
||||
/** invite_id : The token id to use in invites to the team */
|
||||
@field('invite_id') inviteId!: string;
|
||||
|
||||
/** categories : All the categories associated with this team */
|
||||
@children(CATEGORY) categories!: Query<CategoryModel>;
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export const transformTeamRecord = ({action, database, value}: TransformerArgs):
|
||||
team.allowedDomains = raw.allowed_domains;
|
||||
team.isGroupConstrained = Boolean(raw.group_constrained);
|
||||
team.lastTeamIconUpdatedAt = raw.last_team_icon_update;
|
||||
team.inviteId = raw.invite_id;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from './table_schemas';
|
||||
|
||||
export const serverSchema: AppSchema = appSchema({
|
||||
version: 3,
|
||||
version: 4,
|
||||
tables: [
|
||||
CategorySchema,
|
||||
CategoryChannelSchema,
|
||||
|
||||
@@ -19,5 +19,6 @@ export default tableSchema({
|
||||
{name: 'name', type: 'string'},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'invite_id', type: 'string'},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ const {
|
||||
describe('*** Test schema for SERVER database ***', () => {
|
||||
it('=> The SERVER SCHEMA should strictly match', () => {
|
||||
expect(serverSchema).toEqual({
|
||||
version: 3,
|
||||
version: 4,
|
||||
tables: {
|
||||
[CATEGORY]: {
|
||||
name: CATEGORY,
|
||||
@@ -463,6 +463,7 @@ describe('*** Test schema for SERVER database ***', () => {
|
||||
name: {name: 'name', type: 'string'},
|
||||
type: {name: 'type', type: 'string'},
|
||||
update_at: {name: 'update_at', type: 'number'},
|
||||
invite_id: {name: 'invite_id', type: 'string'},
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'allowed_domains', type: 'string'},
|
||||
@@ -474,6 +475,7 @@ describe('*** Test schema for SERVER database ***', () => {
|
||||
{name: 'name', type: 'string'},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'invite_id', type: 'string'},
|
||||
],
|
||||
},
|
||||
[TEAM_CHANNEL_HISTORY]: {
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('components/channel_list/header', () => {
|
||||
pushProxyStatus={PUSH_PROXY_STATUS_VERIFIED}
|
||||
canCreateChannels={true}
|
||||
canJoinChannels={true}
|
||||
canInvitePeople={true}
|
||||
displayName={'Test!'}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -28,7 +28,9 @@ import PlusMenu from './plus_menu';
|
||||
type Props = {
|
||||
canCreateChannels: boolean;
|
||||
canJoinChannels: boolean;
|
||||
displayName: string;
|
||||
canInvitePeople: boolean;
|
||||
displayName?: string;
|
||||
inviteId?: string;
|
||||
iconPad?: boolean;
|
||||
onHeaderPress?: () => void;
|
||||
pushProxyStatus: string;
|
||||
@@ -92,7 +94,9 @@ const hitSlop: Insets = {top: 10, bottom: 30, left: 20, right: 20};
|
||||
const ChannelListHeader = ({
|
||||
canCreateChannels,
|
||||
canJoinChannels,
|
||||
canInvitePeople,
|
||||
displayName,
|
||||
inviteId,
|
||||
iconPad,
|
||||
onHeaderPress,
|
||||
pushProxyStatus,
|
||||
@@ -118,6 +122,9 @@ const ChannelListHeader = ({
|
||||
<PlusMenu
|
||||
canCreateChannels={canCreateChannels}
|
||||
canJoinChannels={canJoinChannels}
|
||||
canInvitePeople={canInvitePeople}
|
||||
displayName={displayName}
|
||||
inviteId={inviteId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -132,6 +139,10 @@ const ChannelListHeader = ({
|
||||
items += 1;
|
||||
}
|
||||
|
||||
if (canInvitePeople) {
|
||||
items += 1;
|
||||
}
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId,
|
||||
renderContent,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Permissions} from '@constants';
|
||||
import {observePermissionForTeam} from '@queries/servers/role';
|
||||
import {observePushVerificationStatus} from '@queries/servers/system';
|
||||
import {observeConfig, observePushVerificationStatus} from '@queries/servers/system';
|
||||
import {observeCurrentTeam} from '@queries/servers/team';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
@@ -21,6 +21,12 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
const config = observeConfig(database);
|
||||
|
||||
const enableOpenServer = config.pipe(
|
||||
switchMap((t) => of$(t?.EnableOpenServer === 'true')),
|
||||
);
|
||||
|
||||
const canJoinChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)),
|
||||
);
|
||||
@@ -37,12 +43,22 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
switchMap(([open, priv]) => of$(open || priv)),
|
||||
);
|
||||
|
||||
const canAddUserToTeam = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.ADD_USER_TO_TEAM, false)),
|
||||
);
|
||||
|
||||
return {
|
||||
canCreateChannels,
|
||||
canJoinChannels,
|
||||
canInvitePeople: combineLatest([enableOpenServer, canAddUserToTeam]).pipe(
|
||||
switchMap(([openServer, addUser]) => of$(openServer && addUser)),
|
||||
),
|
||||
displayName: team.pipe(
|
||||
switchMap((t) => of$(t?.displayName)),
|
||||
),
|
||||
inviteId: team.pipe(
|
||||
switchMap((t) => of$(t?.inviteId)),
|
||||
),
|
||||
pushProxyStatus: observePushVerificationStatus(database),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
import Share from 'react-native-share';
|
||||
import {ShareOptions} from 'react-native-share/lib/typescript/types';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
@@ -14,11 +18,15 @@ import PlusMenuItem from './item';
|
||||
type Props = {
|
||||
canCreateChannels: boolean;
|
||||
canJoinChannels: boolean;
|
||||
canInvitePeople: boolean;
|
||||
displayName?: string;
|
||||
inviteId?: string;
|
||||
}
|
||||
|
||||
const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
|
||||
const PlusMenuList = ({canCreateChannels, canJoinChannels, canInvitePeople, displayName, inviteId}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const browseChannels = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
@@ -48,6 +56,59 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
|
||||
});
|
||||
}, [intl, theme]);
|
||||
|
||||
const invitePeopleToTeam = async () => {
|
||||
await dismissBottomSheet();
|
||||
|
||||
const url = `${serverUrl}/signup_user_complete/?id=${inviteId}`;
|
||||
const title = intl.formatMessage({id: 'invite_people_to_team.title', defaultMessage: 'Join the {team} team'}, {team: displayName});
|
||||
const message = intl.formatMessage({id: 'invite_people_to_team.message', defaultMessage: 'Here´s a link to collaborate and communicate with us on Mattermost.'});
|
||||
const icon = 'data:<data_type>/<file_extension>;base64,<base64_data>';
|
||||
|
||||
const options: ShareOptions = Platform.select({
|
||||
ios: {
|
||||
activityItemSources: [
|
||||
{
|
||||
placeholderItem: {
|
||||
type: 'url',
|
||||
content: url,
|
||||
},
|
||||
item: {
|
||||
default: {
|
||||
type: 'text',
|
||||
content: `${message} ${url}`,
|
||||
},
|
||||
copyToPasteBoard: {
|
||||
type: 'url',
|
||||
content: url,
|
||||
},
|
||||
},
|
||||
subject: {
|
||||
default: title,
|
||||
},
|
||||
linkMetadata: {
|
||||
originalUrl: url,
|
||||
url,
|
||||
title,
|
||||
icon,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
default: {
|
||||
title,
|
||||
subject: title,
|
||||
url,
|
||||
showAppsToView: true,
|
||||
},
|
||||
});
|
||||
|
||||
Share.open(
|
||||
options,
|
||||
).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{canJoinChannels &&
|
||||
@@ -66,6 +127,12 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
|
||||
pickerAction='openDirectMessage'
|
||||
onPress={openDirectMessage}
|
||||
/>
|
||||
{canInvitePeople &&
|
||||
<PlusMenuItem
|
||||
pickerAction='invitePeopleToTeam'
|
||||
onPress={invitePeopleToTeam}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {useIntl} from 'react-intl';
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
|
||||
type PlusMenuItemProps = {
|
||||
pickerAction: 'browseChannels' | 'createNewChannel' | 'openDirectMessage';
|
||||
pickerAction: 'browseChannels' | 'createNewChannel' | 'openDirectMessage' | 'invitePeopleToTeam';
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
@@ -32,14 +32,20 @@ const PlusMenuItem = ({pickerAction, onPress}: PlusMenuItemProps) => {
|
||||
text: intl.formatMessage({id: 'plus_menu.open_direct_message.title', defaultMessage: 'Open a Direct Message'}),
|
||||
testID: 'plus_menu_item.open_direct_message',
|
||||
},
|
||||
invitePeopleToTeam: {
|
||||
icon: 'account-plus-outline',
|
||||
text: intl.formatMessage({id: 'plus_menu.invite_people_to_team.title', defaultMessage: 'Invite people to the team'}),
|
||||
testID: 'plus_menu_item.invite_people_to_team',
|
||||
topDivider: true,
|
||||
},
|
||||
};
|
||||
|
||||
const itemType = menuItems[pickerAction];
|
||||
|
||||
return (
|
||||
<SlideUpPanelItem
|
||||
text={itemType.text}
|
||||
icon={itemType.icon}
|
||||
{...itemType}
|
||||
onPress={onPress}
|
||||
testID={itemType.testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -315,6 +315,8 @@
|
||||
"intro.welcome": "Welcome to {displayName} channel.",
|
||||
"intro.welcome.private": "Only invited members can see messages posted in this private channel.",
|
||||
"intro.welcome.public": "Add some more team members to the channel or start a conversation below.",
|
||||
"invite_people_to_team.message": "Here´s a link to collaborate and communicate with us on Mattermost.",
|
||||
"invite_people_to_team.title": "Join the {team} team",
|
||||
"last_users_message.added_to_channel.type": "were **added to the channel** by {actor}.",
|
||||
"last_users_message.added_to_team.type": "were **added to the team** by {actor}.",
|
||||
"last_users_message.first": "{firstUser} and ",
|
||||
@@ -684,6 +686,7 @@
|
||||
"pinned_messages.empty.title": "No pinned messages yet",
|
||||
"plus_menu.browse_channels.title": "Browse Channels",
|
||||
"plus_menu.create_new_channel.title": "Create New Channel",
|
||||
"plus_menu.invite_people_to_team.title": "Invite people to the team",
|
||||
"plus_menu.open_direct_message.title": "Open a Direct Message",
|
||||
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel.",
|
||||
"post_body.check_for_out_of_channel_mentions.link.and": " and ",
|
||||
|
||||
@@ -6,11 +6,13 @@ class PlusMenu {
|
||||
browseChannelsItem: 'plus_menu_item.browse_channels',
|
||||
createNewChannelItem: 'plus_menu_item.create_new_channel',
|
||||
openDirectMessageItem: 'plus_menu_item.open_direct_message',
|
||||
invitePeopleToTeamItem: 'plus_menu_item.invite_people_to_team',
|
||||
};
|
||||
|
||||
browseChannelsItem = element(by.id(this.testID.browseChannelsItem));
|
||||
createNewChannelItem = element(by.id(this.testID.createNewChannelItem));
|
||||
openDirectMessageItem = element(by.id(this.testID.openDirectMessageItem));
|
||||
invitePeopleToTeamItem = element(by.id(this.testID.invitePeopleToTeamItem));
|
||||
}
|
||||
|
||||
const plusMenu = new PlusMenu();
|
||||
|
||||
@@ -39,6 +39,7 @@ class ChannelListScreen {
|
||||
browseChannelsItem = PlusMenu.browseChannelsItem;
|
||||
createNewChannelItem = PlusMenu.createNewChannelItem;
|
||||
openDirectMessageItem = PlusMenu.openDirectMessageItem;
|
||||
invitePeopleToTeamItem = PlusMenu.invitePeopleToTeamItem;
|
||||
|
||||
getCategoryCollapsed = (categoryKey: string) => {
|
||||
return element(by.id(`${this.testID.categoryHeaderPrefix}${categoryKey}.collapsed.true`));
|
||||
|
||||
76
detox/e2e/test/teams/invite_people.e2e.ts
Normal file
76
detox/e2e/test/teams/invite_people.e2e.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// *******************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a screen)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element testID when selecting an element. Create one if none.
|
||||
// *******************************************************************
|
||||
|
||||
import {Setup} from '@support/server_api';
|
||||
import {
|
||||
serverOneUrl,
|
||||
siteOneUrl,
|
||||
} from '@support/test_config';
|
||||
import {
|
||||
ChannelListScreen,
|
||||
HomeScreen,
|
||||
LoginScreen,
|
||||
ServerScreen,
|
||||
} from '@support/ui/screen';
|
||||
import {isIos} from '@support/utils';
|
||||
import {expect} from 'detox';
|
||||
|
||||
function systemDialog(label: string) {
|
||||
if (device.getPlatform() === 'ios') {
|
||||
return element(by.label(label)).atIndex(0);
|
||||
}
|
||||
return element(by.text(label));
|
||||
}
|
||||
|
||||
describe('Teams - Invite people', () => {
|
||||
const serverOneDisplayName = 'Server 1';
|
||||
|
||||
let testTeam: any;
|
||||
let testUser: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
const {team, user} = await Setup.apiInit(siteOneUrl);
|
||||
|
||||
testTeam = team;
|
||||
testUser = user;
|
||||
|
||||
// # Log in to server
|
||||
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
|
||||
await LoginScreen.login(testUser);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// * Verify on channel list screen
|
||||
await ChannelListScreen.toBeVisible();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// # Close share dialog
|
||||
await ChannelListScreen.headerTeamDisplayName.tap();
|
||||
|
||||
// # Log out
|
||||
await HomeScreen.logout();
|
||||
});
|
||||
|
||||
it('MM-T4729_2 - should be able to share a URL invite to the team', async () => {
|
||||
// # Open plus menu
|
||||
await ChannelListScreen.headerPlusButton.tap();
|
||||
|
||||
// * Verify invite people to team item is available
|
||||
await expect(ChannelListScreen.invitePeopleToTeamItem).toExist();
|
||||
|
||||
// # Tap on invite people to team item
|
||||
await ChannelListScreen.invitePeopleToTeamItem.tap();
|
||||
|
||||
if (isIos()) {
|
||||
// * Verify share dialog is open
|
||||
await expect(systemDialog(`Join the ${testTeam.display_name} team`)).toExist();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,9 @@ declare class TeamModel extends Model {
|
||||
/** allowed_domains : List of domains that can join this team */
|
||||
allowedDomains: string;
|
||||
|
||||
/** invite_id : The token id to use in invites to the team */
|
||||
inviteId: string;
|
||||
|
||||
/** categories : All the categories associated with this team */
|
||||
categories: Query<CategoryModel>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user