Skip to content

Commit 6e39168

Browse files
Add playbooks unified view (#9177) (#9208)
* Add playbooks unified view * Update tests * Fix client function not throwing * Fix types (cherry picked from commit 9272efe) Co-authored-by: Daniel Espino García <[email protected]>
1 parent 4b13302 commit 6e39168

File tree

30 files changed

+1224
-53
lines changed

30 files changed

+1224
-53
lines changed

app/actions/local/channel.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {getCurrentUser, queryUsersById} from '@queries/servers/user';
2121
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
2222
import EphemeralStore from '@store/ephemeral_store';
2323
import {isTablet} from '@utils/helpers';
24-
import {logError, logInfo} from '@utils/log';
24+
import {logDebug, logError, logInfo} from '@utils/log';
2525
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
2626

2727
import type ServerDataOperator from '@database/operator/server_data_operator';
@@ -88,14 +88,16 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
8888
}
8989

9090
if (isTabletDevice) {
91-
dismissAllModalsAndPopToRoot();
91+
await dismissAllModalsAndPopToRoot();
9292
DeviceEventEmitter.emit(NavigationConstants.NAVIGATION_HOME, Screens.CHANNEL);
9393
} else {
94-
dismissAllModalsAndPopToScreen(Screens.CHANNEL, '', undefined, {topBar: {visible: false}});
94+
await dismissAllModalsAndPopToScreen(Screens.CHANNEL, '', undefined, {topBar: {visible: false}});
9595
}
9696

9797
logInfo('channel switch to', channel?.displayName, channelId, (Date.now() - dt), 'ms');
9898
}
99+
} else {
100+
logDebug('failed to navigate to channel because there was no membership, channel id: ', channelId);
99101
}
100102

101103
return {models};

app/actions/remote/channel.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
joinChannelIfNeeded,
2929
markChannelAsRead,
3030
unsetActiveChannelOnServer,
31-
switchToChannelByName,
31+
joinIfNeededAndSwitchToChannel,
3232
goToNPSChannel,
3333
fetchMissingDirectChannelsInfo,
3434
fetchDirectChannelsInfo,
@@ -434,22 +434,22 @@ describe('channel', () => {
434434
});
435435

436436
it('switchToChannelByName - handle not found database', async () => {
437-
const {error} = await switchToChannelByName('foo', '', '', () => {}, intl);
437+
const {error} = await joinIfNeededAndSwitchToChannel('foo', {}, {}, () => {}, intl);
438438
expect(error).toBeDefined();
439439
});
440440

441441
it('switchToChannelByName - base case', async () => {
442442
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user.id}, {id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
443443

444-
const result = await switchToChannelByName(serverUrl, 'channelname', 'teamname', () => {}, intl);
444+
const result = await joinIfNeededAndSwitchToChannel(serverUrl, {name: 'channelname'}, {name: 'teamname'}, () => {}, intl);
445445
expect(result).toBeDefined();
446446
expect(result).not.toHaveProperty('error');
447447
});
448448

449449
it('switchToChannelByName - team redirect', async () => {
450450
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user.id}, {id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
451451

452-
const result = await switchToChannelByName(serverUrl, 'channelname', DeepLink.Redirect, () => {}, intl);
452+
const result = await joinIfNeededAndSwitchToChannel(serverUrl, {name: 'channelname'}, {name: DeepLink.Redirect}, () => {}, intl);
453453
expect(result).toBeDefined();
454454
expect(result).not.toHaveProperty('error');
455455
});

app/actions/remote/channel.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {getActiveServer} from '@queries/app/servers';
1919
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, deleteChannelMembership, queryChannelsById} from '@queries/servers/channel';
2020
import {queryDisplayNamePreferences} from '@queries/servers/preference';
2121
import {getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
22-
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
22+
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory, getTeamById} from '@queries/servers/team';
2323
import {getIsCRTEnabled} from '@queries/servers/thread';
2424
import {getCurrentUser} from '@queries/servers/user';
2525
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
@@ -38,7 +38,7 @@ import {fetchPostsForChannel} from './post';
3838
import {openChannelIfNeeded, savePreference} from './preference';
3939
import {fetchRolesIfNeeded} from './role';
4040
import {forceLogoutIfNecessary} from './session';
41-
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
41+
import {addCurrentUserToTeam, fetchTeamById, fetchTeamByName, removeCurrentUserFromTeam} from './team';
4242
import {fetchProfilesInChannel, fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
4343

4444
import type {Model} from '@nozbe/watermelondb';
@@ -763,29 +763,40 @@ export async function unsetActiveChannelOnServer(serverUrl: string) {
763763
}
764764
}
765765

766-
export async function switchToChannelByName(serverUrl: string, channelName: string, teamName: string, errorHandler: (intl: IntlShape) => void, intl: IntlShape) {
767-
const onError = (joinedTeam: boolean, teamId?: string) => {
766+
export async function joinIfNeededAndSwitchToChannel(
767+
serverUrl: string,
768+
channelInfo: {id?: string; name?: string},
769+
teamInfo: {id?: string; name?: string} | undefined,
770+
errorHandler: (intl: IntlShape) => void,
771+
intl: IntlShape,
772+
) {
773+
const onError = (joinedTeam: boolean, teamIdToRemove?: string) => {
768774
errorHandler(intl);
769-
if (joinedTeam && teamId) {
770-
removeCurrentUserFromTeam(serverUrl, teamId, false);
775+
if (joinedTeam && teamIdToRemove) {
776+
removeCurrentUserFromTeam(serverUrl, teamIdToRemove, false);
771777
}
772778
};
773779

774780
let joinedTeam = false;
775-
let teamId = '';
781+
let teamId = teamInfo?.id || '';
782+
let channelId = channelInfo?.id || '';
783+
const channelName = channelInfo?.name || '';
784+
const teamName = teamInfo?.name || '';
776785

777786
try {
778787
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
779788

780789
if (teamName === DeepLink.Redirect) {
781790
teamId = await getCurrentTeamId(database);
782791
} else {
783-
const team = await getTeamByName(database, teamName);
784-
const isTeamMember = team ? await getMyTeamById(database, team.id) : false;
785-
teamId = team?.id || '';
792+
const team = teamId ? await getTeamById(database, teamId) : await getTeamByName(database, teamName);
793+
const isTeamMember = team ? Boolean(await getMyTeamById(database, team.id)) : false;
794+
if (!teamId) {
795+
teamId = team?.id || '';
796+
}
786797

787798
if (!isTeamMember) {
788-
const fetchRequest = await fetchTeamByName(serverUrl, teamName);
799+
const fetchRequest = teamId ? await fetchTeamById(serverUrl, teamId) : await fetchTeamByName(serverUrl, teamName);
789800
if (!fetchRequest.team) {
790801
onError(joinedTeam);
791802
return {error: fetchRequest.error || 'no team received'};
@@ -800,11 +811,14 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
800811
}
801812
}
802813

803-
const channel = await getChannelByName(database, teamId, channelName);
814+
const channel = channelId ? await getChannelById(database, channelId) : await getChannelByName(database, teamId, channelName);
804815
const isChannelMember = channel ? await getMyChannel(database, channel.id) : false;
805-
let channelId = channel?.id || '';
816+
if (!channelId) {
817+
channelId = channel?.id || '';
818+
}
819+
806820
if (!isChannelMember) {
807-
const fetchRequest = await fetchChannelByName(serverUrl, teamId, channelName, true);
821+
const fetchRequest = channelId ? await fetchChannelById(serverUrl, channelId) : await fetchChannelByName(serverUrl, teamId, channelName, true);
808822
if (!fetchRequest.channel) {
809823
onError(joinedTeam, teamId);
810824
return {error: fetchRequest.error || 'cannot fetch channel'};
@@ -818,7 +832,7 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
818832
}
819833

820834
logInfo('joining channel', fetchRequest.channel.display_name, fetchRequest.channel.id);
821-
const joinRequest = await joinChannel(serverUrl, teamId, undefined, channelName, false);
835+
const joinRequest = await joinChannel(serverUrl, teamId, channelId, channelName, false);
822836
if (!joinRequest.channel) {
823837
onError(joinedTeam, teamId);
824838
return {error: joinRequest.error || 'no channel returned from join'};
@@ -827,7 +841,7 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
827841
channelId = fetchRequest.channel.id;
828842
}
829843

830-
switchToChannelById(serverUrl, channelId, teamId);
844+
await switchToChannelById(serverUrl, channelId, teamId);
831845
return {};
832846
} catch (error) {
833847
onError(joinedTeam, teamId);

app/actions/remote/team.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
sendEmailInvitesToTeam,
1717
fetchMyTeams,
1818
fetchMyTeam,
19+
fetchTeamById,
1920
fetchAllTeams,
2021
fetchTeamsForComponent,
2122
updateCanJoinTeams,
@@ -251,6 +252,14 @@ describe('teams', () => {
251252
expect(result.memberships).toBeDefined();
252253
});
253254

255+
it('fetchTeamById - base case', async () => {
256+
const result = await fetchTeamById(serverUrl, teamId);
257+
expect(result).toBeDefined();
258+
expect(result.team).toBeDefined();
259+
expect(result.team?.id).toBe(teamId);
260+
expect(mockClient.getTeam).toHaveBeenCalledWith(teamId);
261+
});
262+
254263
it('fetchAllTeams - base case', async () => {
255264
const result = await fetchAllTeams(serverUrl);
256265
expect(result).toBeDefined();

app/actions/remote/team.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,18 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLa
205205
}
206206
}
207207

208+
export async function fetchTeamById(serverUrl: string, teamId: string) {
209+
try {
210+
const client = NetworkManager.getClient(serverUrl);
211+
const team = await client.getTeam(teamId);
212+
return {team};
213+
} catch (error) {
214+
logDebug('error on fetchTeamById', getFullErrorMessage(error));
215+
forceLogoutIfNecessary(serverUrl, error);
216+
return {error};
217+
}
218+
}
219+
208220
export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyTeamsRequest> {
209221
try {
210222
const client = NetworkManager.getClient(serverUrl);

app/products/playbooks/actions/remote/runs.test.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {getLastPlaybookRunsFetchAt} from '@playbooks/database/queries/run';
1010
import EphemeralStore from '@store/ephemeral_store';
1111
import TestHelper from '@test/test_helper';
1212

13-
import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, setOwner, finishRun} from './runs';
13+
import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, fetchPlaybookRunsPageForParticipant, setOwner, finishRun} from './runs';
1414

1515
const serverUrl = 'baseHandler.test.com';
1616
const channelId = 'channel-id-1';
@@ -320,3 +320,98 @@ describe('finishRun', () => {
320320
expect(mockClient.finishRun).toHaveBeenCalledWith('');
321321
});
322322
});
323+
324+
describe('fetchPlaybookRunsPageForParticipant', () => {
325+
const participantId = 'participant-id-1';
326+
327+
beforeEach(() => {
328+
jest.clearAllMocks();
329+
});
330+
331+
it('should fetch single page of playbook runs for participant successfully', async () => {
332+
const mockRuns = [mockPlaybookRun, mockPlaybookRun2];
333+
mockClient.fetchPlaybookRuns.mockResolvedValue({
334+
items: mockRuns,
335+
has_more: false,
336+
});
337+
338+
const result = await fetchPlaybookRunsPageForParticipant(serverUrl, participantId);
339+
340+
expect(result).toBeDefined();
341+
expect(result.error).toBeUndefined();
342+
expect(result.runs).toEqual(mockRuns);
343+
expect(result.hasMore).toBe(false);
344+
expect(mockClient.fetchPlaybookRuns).toHaveBeenCalledWith({
345+
page: 0,
346+
per_page: PER_PAGE_DEFAULT,
347+
participant_id: participantId,
348+
sort: 'create_at',
349+
direction: 'desc',
350+
});
351+
});
352+
353+
it('should handle pagination with has_more = true', async () => {
354+
const mockRuns = [mockPlaybookRun];
355+
mockClient.fetchPlaybookRuns.mockResolvedValue({
356+
items: mockRuns,
357+
has_more: true,
358+
});
359+
360+
const result = await fetchPlaybookRunsPageForParticipant(serverUrl, participantId, 2);
361+
362+
expect(result).toBeDefined();
363+
expect(result.error).toBeUndefined();
364+
expect(result.runs).toEqual(mockRuns);
365+
expect(result.hasMore).toBe(true);
366+
expect(mockClient.fetchPlaybookRuns).toHaveBeenCalledWith({
367+
page: 2,
368+
per_page: PER_PAGE_DEFAULT,
369+
participant_id: participantId,
370+
sort: 'create_at',
371+
direction: 'desc',
372+
});
373+
});
374+
375+
it('should handle network error', async () => {
376+
mockClient.fetchPlaybookRuns.mockRejectedValue(new Error('Network error'));
377+
378+
const result = await fetchPlaybookRunsPageForParticipant(serverUrl, participantId);
379+
380+
expect(result).toBeDefined();
381+
expect(result.error).toBeDefined();
382+
expect(result.runs).toBeUndefined();
383+
expect(result.hasMore).toBeUndefined();
384+
});
385+
386+
it('should handle empty results', async () => {
387+
mockClient.fetchPlaybookRuns.mockResolvedValue({
388+
items: [],
389+
has_more: false,
390+
});
391+
392+
const result = await fetchPlaybookRunsPageForParticipant(serverUrl, participantId);
393+
394+
expect(result).toBeDefined();
395+
expect(result.error).toBeUndefined();
396+
expect(result.runs).toEqual([]);
397+
expect(result.hasMore).toBe(false);
398+
});
399+
400+
it('should default to page 0 when no page is provided', async () => {
401+
const mockRuns = [mockPlaybookRun];
402+
mockClient.fetchPlaybookRuns.mockResolvedValue({
403+
items: mockRuns,
404+
has_more: false,
405+
});
406+
407+
await fetchPlaybookRunsPageForParticipant(serverUrl, participantId);
408+
409+
expect(mockClient.fetchPlaybookRuns).toHaveBeenCalledWith({
410+
page: 0,
411+
per_page: PER_PAGE_DEFAULT,
412+
participant_id: participantId,
413+
sort: 'create_at',
414+
direction: 'desc',
415+
});
416+
});
417+
});

app/products/playbooks/actions/remote/runs.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,23 @@ export const finishRun = async (serverUrl: string, playbookRunId: string) => {
157157
return {error};
158158
}
159159
};
160+
161+
export const fetchPlaybookRunsPageForParticipant = async (serverUrl: string, participantId: string, page = 0) => {
162+
try {
163+
const client = NetworkManager.getClient(serverUrl);
164+
165+
const {items: runs, has_more} = await client.fetchPlaybookRuns({
166+
page,
167+
per_page: PER_PAGE_DEFAULT,
168+
participant_id: participantId,
169+
sort: 'create_at',
170+
direction: 'desc',
171+
});
172+
173+
return {runs, hasMore: has_more};
174+
} catch (error) {
175+
logDebug('error on fetchPlaybookRunsPageForParticipant', getFullErrorMessage(error));
176+
forceLogoutIfNecessary(serverUrl, error);
177+
return {error};
178+
}
179+
};

app/products/playbooks/client/rest.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,10 @@ describe('fetchPlaybookRuns', () => {
5656

5757
test('should return default response when doFetch throws error', async () => {
5858
const params = {team_id: 'team1', page: 0, per_page: 10};
59-
const expectedResponse = {items: [], total_count: 0, page_count: 0, has_more: false};
6059

6160
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
6261

63-
const result = await client.fetchPlaybookRuns(params);
64-
65-
expect(result).toEqual(expectedResponse);
62+
expect(client.fetchPlaybookRuns(params)).rejects.toThrow('Network error');
6663
});
6764
});
6865

app/products/playbooks/client/rest.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,11 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
5050
fetchPlaybookRuns = async (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => {
5151
const queryParams = buildQueryString(params);
5252

53-
try {
54-
const data = await this.doFetch(
55-
`${this.getPlaybookRunsRoute()}${queryParams}`,
56-
{method: 'get', groupLabel},
57-
);
58-
return data || {items: [], total_count: 0, page_count: 0, has_more: false};
59-
} catch (error) {
60-
return {items: [], total_count: 0, page_count: 0, has_more: false};
61-
}
53+
const data = await this.doFetch(
54+
`${this.getPlaybookRunsRoute()}${queryParams}`,
55+
{method: 'get', groupLabel},
56+
);
57+
return data || {items: [], total_count: 0, page_count: 0, has_more: false};
6258
};
6359

6460
fetchPlaybookRun = async (id: string, groupLabel?: RequestGroupLabel) => {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2+
// See LICENSE.txt for license information.
3+
4+
import PlaybooksButton from './playbooks_button';
5+
6+
export default PlaybooksButton;

0 commit comments

Comments
 (0)