mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-61032: Add default_team_id to accept invite flow (#28841)
* add default_team_id to accept invite api * add team selector to accept invite flow UI * e2e * lint/i18n
This commit is contained in:
parent
5a923e0b94
commit
fcded9559c
@ -277,6 +277,7 @@
|
||||
required:
|
||||
- invite
|
||||
- name
|
||||
- default_team_id
|
||||
- password
|
||||
properties:
|
||||
invite:
|
||||
@ -285,6 +286,8 @@
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
default_team_id:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
description: The password to decrypt the invite code.
|
||||
|
@ -84,6 +84,9 @@ describe('Connected Workspaces', () => {
|
||||
cy.findByText('Accept a secure connection from another server');
|
||||
cy.findByText('Enter the encrypted invitation code shared to you by the admin of the server you are connecting with.');
|
||||
|
||||
// * Verify accept disabled
|
||||
cy.uiGetButton('Accept').should('be.disabled');
|
||||
|
||||
// # Enter org name
|
||||
cy.findByRole('textbox', {name: 'Organization name'}).type(orgDisplayName);
|
||||
|
||||
@ -93,6 +96,13 @@ describe('Connected Workspaces', () => {
|
||||
// # Enter bad password
|
||||
cy.findByRole('textbox', {name: 'Password'}).type('123abc');
|
||||
|
||||
// * Verify accept still disabled
|
||||
cy.uiGetButton('Accept').should('be.disabled');
|
||||
|
||||
// # Select team
|
||||
cy.findByTestId('destination-team-input').click().
|
||||
findByRole('textbox').type(`${testTeam2.display_name}{enter}`);
|
||||
|
||||
// # Try accept
|
||||
cy.uiGetButton('Accept').click();
|
||||
|
||||
|
@ -464,6 +464,16 @@ func remoteClusterAcceptInvite(c *Context, w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
if rcAcceptInvite.DefaultTeamId == "" {
|
||||
c.SetInvalidParam("remoteCluster.default_team_id")
|
||||
return
|
||||
}
|
||||
|
||||
if _, teamErr := c.App.GetTeam(rcAcceptInvite.DefaultTeamId); teamErr != nil {
|
||||
c.SetInvalidParamWithErr("remoteCluster.default_team_id", teamErr)
|
||||
return
|
||||
}
|
||||
|
||||
audit.AddEventParameter(auditRec, "name", rcAcceptInvite.Name)
|
||||
audit.AddEventParameter(auditRec, "display_name", rcAcceptInvite.DisplayName)
|
||||
|
||||
@ -485,7 +495,7 @@ func remoteClusterAcceptInvite(c *Context, w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
rc, aErr := rcs.AcceptInvitation(invite, rcAcceptInvite.Name, rcAcceptInvite.DisplayName, c.AppContext.Session().UserId, url)
|
||||
rc, aErr := rcs.AcceptInvitation(invite, rcAcceptInvite.Name, rcAcceptInvite.DisplayName, c.AppContext.Session().UserId, url, rcAcceptInvite.DefaultTeamId)
|
||||
if aErr != nil {
|
||||
c.Err = model.NewAppError("remoteClusterAcceptInvite", "api.remote_cluster.accept_invitation_error", nil, "", http.StatusInternalServerError).Wrap(aErr)
|
||||
if appErr, ok := aErr.(*model.AppError); ok {
|
||||
|
@ -298,6 +298,7 @@ func TestRemoteClusterAcceptinvite(t *testing.T) {
|
||||
Name: "remotecluster",
|
||||
Invite: "myinvitecode",
|
||||
Password: "mysupersecret",
|
||||
DefaultTeamId: "",
|
||||
}
|
||||
|
||||
t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) {
|
||||
@ -313,6 +314,8 @@ func TestRemoteClusterAcceptinvite(t *testing.T) {
|
||||
th := setupForSharedChannels(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
rcAcceptInvite.DefaultTeamId = th.BasicTeam.Id
|
||||
|
||||
remoteId := model.NewId()
|
||||
invite := &model.RemoteClusterInvite{
|
||||
RemoteId: remoteId,
|
||||
@ -335,7 +338,7 @@ func TestRemoteClusterAcceptinvite(t *testing.T) {
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
|
||||
|
||||
t.Run("should fail if the parameters are not valid", func(t *testing.T) {
|
||||
t.Run("should fail if the name parameter is not valid", func(t *testing.T) {
|
||||
rcAcceptInvite.Name = ""
|
||||
defer func() { rcAcceptInvite.Name = "remotecluster" }()
|
||||
|
||||
@ -345,6 +348,26 @@ func TestRemoteClusterAcceptinvite(t *testing.T) {
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
t.Run("should fail if the default team parameter is empty", func(t *testing.T) {
|
||||
rcAcceptInvite.DefaultTeamId = ""
|
||||
defer func() { rcAcceptInvite.DefaultTeamId = th.BasicTeam.Id }()
|
||||
|
||||
rc, resp, err := th.SystemAdminClient.RemoteClusterAcceptInvite(context.Background(), rcAcceptInvite)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
t.Run("should fail if the default team provided doesn't exist", func(t *testing.T) {
|
||||
rcAcceptInvite.DefaultTeamId = model.NewId()
|
||||
defer func() { rcAcceptInvite.DefaultTeamId = th.BasicTeam.Id }()
|
||||
|
||||
rc, resp, err := th.SystemAdminClient.RemoteClusterAcceptInvite(context.Background(), rcAcceptInvite)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, rc)
|
||||
})
|
||||
|
||||
t.Run("should fail with the correct status code if the invite returns an app error", func(t *testing.T) {
|
||||
rcAcceptInvite.Invite = "malformedinvite"
|
||||
// reset the invite after
|
||||
|
@ -190,7 +190,7 @@ func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs ma
|
||||
return responsef(args.T("api.command_remote.site_url_not_set"))
|
||||
}
|
||||
|
||||
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, url)
|
||||
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, url, "")
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_remote.accept_invitation.error", map[string]any{"Error": err.Error()}))
|
||||
}
|
||||
|
@ -12,11 +12,12 @@ import (
|
||||
)
|
||||
|
||||
// AcceptInvitation is called when accepting an invitation to connect with a remote cluster.
|
||||
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName, creatorId string, siteURL string) (*model.RemoteCluster, error) {
|
||||
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, siteURL string, defaultTeamId string) (*model.RemoteCluster, error) {
|
||||
rc := &model.RemoteCluster{
|
||||
RemoteId: invite.RemoteId,
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
DefaultTeamId: defaultTeamId,
|
||||
Token: model.NewId(),
|
||||
RemoteToken: invite.Token,
|
||||
SiteURL: invite.SiteURL,
|
||||
|
@ -68,7 +68,7 @@ type RemoteClusterServiceIFace interface {
|
||||
SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f SendMsgResultFunc) error
|
||||
SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp ReaderProvider, f SendFileResultFunc) error
|
||||
SendProfileImage(ctx context.Context, userID string, rc *model.RemoteCluster, provider ProfileImageProvider, f SendProfileImageResultFunc) error
|
||||
AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, siteURL string) (*model.RemoteCluster, error)
|
||||
AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, siteURL string, defaultTeamId string) (*model.RemoteCluster, error)
|
||||
ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) Response
|
||||
ReceiveInviteConfirmation(invite model.RemoteClusterInvite) (*model.RemoteCluster, error)
|
||||
PingNow(rc *model.RemoteCluster)
|
||||
|
@ -464,6 +464,7 @@ func (rci *RemoteClusterInvite) Decrypt(encrypted []byte, password string) error
|
||||
type RemoteClusterAcceptInvite struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
DefaultTeamId string `json:"default_team_id"`
|
||||
Invite string `json:"invite"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
@ -156,6 +156,14 @@ export const ModalFieldsetWrapper = styled.div`
|
||||
background: none !important;
|
||||
height: 34px !important;
|
||||
}
|
||||
|
||||
.Input_container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.DropdownInput.Input_container {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModalLegend = styled.legend`
|
||||
|
@ -116,7 +116,7 @@ export const useRemoteClusterAcceptInvite = () => {
|
||||
modalId: ModalIdentifiers.SECURE_CONNECTION_ACCEPT_INVITE,
|
||||
dialogType: SecureConnectionAcceptInviteModal,
|
||||
dialogProps: {
|
||||
onConfirm: async (acceptInvite: PartialExcept<RemoteClusterAcceptInvite, 'display_name' | 'invite' | 'password'>) => {
|
||||
onConfirm: async (acceptInvite: PartialExcept<RemoteClusterAcceptInvite, 'display_name' | 'default_team_id' | 'invite' | 'password'>) => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const rc = await Client4.acceptInviteRemoteCluster({
|
||||
|
@ -13,12 +13,13 @@ import LoadingScreen from 'components/loading_screen';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
|
||||
import {ModalFieldset, ModalParagraph} from '../controls';
|
||||
import {isErrorState, isPendingState} from '../utils';
|
||||
import TeamSelector from '../team_selector';
|
||||
import {isErrorState, isPendingState, useTeamOptions} from '../utils';
|
||||
|
||||
type Props = {
|
||||
creating?: boolean;
|
||||
password?: string;
|
||||
onConfirm: (accept: PartialExcept<RemoteClusterAcceptInvite, 'display_name' | 'invite' | 'password'>) => Promise<RemoteCluster>;
|
||||
onConfirm: (accept: PartialExcept<RemoteClusterAcceptInvite, 'display_name' | 'default_team_id' | 'invite' | 'password'>) => Promise<RemoteCluster>;
|
||||
onCancel?: () => void;
|
||||
onExited: () => void;
|
||||
onHide: () => void;
|
||||
@ -34,12 +35,16 @@ function SecureConnectionAcceptInviteModal({
|
||||
}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [defaultTeamId, setDefaultTeamId] = useState('');
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [saving, setSaving] = useState<boolean | ClientError>(false);
|
||||
|
||||
const teamsById = useTeamOptions();
|
||||
|
||||
const need = {
|
||||
displayName: !displayName,
|
||||
defaultTeamId: !defaultTeamId,
|
||||
inviteCode: !inviteCode,
|
||||
password: !password,
|
||||
};
|
||||
@ -50,7 +55,12 @@ function SecureConnectionAcceptInviteModal({
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
await onConfirm({display_name: displayName, invite: inviteCode, password});
|
||||
await onConfirm({
|
||||
display_name: displayName,
|
||||
default_team_id: defaultTeamId,
|
||||
invite: inviteCode,
|
||||
password,
|
||||
});
|
||||
setSaving(false);
|
||||
onHide();
|
||||
} catch (err) {
|
||||
@ -90,6 +100,7 @@ function SecureConnectionAcceptInviteModal({
|
||||
modalHeaderText={title}
|
||||
onExited={onExited}
|
||||
compassDesign={true}
|
||||
bodyOverflowVisible={true}
|
||||
autoCloseOnConfirmButton={false}
|
||||
errorText={isErrorState(saving) && (
|
||||
<FormattedMessage
|
||||
@ -120,6 +131,23 @@ function SecureConnectionAcceptInviteModal({
|
||||
onChange={handleDisplayNameChange}
|
||||
data-testid='display-name'
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id={'admin.secure_connections.accept_invite.select_team'}
|
||||
defaultMessage={'Please select the destination team where channels will be placed.'}
|
||||
tagName={ModalParagraph}
|
||||
/>
|
||||
<TeamSelector
|
||||
testId='destination-team-input'
|
||||
value={defaultTeamId}
|
||||
teamsById={teamsById}
|
||||
onChange={setDefaultTeamId}
|
||||
legend={formatMessage({
|
||||
id: 'admin.secure_connections.accept_invite.select_team.legend',
|
||||
defaultMessage: 'Select a team',
|
||||
})}
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id={'admin.secure_connections.accept_invite.prompt_invite_password'}
|
||||
defaultMessage={'Enter the encrypted invitation code shared to you by the admin of the server you are connecting with.'}
|
||||
|
@ -12,11 +12,8 @@ import styled from 'styled-components';
|
||||
|
||||
import {GlobeIcon, LockIcon, PlusIcon, ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import {isRemoteClusterPatch, type RemoteCluster} from '@mattermost/types/remote_clusters';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {setNavigationBlocked} from 'actions/admin_actions';
|
||||
|
||||
@ -47,7 +44,7 @@ import {
|
||||
import {useRemoteClusterCreate, useSharedChannelsAdd, useSharedChannelsRemove} from './modals/modal_utils';
|
||||
import TeamSelector from './team_selector';
|
||||
import type {SharedChannelRemoteRow} from './utils';
|
||||
import {getEditLocation, isConfirmed, isErrorState, isPendingState, useRemoteClusterEdit, useSharedChannelRemoteRows} from './utils';
|
||||
import {getEditLocation, isConfirmed, isErrorState, isPendingState, useRemoteClusterEdit, useSharedChannelRemoteRows, useTeamOptions} from './utils';
|
||||
|
||||
import {AdminConsoleListTable} from '../list_table';
|
||||
import SaveChangesPanel from '../team_channel_settings/save_changes_panel';
|
||||
@ -73,6 +70,8 @@ export default function SecureConnectionDetail(props: Props) {
|
||||
|
||||
const {promptCreate, saving: creating} = useRemoteClusterCreate();
|
||||
|
||||
const teamsById = useTeamOptions();
|
||||
|
||||
useEffect(() => {
|
||||
// keep history cache up to date
|
||||
history.replace({...location, state: currentRemoteCluster});
|
||||
@ -87,8 +86,6 @@ export default function SecureConnectionDetail(props: Props) {
|
||||
applyPatch({display_name: value});
|
||||
};
|
||||
|
||||
const teams = useSelector(getActiveTeamsList);
|
||||
const teamsById = useMemo(() => teams.reduce<IDMappedObjects<Team>>((teams, team) => ({...teams, [team.id]: team}), {}), [teams]);
|
||||
const handleTeamChange = (teamId: string) => {
|
||||
applyPatch({default_team_id: teamId});
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ export type Props = {
|
||||
teamsById: IDMappedObjects<Team>;
|
||||
onChange: (teamId: string) => void;
|
||||
testId: string;
|
||||
legend?: string;
|
||||
}
|
||||
|
||||
const TeamSelector = (props: Props): JSX.Element => {
|
||||
@ -40,6 +41,7 @@ const TeamSelector = (props: Props): JSX.Element => {
|
||||
value={value ? {label: value.display_name, value: value.id} : undefined}
|
||||
options={teamValues}
|
||||
name='team_selector'
|
||||
legend={props.legend}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -3,8 +3,8 @@
|
||||
|
||||
import type {LocationDescriptor} from 'history';
|
||||
import {DateTime, Interval} from 'luxon';
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {ClientError} from '@mattermost/client';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
@ -17,7 +17,7 @@ import {ChannelTypes} from 'mattermost-redux/action_types';
|
||||
import {getChannel as fetchChannel} from 'mattermost-redux/actions/channels';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getActiveTeamsList, getTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
@ -228,6 +228,12 @@ export const useSharedChannelRemoteRows = (remoteId: string, opts: {filter: 'hom
|
||||
return [sharedChannelRemotes, {loading, error, fetch}] as const;
|
||||
};
|
||||
|
||||
export const useTeamOptions = () => {
|
||||
const teams = useSelector(getActiveTeamsList);
|
||||
const teamsById = useMemo(() => teams.reduce<IDMappedObjects<Team>>((teams, team) => ({...teams, [team.id]: team}), {}), [teams]);
|
||||
return teamsById;
|
||||
};
|
||||
|
||||
export const getEditLocation = (rc: RemoteCluster): LocationDescriptor<RemoteCluster> => {
|
||||
return {pathname: `/admin_console/environment/secure_connections/${rc.remote_id}`, state: rc};
|
||||
};
|
||||
|
@ -2222,6 +2222,8 @@
|
||||
"admin.secure_connections.accept_invite.prompt": "Accept a secure connection from another server",
|
||||
"admin.secure_connections.accept_invite.prompt_invite_password": "Enter the encrypted invitation code shared to you by the admin of the server you are connecting with.",
|
||||
"admin.secure_connections.accept_invite.saving_changes_error": "There was an error while accepting the invite.",
|
||||
"admin.secure_connections.accept_invite.select_team": "Please select the destination team where channels will be placed.",
|
||||
"admin.secure_connections.accept_invite.select_team.legend": "Select a team",
|
||||
"admin.secure_connections.accept_invite.share_title": "Accept a connection invite",
|
||||
"admin.secure_connections.confirm.delete.button": "Yes, delete",
|
||||
"admin.secure_connections.confirm.delete.text": "Are you sure you want to delete the secure connection <strong>{displayName}</strong>?",
|
||||
|
@ -11,6 +11,7 @@ export type RemoteClusterInvite = {
|
||||
export type RemoteClusterAcceptInvite = {
|
||||
name: string;
|
||||
display_name: string;
|
||||
default_team_id: string;
|
||||
invite: string;
|
||||
password: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user