Merge pull request #6719 from julmondragon/MM-39720
This commit is contained in:
commit
b9ae96d45b
17 changed files with 251 additions and 10 deletions
|
|
@ -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!'}
|
||||
/>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,11 +24,14 @@ import {typography} from '@utils/typography';
|
|||
|
||||
import LoadingUnreads from './loading_unreads';
|
||||
import PlusMenu from './plus_menu';
|
||||
import {SEPARATOR_HEIGHT} from './plus_menu/separator';
|
||||
|
||||
type Props = {
|
||||
canCreateChannels: boolean;
|
||||
canJoinChannels: boolean;
|
||||
displayName: string;
|
||||
canInvitePeople: boolean;
|
||||
displayName?: string;
|
||||
inviteId?: string;
|
||||
iconPad?: boolean;
|
||||
onHeaderPress?: () => void;
|
||||
pushProxyStatus: string;
|
||||
|
|
@ -92,7 +95,9 @@ const hitSlop: Insets = {top: 10, bottom: 30, left: 20, right: 20};
|
|||
const ChannelListHeader = ({
|
||||
canCreateChannels,
|
||||
canJoinChannels,
|
||||
canInvitePeople,
|
||||
displayName,
|
||||
inviteId,
|
||||
iconPad,
|
||||
onHeaderPress,
|
||||
pushProxyStatus,
|
||||
|
|
@ -118,12 +123,17 @@ const ChannelListHeader = ({
|
|||
<PlusMenu
|
||||
canCreateChannels={canCreateChannels}
|
||||
canJoinChannels={canJoinChannels}
|
||||
canInvitePeople={canInvitePeople}
|
||||
displayName={displayName}
|
||||
inviteId={inviteId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const closeButtonId = 'close-plus-menu';
|
||||
let items = 1;
|
||||
let separators = 0;
|
||||
|
||||
if (canCreateChannels) {
|
||||
items += 1;
|
||||
}
|
||||
|
|
@ -132,10 +142,15 @@ const ChannelListHeader = ({
|
|||
items += 1;
|
||||
}
|
||||
|
||||
if (canInvitePeople) {
|
||||
items += 1;
|
||||
separators += 1;
|
||||
}
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId,
|
||||
renderContent,
|
||||
snapPoints: [bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom), 10],
|
||||
snapPoints: [bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom) + (separators * SEPARATOR_HEIGHT), 10],
|
||||
theme,
|
||||
title: intl.formatMessage({id: 'home.header.plus_menu', defaultMessage: 'Options'}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {observeConfigBooleanValue, observePushVerificationStatus} from '@queries/servers/system';
|
||||
import {observeCurrentTeam} from '@queries/servers/team';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
|
|
@ -21,6 +21,8 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
|||
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
const enableOpenServer = observeConfigBooleanValue(database, 'EnableOpenServer');
|
||||
|
||||
const canJoinChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)),
|
||||
);
|
||||
|
|
@ -37,12 +39,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,22 +3,31 @@
|
|||
|
||||
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';
|
||||
|
||||
import PlusMenuItem from './item';
|
||||
import PlusMenuSeparator from './separator';
|
||||
|
||||
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 +57,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 +128,15 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
|
|||
pickerAction='openDirectMessage'
|
||||
onPress={openDirectMessage}
|
||||
/>
|
||||
{canInvitePeople &&
|
||||
<>
|
||||
<PlusMenuSeparator/>
|
||||
<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,19 @@ 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',
|
||||
},
|
||||
};
|
||||
|
||||
const itemType = menuItems[pickerAction];
|
||||
|
||||
return (
|
||||
<SlideUpPanelItem
|
||||
text={itemType.text}
|
||||
icon={itemType.icon}
|
||||
{...itemType}
|
||||
onPress={onPress}
|
||||
testID={itemType.testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const MARGIN_VERTICAL = 8;
|
||||
const BORDER = 1;
|
||||
export const SEPARATOR_HEIGHT = (MARGIN_VERTICAL * 2) + BORDER;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
separator: {
|
||||
marginTop: MARGIN_VERTICAL,
|
||||
marginBottom: MARGIN_VERTICAL,
|
||||
borderTopWidth: BORDER,
|
||||
borderStyle: 'solid',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
}));
|
||||
|
||||
const PlusMenuSeparator = () => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlusMenuSeparator;
|
||||
|
|
@ -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 ",
|
||||
|
|
@ -679,6 +681,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-T5221 - 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>;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue