Skip to content

Commit 468b271

Browse files
authored
Add create run (#9203)
* Add playbook start a run * UI fixes * Add i18n * Address feedback * Fix typo
1 parent 9272efe commit 468b271

File tree

17 files changed

+1022
-11
lines changed

17 files changed

+1022
-11
lines changed

app/components/floating_input/floating_autocomplete_selector/floating_autocomplete_selector.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,11 @@ function AutoCompleteSelector({
172172
Promise.all(namePromises).then((names) => {
173173
setItemText(names.join(', '));
174174
});
175-
}, [dataSource, teammateNameDisplay, intl, options, selected, serverUrl]);
175+
176+
// We want to run this only in the first render, since it is only for the default value.
177+
// Future changes in the selected value will update the itemText accordingly.
178+
// eslint-disable-next-line react-hooks/exhaustive-deps
179+
}, []);
176180

177181
const inputStyle = useMemo(() => {
178182
const res: StyleProp<ViewStyle> = [style.input];
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2+
// See LICENSE.txt for license information.
3+
4+
import {forceLogoutIfNecessary} from '@actions/remote/session';
5+
import NetworkManager from '@managers/network_manager';
6+
import {getFullErrorMessage} from '@utils/errors';
7+
import {logDebug} from '@utils/log';
8+
9+
export async function fetchPlaybooks(serverUrl: string, params: FetchPlaybooksParams) {
10+
try {
11+
const client = NetworkManager.getClient(serverUrl);
12+
const playbooks = await client.fetchPlaybooks(params);
13+
return {data: playbooks};
14+
} catch (error) {
15+
logDebug('error on fetchPlaybooks', getFullErrorMessage(error));
16+
forceLogoutIfNecessary(serverUrl, error);
17+
return {error};
18+
}
19+
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,27 @@ export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId
136136
}
137137
};
138138

139+
export const createPlaybookRun = async (
140+
serverUrl: string,
141+
playbook_id: string,
142+
owner_user_id: string,
143+
team_id: string,
144+
name: string,
145+
description: string,
146+
channel_id?: string,
147+
create_public_run?: boolean,
148+
) => {
149+
try {
150+
const client = NetworkManager.getClient(serverUrl);
151+
const run = await client.createPlaybookRun(playbook_id, owner_user_id, team_id, name, description, channel_id, create_public_run);
152+
return {data: run};
153+
} catch (error) {
154+
logDebug('error on createPlaybookRun', getFullErrorMessage(error));
155+
forceLogoutIfNecessary(serverUrl, error);
156+
return {error};
157+
}
158+
};
159+
139160
export const postStatusUpdate = async (serverUrl: string, playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => {
140161
try {
141162
const client = NetworkManager.getClient(serverUrl);

app/products/playbooks/client/rest.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ import type ClientBase from '@client/rest/base';
77

88
export interface ClientPlaybooksMix {
99

10+
// Playbooks
11+
fetchPlaybooks: (params: FetchPlaybooksParams) => Promise<FetchPlaybooksReturn>;
12+
1013
// Playbook Runs
1114
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
1215
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
1316
fetchPlaybookRunMetadata: (id: string) => Promise<PlaybookRunMetadata>;
1417
setOwner: (playbookRunId: string, ownerId: string) => Promise<void>;
1518

1619
// Run Management
17-
// finishRun: (playbookRunId: string) => Promise<any>;
1820
finishRun: (playbookRunId: string) => Promise<void>;
21+
createPlaybookRun: (playbook_id: string, owner_user_id: string, team_id: string, name: string, description: string, channel_id?: string, create_public_run?: boolean) => Promise<PlaybookRun>;
1922
postStatusUpdate: (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => Promise<void>;
2023

2124
// Checklist Management
@@ -46,6 +49,17 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
4649
return `${this.getPlaybookRunsRoute()}/${runId}`;
4750
};
4851

52+
// Playbooks
53+
fetchPlaybooks(params: FetchPlaybooksParams) {
54+
const queryParams = buildQueryString({
55+
...params,
56+
});
57+
return this.doFetch(
58+
`${this.getPlaybooksRoute()}/playbooks${queryParams}`,
59+
{method: 'get'},
60+
);
61+
}
62+
4963
// Playbook Runs
5064
fetchPlaybookRuns = async (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => {
5165
const queryParams = buildQueryString(params);
@@ -87,6 +101,30 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
87101
);
88102
};
89103

104+
createPlaybookRun = async (
105+
playbook_id: string,
106+
owner_user_id: string,
107+
team_id: string,
108+
name: string,
109+
description: string,
110+
channel_id?: string,
111+
create_public_run?: boolean,
112+
) => {
113+
const data = await this.doFetch(`${this.getPlaybookRunsRoute()}`, {
114+
method: 'post',
115+
body: {
116+
owner_user_id,
117+
team_id,
118+
name,
119+
description,
120+
playbook_id,
121+
channel_id,
122+
create_public_run,
123+
},
124+
});
125+
return data;
126+
};
127+
90128
postStatusUpdate = async (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => {
91129
const body = {
92130
type: 'dialog_submission',

app/products/playbooks/constants/screens.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand';
88
export const PLAYBOOK_POST_UPDATE = 'PlaybookPostUpdate';
99
export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser';
1010
export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate';
11+
export const PLAYBOOKS_SELECT_PLAYBOOK = 'PlaybooksSelectPlaybook';
12+
export const PLAYBOOKS_START_A_RUN = 'PlaybooksStartARun';
1113

1214
export default {
1315
PLAYBOOKS_RUNS,
@@ -17,4 +19,6 @@ export default {
1719
PLAYBOOK_POST_UPDATE,
1820
PLAYBOOK_SELECT_USER,
1921
PLAYBOOKS_SELECT_DATE,
22+
PLAYBOOKS_SELECT_PLAYBOOK,
23+
PLAYBOOKS_START_A_RUN,
2024
} as const;

app/products/playbooks/screens/index.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import PlaybookRun from './playbook_run';
1313
import PlaybookRuns from './playbooks_runs';
1414
import PostUpdate from './post_update';
1515
import SelectDate from './select_date';
16+
import SelectPlaybook from './select_playbook';
1617
import SelectUser from './select_user';
18+
import StartARun from './start_a_run';
1719

1820
import {loadPlaybooksScreen} from '.';
1921

@@ -54,6 +56,18 @@ jest.mock('@playbooks/screens/select_date', () => ({
5456
}));
5557
jest.mocked(SelectDate).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_SELECT_DATE}</Text>);
5658

59+
jest.mock('@playbooks/screens/start_a_run', () => ({
60+
__esModule: true,
61+
default: jest.fn(),
62+
}));
63+
jest.mocked(StartARun).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_START_A_RUN}</Text>);
64+
65+
jest.mock('@playbooks/screens/select_playbook', () => ({
66+
__esModule: true,
67+
default: jest.fn(),
68+
}));
69+
jest.mocked(SelectPlaybook).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_SELECT_PLAYBOOK}</Text>);
70+
5771
jest.mock('@playbooks/screens/participant_playbooks', () => ({
5872
__esModule: true,
5973
default: jest.fn(),

app/products/playbooks/screens/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function loadPlaybooksScreen(screenName: string | number) {
2020
return withServerDatabase(require('@playbooks/screens/select_user').default);
2121
case Screens.PLAYBOOKS_SELECT_DATE:
2222
return withServerDatabase(require('@playbooks/screens/select_date').default);
23+
case Screens.PLAYBOOKS_SELECT_PLAYBOOK:
24+
return withServerDatabase(require('@playbooks/screens/select_playbook').default);
25+
case Screens.PLAYBOOKS_START_A_RUN:
26+
return withServerDatabase(require('@playbooks/screens/start_a_run').default);
2327
default:
2428
return undefined;
2529
}

app/products/playbooks/screens/navigation.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,31 @@ export async function goToSelectDate(
116116
selectedDate,
117117
}, options);
118118
}
119+
120+
export async function goToSelectPlaybook(
121+
intl: IntlShape,
122+
theme: Theme,
123+
) {
124+
const title = intl.formatMessage({id: 'playbooks.select_playbook.title', defaultMessage: 'Start a run'});
125+
goToScreen(Screens.PLAYBOOKS_SELECT_PLAYBOOK, title, {}, {
126+
topBar: {
127+
subtitle: {
128+
text: intl.formatMessage({id: 'playbooks.select_playbook.subtitle', defaultMessage: 'Select a playbook'}),
129+
color: changeOpacity(theme.sidebarText, 0.72),
130+
},
131+
},
132+
});
133+
}
134+
135+
export async function goToStartARun(intl: IntlShape, theme: Theme, playbook: Playbook, onRunCreated: (run: PlaybookRun) => void) {
136+
const title = intl.formatMessage({id: 'playbooks.start_a_run.title', defaultMessage: 'Start a run'});
137+
const subtitle = playbook.title;
138+
goToScreen(Screens.PLAYBOOKS_START_A_RUN, title, {playbook, onRunCreated}, {
139+
topBar: {
140+
subtitle: {
141+
text: subtitle,
142+
color: changeOpacity(theme.sidebarText, 0.72),
143+
},
144+
},
145+
});
146+
}

app/products/playbooks/screens/playbooks_runs/playbook_runs.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33

44
import {FlashList, type ListRenderItem} from '@shopify/flash-list';
55
import React, {useCallback, useMemo, useState} from 'react';
6-
import {defineMessage} from 'react-intl';
6+
import {defineMessage, useIntl} from 'react-intl';
77
import {StyleSheet, View} from 'react-native';
88

9+
import Button from '@components/button';
910
import {Screens} from '@constants';
1011
import {useTheme} from '@context/theme';
1112
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@@ -15,6 +16,8 @@ import {isRunFinished} from '@playbooks/utils/run';
1516
import {popTopScreen} from '@screens/navigation';
1617
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
1718

19+
import {goToSelectPlaybook} from '../navigation';
20+
1821
import EmptyState from './empty_state';
1922
import PlaybookCard, {CARD_HEIGHT} from './playbook_card';
2023
import ShowMoreButton from './show_more_button';
@@ -43,6 +46,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => ({
4346
borderBottomWidth: 1,
4447
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.12),
4548
},
49+
startANewRunButtonContainer: {
50+
padding: 20,
51+
},
4652
}));
4753

4854
const ItemSeparator = () => {
@@ -71,6 +77,7 @@ const PlaybookRuns = ({
7177
allRuns,
7278
componentId,
7379
}: Props) => {
80+
const intl = useIntl();
7481
const theme = useTheme();
7582
const styles = getStyleFromTheme(theme);
7683

@@ -132,17 +139,32 @@ const PlaybookRuns = ({
132139
);
133140
}, []);
134141

142+
const startANewRun = useCallback(() => {
143+
goToSelectPlaybook(intl, theme);
144+
}, [intl, theme]);
145+
135146
let content = (<EmptyState tab={activeTab}/>);
136147
if (!isEmpty) {
137148
content = (
138-
<FlashList
139-
data={data}
140-
renderItem={renderItem}
141-
contentContainerStyle={styles.container}
142-
ItemSeparatorComponent={ItemSeparator}
143-
estimatedItemSize={CARD_HEIGHT}
144-
ListFooterComponent={footerComponent}
145-
/>
149+
<>
150+
<FlashList
151+
data={data}
152+
renderItem={renderItem}
153+
contentContainerStyle={styles.container}
154+
ItemSeparatorComponent={ItemSeparator}
155+
estimatedItemSize={CARD_HEIGHT}
156+
ListFooterComponent={footerComponent}
157+
/>
158+
<View style={styles.startANewRunButtonContainer}>
159+
<Button
160+
emphasis='tertiary'
161+
onPress={startANewRun}
162+
text={intl.formatMessage({id: 'playbooks.runs.start_a_new_run', defaultMessage: 'Start a new run'})}
163+
size='lg'
164+
theme={theme}
165+
/>
166+
</View>
167+
</>
146168
);
147169
}
148170

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2+
// See LICENSE.txt for license information.
3+
4+
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
5+
import {of as of$} from 'rxjs';
6+
import {switchMap} from 'rxjs/operators';
7+
8+
import {queryPlaybookRunsPerChannel} from '@playbooks/database/queries/run';
9+
import {observeCurrentUserId, observeCurrentTeamId, observeCurrentChannelId} from '@queries/servers/system';
10+
11+
import SelectPlaybook from './select_playbook';
12+
13+
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
14+
import type {WithDatabaseArgs} from '@typings/database/database';
15+
16+
function getPlaybookIdsFromRuns(runs: PlaybookRunModel[]) {
17+
return runs.reduce((acc, run) => {
18+
acc.add(run.playbookId);
19+
return acc;
20+
}, new Set<string>());
21+
}
22+
23+
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
24+
const playbooksUsedInChannel = observeCurrentChannelId(database).pipe(
25+
switchMap((id) => (id ? queryPlaybookRunsPerChannel(database, id).observe() : of$([]))),
26+
switchMap((runs) => of$(getPlaybookIdsFromRuns(runs))),
27+
);
28+
return {
29+
currentUserId: observeCurrentUserId(database),
30+
currentTeamId: observeCurrentTeamId(database),
31+
playbooksUsedInChannel,
32+
};
33+
});
34+
35+
export default withDatabase(enhanced(SelectPlaybook));

0 commit comments

Comments
 (0)