diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx
index f1636b810b..593966eda5 100644
--- a/webapp/channels/src/actions/global_actions.tsx
+++ b/webapp/channels/src/actions/global_actions.tsx
@@ -329,7 +329,18 @@ export async function getTeamRedirectChannelIfIsAccesible(user: UserProfile, tea
return null;
}
-export async function redirectUserToDefaultTeam() {
+function historyPushWithQueryParams(path: string, queryParams?: URLSearchParams) {
+ if (queryParams) {
+ getHistory().push({
+ pathname: path,
+ search: queryParams.toString(),
+ });
+ } else {
+ getHistory().push(path);
+ }
+}
+
+export async function redirectUserToDefaultTeam(searchParams?: URLSearchParams) {
let state = getState();
// Assume we need to load the user if they don't have any team memberships loaded or the user loaded
@@ -356,11 +367,11 @@ export async function redirectUserToDefaultTeam() {
const teams = getActiveTeamsList(state);
if (teams.length === 0) {
if (isUserFirstAdmin && onboardingFlowEnabled) {
- getHistory().push('/preparing-workspace');
+ historyPushWithQueryParams('/preparing-workspace', searchParams);
return;
}
- getHistory().push('/select_team');
+ historyPushWithQueryParams('/select_team', searchParams);
return;
}
@@ -373,7 +384,7 @@ export async function redirectUserToDefaultTeam() {
const channel = await getTeamRedirectChannelIfIsAccesible(user, team);
if (channel) {
dispatch(selectChannel(channel.id));
- getHistory().push(`/${team.name}/channels/${channel.name}`);
+ historyPushWithQueryParams(`/${team.name}/channels/${channel.name}`, searchParams);
return;
}
}
@@ -385,10 +396,10 @@ export async function redirectUserToDefaultTeam() {
const channel = await getTeamRedirectChannelIfIsAccesible(user, myTeam); // eslint-disable-line no-await-in-loop
if (channel) {
dispatch(selectChannel(channel.id));
- getHistory().push(`/${myTeam.name}/channels/${channel.name}`);
+ historyPushWithQueryParams(`/${myTeam.name}/channels/${channel.name}`, searchParams);
return;
}
}
- getHistory().push('/select_team');
+ historyPushWithQueryParams('/select_team', searchParams);
}
diff --git a/webapp/channels/src/components/channel_layout/channel_controller.test.tsx b/webapp/channels/src/components/channel_layout/channel_controller.test.tsx
index 6f53beb10c..d5907aa06e 100644
--- a/webapp/channels/src/components/channel_layout/channel_controller.test.tsx
+++ b/webapp/channels/src/components/channel_layout/channel_controller.test.tsx
@@ -1,12 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {render, act} from '@testing-library/react';
+import {act} from '@testing-library/react';
import React from 'react';
import {Provider} from 'react-redux';
import * as actions from 'actions/status_actions';
+import {renderWithContext} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import Constants from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
@@ -54,7 +55,7 @@ describe('ChannelController', () => {
mockState.entities.general.config.EnableUserStatuses = 'true';
const store = mockStore(mockState);
- render(
+ renderWithContext(
,
@@ -71,7 +72,7 @@ describe('ChannelController', () => {
const store = mockStore(mockState);
mockState.entities.general.config.EnableUserStatuses = 'false';
- render(
+ renderWithContext(
,
diff --git a/webapp/channels/src/components/channel_layout/channel_controller.tsx b/webapp/channels/src/components/channel_layout/channel_controller.tsx
index 7836c213ad..0e962e0ce9 100644
--- a/webapp/channels/src/components/channel_layout/channel_controller.tsx
+++ b/webapp/channels/src/components/channel_layout/channel_controller.tsx
@@ -13,6 +13,7 @@ import {addVisibleUsersInCurrentChannelAndSelfToStatusPoll} from 'actions/status
import {makeAsyncComponent} from 'components/async_load';
import CenterChannel from 'components/channel_layout/center_channel';
import LoadingScreen from 'components/loading_screen';
+import QueryParamActionController from 'components/query_param_actions/query_param_action_controller';
import Sidebar from 'components/sidebar';
import CRTPostsChannelResetWatcher from 'components/threading/channel_threads/posts_channel_reset_watcher';
import UnreadsStatusHandler from 'components/unreads_status_handler';
@@ -69,6 +70,7 @@ export default function ChannelController(props: Props) {
return (
<>
+
void;
+ title?: string;
+ description?: string;
}
export default function NoPermissionsView(props: Props) {
return (
<>
+
+
+
+
+
diff --git a/webapp/channels/src/components/query_param_actions/query_param_action_controller.test.tsx b/webapp/channels/src/components/query_param_actions/query_param_action_controller.test.tsx
new file mode 100644
index 0000000000..4243a7a5b1
--- /dev/null
+++ b/webapp/channels/src/components/query_param_actions/query_param_action_controller.test.tsx
@@ -0,0 +1,116 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {useDispatch} from 'react-redux';
+import {MemoryRouter, Route, useHistory} from 'react-router-dom';
+
+import {openModal} from 'actions/views/modals';
+
+import {renderWithContext} from 'tests/react_testing_utils';
+
+import QueryParamActionController from './query_param_action_controller';
+
+// Mock react-redux since we just care about calling logic
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useDispatch: jest.fn(),
+}));
+
+jest.mock('actions/views/modals', () => ({
+ openModal: jest.fn(),
+}));
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: jest.fn(),
+}));
+
+describe('QueryParamActionController', () => {
+ let mockDispatch: jest.Mock;
+
+ // Define a custom type for mockHistory that includes the replace method
+ interface MockHistory extends jest.Mock {
+ replace: jest.Mock;
+ }
+ let mockHistory: MockHistory;
+
+ beforeEach(() => {
+ mockDispatch = jest.fn();
+ (useDispatch as jest.Mock).mockReturnValue(mockDispatch);
+ mockHistory = {
+ replace: jest.fn(),
+ } as MockHistory;
+ (useHistory as jest.Mock).mockReturnValue(mockHistory);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should dispatch openModal for INVITATION modal ID when passed valid open_invitation_modal action', () => {
+ renderWithContext(
+
+
+ ,
+ );
+
+ expect(mockDispatch).toHaveBeenCalledWith(
+ openModal({
+ modalId: 'INVITATION',
+ dialogType: expect.any(Function),
+ }),
+ );
+ });
+
+ it('should not dispatch any action when action query parameter is not present', () => {
+ renderWithContext(
+
+
+ ,
+ );
+
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+
+ it('should not dispatch any action when action query parameter is not in list', () => {
+ renderWithContext(
+
+
+ ,
+ );
+
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+
+ it('should remove the action query parameter after dispatching the action', () => {
+ renderWithContext(
+
+
+ ,
+ );
+
+ expect(mockDispatch).toHaveBeenCalledWith(
+ openModal({
+ modalId: 'INVITATION',
+ dialogType: expect.any(Function),
+ }),
+ );
+
+ expect(mockHistory.replace).toHaveBeenCalledWith({
+ search: '',
+ });
+ });
+});
diff --git a/webapp/channels/src/components/query_param_actions/query_param_action_controller.tsx b/webapp/channels/src/components/query_param_actions/query_param_action_controller.tsx
new file mode 100644
index 0000000000..a2c7e64d0e
--- /dev/null
+++ b/webapp/channels/src/components/query_param_actions/query_param_action_controller.tsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {useEffect} from 'react';
+import {useDispatch} from 'react-redux';
+import {useHistory, useLocation} from 'react-router-dom';
+
+import {openModal} from 'actions/views/modals';
+
+import InvitationModal from 'components/invitation_modal';
+
+import {ModalIdentifiers} from 'utils/constants';
+
+import type {ModalData} from 'types/actions';
+
+interface ActionMap {
+ [key: string]: ModalData;
+}
+
+function QueryParamActionController() {
+ const location = useLocation();
+ const dispatch = useDispatch();
+ const history = useHistory();
+
+ const actionMap: ActionMap = {
+ open_invitation_modal: {
+ modalId: ModalIdentifiers.INVITATION,
+ dialogType: InvitationModal,
+ },
+ };
+
+ useEffect(() => {
+ const searchParams = new URLSearchParams(location.search);
+ const action = searchParams.get('action');
+
+ if (action && actionMap[action]) {
+ dispatch(openModal(actionMap[action]));
+
+ // Delete the action after it's been invoked so that it's not locked for subsequent refreshes
+ searchParams.delete('action');
+ history.replace({
+ search: searchParams.toString(),
+ });
+ }
+ }, [location, actionMap]);
+
+ return null;
+}
+
+export default QueryParamActionController;
diff --git a/webapp/channels/src/components/root/actions/index.ts b/webapp/channels/src/components/root/actions/index.ts
index 3a4d91d09d..c101327fa5 100644
--- a/webapp/channels/src/components/root/actions/index.ts
+++ b/webapp/channels/src/components/root/actions/index.ts
@@ -99,12 +99,12 @@ export function registerCustomPostRenderer(type: string, component: any, id: str
};
}
-export function redirectToOnboardingOrDefaultTeam(history: History): ThunkActionFunc {
+export function redirectToOnboardingOrDefaultTeam(history: History, searchParams?: URLSearchParams): ThunkActionFunc {
return async (dispatch, getState) => {
const state = getState();
const isUserAdmin = isCurrentUserSystemAdmin(state);
if (!isUserAdmin) {
- redirectUserToDefaultTeam();
+ redirectUserToDefaultTeam(searchParams);
return;
}
@@ -113,19 +113,19 @@ export function redirectToOnboardingOrDefaultTeam(history: History): ThunkAction
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state);
if (teams.length > 0 || !onboardingFlowEnabled) {
- redirectUserToDefaultTeam();
+ redirectUserToDefaultTeam(searchParams);
return;
}
const firstAdminSetupComplete = await dispatch(getFirstAdminSetupComplete());
if (firstAdminSetupComplete?.data) {
- redirectUserToDefaultTeam();
+ redirectUserToDefaultTeam(searchParams);
return;
}
const profilesResult = await dispatch(getProfiles(0, General.PROFILE_CHUNK_SIZE, {roles: General.SYSTEM_ADMIN_ROLE}));
if (profilesResult.error) {
- redirectUserToDefaultTeam();
+ redirectUserToDefaultTeam(searchParams);
return;
}
const currentUser = getCurrentUser(getState());
@@ -141,7 +141,7 @@ export function redirectToOnboardingOrDefaultTeam(history: History): ThunkAction
return;
}
- redirectUserToDefaultTeam();
+ redirectUserToDefaultTeam(searchParams);
};
}
diff --git a/webapp/channels/src/components/root/root.tsx b/webapp/channels/src/components/root/root.tsx
index 6bba0340a2..7d57f60caa 100644
--- a/webapp/channels/src/components/root/root.tsx
+++ b/webapp/channels/src/components/root/root.tsx
@@ -317,7 +317,7 @@ export default class Root extends React.PureComponent {
if (isUserAtRootRoute) {
if (isMeRequested) {
- this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history);
+ this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history, new URLSearchParams(this.props.location.search));
} else if (this.props.noAccounts) {
this.props.history.push('/signup_user_complete');
}
diff --git a/webapp/channels/src/components/root/root_redirect/root_redirect.tsx b/webapp/channels/src/components/root/root_redirect/root_redirect.tsx
index 3a1db8624e..199aec748a 100644
--- a/webapp/channels/src/components/root/root_redirect/root_redirect.tsx
+++ b/webapp/channels/src/components/root/root_redirect/root_redirect.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
-import {Redirect, useHistory} from 'react-router-dom';
+import {Redirect, useHistory, useLocation} from 'react-router-dom';
import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -21,6 +21,7 @@ export type Props = {
export default function RootRedirect(props: Props) {
const history = useHistory();
+ const location = useLocation();
useEffect(() => {
if (props.currentUserId) {
@@ -30,11 +31,11 @@ export default function RootRedirect(props: Props) {
if (firstAdminCompletedSignup.data === false && props.isFirstAdmin && !props.areThereTeams) {
history.push('/preparing-workspace');
} else {
- GlobalActions.redirectUserToDefaultTeam();
+ GlobalActions.redirectUserToDefaultTeam(new URLSearchParams(location.search));
}
});
} else {
- GlobalActions.redirectUserToDefaultTeam();
+ GlobalActions.redirectUserToDefaultTeam(new URLSearchParams(location.search));
}
}
}, [props.currentUserId, props.isElegibleForFirstAdmingOnboarding]);
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 8386b1f190..0cb42ea4c5 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -4011,7 +4011,7 @@
"invite_modal.invited_guests": "Guests",
"invite_modal.invited_members": "Members",
"invite_modal.no_permissions.description": "You do not have permissions to add users or guests. If this seems like an error, please reach out to your system administrator.",
- "invite_modal.no_permissions.title": "Unable to continue",
+ "invite_modal.no_permissions.title": "Unable to invite people",
"invite_modal.people": "people",
"invite_modal.restricted_invite_guest.post_trial_description": "Collaborate with users outside of your organization while tightly controlling their access to channels and team members. Upgrade to the Professional plan to create unlimited user groups.",
"invite_modal.restricted_invite_guest.post_trial_title": "Upgrade to invite guest",