Add create run (#9203)
* Add playbook start a run * UI fixes * Add i18n * Address feedback * Fix typo
This commit is contained in:
parent
9272efe2a8
commit
468b271928
17 changed files with 1022 additions and 11 deletions
|
|
@ -172,7 +172,11 @@ function AutoCompleteSelector({
|
|||
Promise.all(namePromises).then((names) => {
|
||||
setItemText(names.join(', '));
|
||||
});
|
||||
}, [dataSource, teammateNameDisplay, intl, options, selected, serverUrl]);
|
||||
|
||||
// We want to run this only in the first render, since it is only for the default value.
|
||||
// Future changes in the selected value will update the itemText accordingly.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const inputStyle = useMemo(() => {
|
||||
const res: StyleProp<ViewStyle> = [style.input];
|
||||
|
|
|
|||
19
app/products/playbooks/actions/remote/playbooks.ts
Normal file
19
app/products/playbooks/actions/remote/playbooks.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
export async function fetchPlaybooks(serverUrl: string, params: FetchPlaybooksParams) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const playbooks = await client.fetchPlaybooks(params);
|
||||
return {data: playbooks};
|
||||
} catch (error) {
|
||||
logDebug('error on fetchPlaybooks', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +136,27 @@ export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId
|
|||
}
|
||||
};
|
||||
|
||||
export const createPlaybookRun = async (
|
||||
serverUrl: string,
|
||||
playbook_id: string,
|
||||
owner_user_id: string,
|
||||
team_id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
channel_id?: string,
|
||||
create_public_run?: boolean,
|
||||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const run = await client.createPlaybookRun(playbook_id, owner_user_id, team_id, name, description, channel_id, create_public_run);
|
||||
return {data: run};
|
||||
} catch (error) {
|
||||
logDebug('error on createPlaybookRun', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const postStatusUpdate = async (serverUrl: string, playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import type ClientBase from '@client/rest/base';
|
|||
|
||||
export interface ClientPlaybooksMix {
|
||||
|
||||
// Playbooks
|
||||
fetchPlaybooks: (params: FetchPlaybooksParams) => Promise<FetchPlaybooksReturn>;
|
||||
|
||||
// Playbook Runs
|
||||
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
|
||||
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
||||
|
|
@ -14,8 +17,8 @@ export interface ClientPlaybooksMix {
|
|||
setOwner: (playbookRunId: string, ownerId: string) => Promise<void>;
|
||||
|
||||
// Run Management
|
||||
// finishRun: (playbookRunId: string) => Promise<any>;
|
||||
finishRun: (playbookRunId: string) => Promise<void>;
|
||||
createPlaybookRun: (playbook_id: string, owner_user_id: string, team_id: string, name: string, description: string, channel_id?: string, create_public_run?: boolean) => Promise<PlaybookRun>;
|
||||
postStatusUpdate: (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => Promise<void>;
|
||||
|
||||
// Checklist Management
|
||||
|
|
@ -46,6 +49,17 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
return `${this.getPlaybookRunsRoute()}/${runId}`;
|
||||
};
|
||||
|
||||
// Playbooks
|
||||
fetchPlaybooks(params: FetchPlaybooksParams) {
|
||||
const queryParams = buildQueryString({
|
||||
...params,
|
||||
});
|
||||
return this.doFetch(
|
||||
`${this.getPlaybooksRoute()}/playbooks${queryParams}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
|
||||
// Playbook Runs
|
||||
fetchPlaybookRuns = async (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => {
|
||||
const queryParams = buildQueryString(params);
|
||||
|
|
@ -87,6 +101,30 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
);
|
||||
};
|
||||
|
||||
createPlaybookRun = async (
|
||||
playbook_id: string,
|
||||
owner_user_id: string,
|
||||
team_id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
channel_id?: string,
|
||||
create_public_run?: boolean,
|
||||
) => {
|
||||
const data = await this.doFetch(`${this.getPlaybookRunsRoute()}`, {
|
||||
method: 'post',
|
||||
body: {
|
||||
owner_user_id,
|
||||
team_id,
|
||||
name,
|
||||
description,
|
||||
playbook_id,
|
||||
channel_id,
|
||||
create_public_run,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
postStatusUpdate = async (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => {
|
||||
const body = {
|
||||
type: 'dialog_submission',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand';
|
|||
export const PLAYBOOK_POST_UPDATE = 'PlaybookPostUpdate';
|
||||
export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser';
|
||||
export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate';
|
||||
export const PLAYBOOKS_SELECT_PLAYBOOK = 'PlaybooksSelectPlaybook';
|
||||
export const PLAYBOOKS_START_A_RUN = 'PlaybooksStartARun';
|
||||
|
||||
export default {
|
||||
PLAYBOOKS_RUNS,
|
||||
|
|
@ -17,4 +19,6 @@ export default {
|
|||
PLAYBOOK_POST_UPDATE,
|
||||
PLAYBOOK_SELECT_USER,
|
||||
PLAYBOOKS_SELECT_DATE,
|
||||
PLAYBOOKS_SELECT_PLAYBOOK,
|
||||
PLAYBOOKS_START_A_RUN,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import PlaybookRun from './playbook_run';
|
|||
import PlaybookRuns from './playbooks_runs';
|
||||
import PostUpdate from './post_update';
|
||||
import SelectDate from './select_date';
|
||||
import SelectPlaybook from './select_playbook';
|
||||
import SelectUser from './select_user';
|
||||
import StartARun from './start_a_run';
|
||||
|
||||
import {loadPlaybooksScreen} from '.';
|
||||
|
||||
|
|
@ -54,6 +56,18 @@ jest.mock('@playbooks/screens/select_date', () => ({
|
|||
}));
|
||||
jest.mocked(SelectDate).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_SELECT_DATE}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/start_a_run', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(StartARun).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_START_A_RUN}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/select_playbook', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(SelectPlaybook).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_SELECT_PLAYBOOK}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/participant_playbooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ export function loadPlaybooksScreen(screenName: string | number) {
|
|||
return withServerDatabase(require('@playbooks/screens/select_user').default);
|
||||
case Screens.PLAYBOOKS_SELECT_DATE:
|
||||
return withServerDatabase(require('@playbooks/screens/select_date').default);
|
||||
case Screens.PLAYBOOKS_SELECT_PLAYBOOK:
|
||||
return withServerDatabase(require('@playbooks/screens/select_playbook').default);
|
||||
case Screens.PLAYBOOKS_START_A_RUN:
|
||||
return withServerDatabase(require('@playbooks/screens/start_a_run').default);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,3 +116,31 @@ export async function goToSelectDate(
|
|||
selectedDate,
|
||||
}, options);
|
||||
}
|
||||
|
||||
export async function goToSelectPlaybook(
|
||||
intl: IntlShape,
|
||||
theme: Theme,
|
||||
) {
|
||||
const title = intl.formatMessage({id: 'playbooks.select_playbook.title', defaultMessage: 'Start a run'});
|
||||
goToScreen(Screens.PLAYBOOKS_SELECT_PLAYBOOK, title, {}, {
|
||||
topBar: {
|
||||
subtitle: {
|
||||
text: intl.formatMessage({id: 'playbooks.select_playbook.subtitle', defaultMessage: 'Select a playbook'}),
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function goToStartARun(intl: IntlShape, theme: Theme, playbook: Playbook, onRunCreated: (run: PlaybookRun) => void) {
|
||||
const title = intl.formatMessage({id: 'playbooks.start_a_run.title', defaultMessage: 'Start a run'});
|
||||
const subtitle = playbook.title;
|
||||
goToScreen(Screens.PLAYBOOKS_START_A_RUN, title, {playbook, onRunCreated}, {
|
||||
topBar: {
|
||||
subtitle: {
|
||||
text: subtitle,
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
import {FlashList, type ListRenderItem} from '@shopify/flash-list';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {defineMessage} from 'react-intl';
|
||||
import {defineMessage, useIntl} from 'react-intl';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import Button from '@components/button';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
|
|
@ -15,6 +16,8 @@ import {isRunFinished} from '@playbooks/utils/run';
|
|||
import {popTopScreen} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {goToSelectPlaybook} from '../navigation';
|
||||
|
||||
import EmptyState from './empty_state';
|
||||
import PlaybookCard, {CARD_HEIGHT} from './playbook_card';
|
||||
import ShowMoreButton from './show_more_button';
|
||||
|
|
@ -43,6 +46,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
borderBottomWidth: 1,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.12),
|
||||
},
|
||||
startANewRunButtonContainer: {
|
||||
padding: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
const ItemSeparator = () => {
|
||||
|
|
@ -71,6 +77,7 @@ const PlaybookRuns = ({
|
|||
allRuns,
|
||||
componentId,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
|
|
@ -132,17 +139,32 @@ const PlaybookRuns = ({
|
|||
);
|
||||
}, []);
|
||||
|
||||
const startANewRun = useCallback(() => {
|
||||
goToSelectPlaybook(intl, theme);
|
||||
}, [intl, theme]);
|
||||
|
||||
let content = (<EmptyState tab={activeTab}/>);
|
||||
if (!isEmpty) {
|
||||
content = (
|
||||
<FlashList
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.container}
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
estimatedItemSize={CARD_HEIGHT}
|
||||
ListFooterComponent={footerComponent}
|
||||
/>
|
||||
<>
|
||||
<FlashList
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.container}
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
estimatedItemSize={CARD_HEIGHT}
|
||||
ListFooterComponent={footerComponent}
|
||||
/>
|
||||
<View style={styles.startANewRunButtonContainer}>
|
||||
<Button
|
||||
emphasis='tertiary'
|
||||
onPress={startANewRun}
|
||||
text={intl.formatMessage({id: 'playbooks.runs.start_a_new_run', defaultMessage: 'Start a new run'})}
|
||||
size='lg'
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
35
app/products/playbooks/screens/select_playbook/index.ts
Normal file
35
app/products/playbooks/screens/select_playbook/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryPlaybookRunsPerChannel} from '@playbooks/database/queries/run';
|
||||
import {observeCurrentUserId, observeCurrentTeamId, observeCurrentChannelId} from '@queries/servers/system';
|
||||
|
||||
import SelectPlaybook from './select_playbook';
|
||||
|
||||
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
function getPlaybookIdsFromRuns(runs: PlaybookRunModel[]) {
|
||||
return runs.reduce((acc, run) => {
|
||||
acc.add(run.playbookId);
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const playbooksUsedInChannel = observeCurrentChannelId(database).pipe(
|
||||
switchMap((id) => (id ? queryPlaybookRunsPerChannel(database, id).observe() : of$([]))),
|
||||
switchMap((runs) => of$(getPlaybookIdsFromRuns(runs))),
|
||||
);
|
||||
return {
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
playbooksUsedInChannel,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(SelectPlaybook));
|
||||
116
app/products/playbooks/screens/select_playbook/playbook_row.tsx
Normal file
116
app/products/playbooks/screens/select_playbook/playbook_row.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, Text, TouchableOpacity} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {getFriendlyDate} from '@components/friendly_date';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
playbook: Playbook;
|
||||
onPress?: (playbook: Playbook) => void;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
statusText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 75, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const PlaybookRow = ({playbook, onPress, testID}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const handlePress = () => {
|
||||
onPress?.(playbook);
|
||||
};
|
||||
|
||||
const formatLastUsed = (lastRunAt: number) => {
|
||||
if (!lastRunAt) {
|
||||
return intl.formatMessage({
|
||||
id: 'playbooks.row.never_used',
|
||||
defaultMessage: 'Never used',
|
||||
});
|
||||
}
|
||||
|
||||
const formattedTime = getFriendlyDate(intl, lastRunAt);
|
||||
|
||||
return intl.formatMessage({
|
||||
id: 'playbooks.row.last_used',
|
||||
defaultMessage: 'Last used {time}',
|
||||
}, {time: formattedTime});
|
||||
};
|
||||
|
||||
const formatRunsInProgress = (activeRuns: number) => {
|
||||
if (activeRuns === 0) {
|
||||
return intl.formatMessage({
|
||||
id: 'playbooks.row.no_runs',
|
||||
defaultMessage: 'No runs in progress',
|
||||
});
|
||||
}
|
||||
return intl.formatMessage({
|
||||
id: 'playbooks.row.runs',
|
||||
defaultMessage: '{count} {count, plural, one {run} other {runs}} in progress',
|
||||
}, {count: activeRuns});
|
||||
};
|
||||
|
||||
const statusText = `${formatLastUsed(playbook.last_run_at)} • ${formatRunsInProgress(playbook.active_runs)}`;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={styles.container}
|
||||
testID={testID}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<CompassIcon
|
||||
name={playbook.public ? 'book-outline' : 'book-lock-outline'}
|
||||
size={24}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
/>
|
||||
<View style={styles.contentContainer}>
|
||||
<Text
|
||||
style={styles.title}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{playbook.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.statusText}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{statusText}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaybookRow;
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, SectionList, Text, View, type DefaultSectionT, type ListRenderItemInfo, type SectionListData} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import SearchBar from '@components/search';
|
||||
import {General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {fetchPlaybooks} from '@playbooks/actions/remote/playbooks';
|
||||
import {fetchPlaybookRunsForChannel} from '@playbooks/actions/remote/runs';
|
||||
import {
|
||||
popTo,
|
||||
popTopScreen,
|
||||
} from '@screens/navigation';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import {goToPlaybookRun, goToStartARun} from '../navigation';
|
||||
|
||||
import PlaybookRow from './playbook_row';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const close = () => {
|
||||
popTopScreen();
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
currentTeamId: string;
|
||||
currentUserId: string;
|
||||
componentId: AvailableScreens;
|
||||
playbooksUsedInChannel: Set<string>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 24,
|
||||
gap: 24,
|
||||
},
|
||||
searchBar: {
|
||||
marginVertical: 5,
|
||||
height: 38,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
noResultContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
...typography('Body', 600, 'Regular'),
|
||||
},
|
||||
searchBarInput: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 0,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
},
|
||||
sectionHeader: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const EMPTY_DATA: Playbook[] = [];
|
||||
|
||||
function SelectPlaybook({
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
componentId,
|
||||
playbooksUsedInChannel,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
// HOOKS
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [searching, setSearching] = useState<boolean>(false);
|
||||
const [term, setTerm] = useState<string>('');
|
||||
const [data, setData] = useState<Playbook[]>([]);
|
||||
const [searchResults, setSearchResults] = useState<Playbook[]>([]);
|
||||
|
||||
const page = useRef<number>(-1);
|
||||
const next = useRef<boolean>(true);
|
||||
|
||||
// Callbacks
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
setSearchResults([]);
|
||||
setSearching(false);
|
||||
}, []);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (next.current && !loading) {
|
||||
setLoading(true);
|
||||
const result = await fetchPlaybooks(serverUrl, {
|
||||
team_id: currentTeamId,
|
||||
page: page.current + 1,
|
||||
});
|
||||
|
||||
if (result.data) {
|
||||
setData((prev) => [...prev, ...result.data.items]);
|
||||
}
|
||||
|
||||
page.current++;
|
||||
next.current = Boolean(result.data?.has_more);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loading, serverUrl, currentTeamId]);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
if (!text) {
|
||||
clearSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
setTerm(text);
|
||||
setSearching(true);
|
||||
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
|
||||
searchTimeoutId.current = setTimeout(async () => {
|
||||
const result = await fetchPlaybooks(serverUrl, {
|
||||
team_id: currentTeamId,
|
||||
search_term: text,
|
||||
sort: 'last_run_at',
|
||||
});
|
||||
|
||||
if (result.data) {
|
||||
setSearchResults(result.data.items);
|
||||
}
|
||||
|
||||
setSearching(false);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
}, [clearSearch, serverUrl, currentTeamId]);
|
||||
|
||||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
searchTimeoutId.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadMore();
|
||||
|
||||
// We only want to load the playbooks once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const renderNoResults = useCallback((): JSX.Element | null => {
|
||||
if (searching || (loading && page.current === -1)) {
|
||||
return (
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
containerStyle={style.loadingContainer}
|
||||
size='large'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='playbooks.create_run.select_playbook.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [loading, searching, style.loadingContainer, style.noResultContainer, style.noResultText, theme.buttonBg]);
|
||||
|
||||
const onRunCreated = useCallback(async (run: PlaybookRun) => {
|
||||
await popTo(Screens.HOME);
|
||||
await fetchPlaybookRunsForChannel(serverUrl, run.channel_id);
|
||||
await switchToChannelById(serverUrl, run.channel_id);
|
||||
await goToPlaybookRun(intl, run.id);
|
||||
}, [intl, serverUrl]);
|
||||
|
||||
const onPress = useCallback((playbook: Playbook) => {
|
||||
goToStartARun(intl, theme, playbook, onRunCreated);
|
||||
}, [intl, onRunCreated, theme]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<Playbook>) => {
|
||||
return (
|
||||
<PlaybookRow
|
||||
playbook={item}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
}, [onPress]);
|
||||
|
||||
const renderLoading = useCallback(() => {
|
||||
if (!loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
containerStyle={style.loadingContainer}
|
||||
size='large'
|
||||
/>
|
||||
);
|
||||
}, [loading, style.loadingContainer, theme.buttonBg]);
|
||||
|
||||
const renderSectionHeader = useCallback(({section}: {section: SectionListData<Playbook, DefaultSectionT>}) => {
|
||||
return (
|
||||
<Text style={style.sectionHeader}>{section.title}</Text>
|
||||
);
|
||||
}, [style.sectionHeader]);
|
||||
|
||||
const sections: Array<SectionListData<Playbook, DefaultSectionT>> = useMemo(() => {
|
||||
type PlaybookSections = {
|
||||
inThisChannel: Playbook[];
|
||||
yourPlaybooks: Playbook[];
|
||||
otherPlaybooks: Playbook[];
|
||||
}
|
||||
const reducedPlaybooks = data.reduce<PlaybookSections>((acc, playbook) => {
|
||||
function isMember(member: PlaybookMember) {
|
||||
return member.user_id === currentUserId;
|
||||
}
|
||||
if (playbooksUsedInChannel.has(playbook.id)) {
|
||||
acc.inThisChannel.push(playbook);
|
||||
} else if (playbook.members.some(isMember)) {
|
||||
acc.yourPlaybooks.push(playbook);
|
||||
} else {
|
||||
acc.otherPlaybooks.push(playbook);
|
||||
}
|
||||
return acc;
|
||||
}, {inThisChannel: [], yourPlaybooks: [], otherPlaybooks: []});
|
||||
const allSections = [
|
||||
{
|
||||
title: intl.formatMessage({id: 'playbooks.select_playbook.in_this_channel', defaultMessage: 'In This Channel'}),
|
||||
data: reducedPlaybooks.inThisChannel,
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({id: 'playbooks.select_playbook.your_playbooks', defaultMessage: 'Your Playbooks'}),
|
||||
data: reducedPlaybooks.yourPlaybooks,
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({id: 'playbooks.select_playbook.other_playbooks', defaultMessage: 'Other Playbooks'}),
|
||||
data: reducedPlaybooks.otherPlaybooks,
|
||||
},
|
||||
];
|
||||
|
||||
return allSections.filter((section) => section.data.length > 0);
|
||||
}, [currentUserId, data, intl, playbooksUsedInChannel]);
|
||||
|
||||
let shownData = EMPTY_DATA;
|
||||
if (!loading) {
|
||||
if (term) {
|
||||
shownData = searchResults;
|
||||
} else {
|
||||
shownData = data;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={style.container}
|
||||
>
|
||||
<View
|
||||
testID='integration_selector.screen'
|
||||
style={style.searchBar}
|
||||
>
|
||||
<SearchBar
|
||||
testID='selector.search_bar'
|
||||
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
inputStyle={style.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
showLoading={searching}
|
||||
/>
|
||||
</View>
|
||||
{term && (
|
||||
<FlatList
|
||||
data={shownData}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={renderNoResults}
|
||||
onEndReached={loadMore}
|
||||
ListFooterComponent={renderLoading}
|
||||
/>
|
||||
)}
|
||||
{!term && (
|
||||
<SectionList
|
||||
sections={sections}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={renderNoResults}
|
||||
onEndReached={loadMore}
|
||||
ListFooterComponent={renderLoading}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectPlaybook;
|
||||
19
app/products/playbooks/screens/start_a_run/index.ts
Normal file
19
app/products/playbooks/screens/start_a_run/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
|
||||
import {observeCurrentUserId, observeCurrentTeamId} from '@queries/servers/system';
|
||||
|
||||
import StartARun from './start_a_run';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(StartARun));
|
||||
280
app/products/playbooks/screens/start_a_run/start_a_run.tsx
Normal file
280
app/products/playbooks/screens/start_a_run/start_a_run.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useCallback, useEffect} from 'react';
|
||||
import {useIntl, type IntlShape} from 'react-intl';
|
||||
import {ScrollView, View} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FloatingAutocompleteSelector from '@components/floating_input/floating_autocomplete_selector';
|
||||
import FloatingTextInput from '@components/floating_input/floating_text_input_label';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {createPlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
import type {OptionsTopBarButton} from 'react-native-navigation';
|
||||
|
||||
type ChannelOption = 'existing' | 'new';
|
||||
|
||||
export type Props = {
|
||||
componentId: AvailableScreens;
|
||||
playbook: Playbook;
|
||||
currentUserId: string;
|
||||
currentTeamId: string;
|
||||
onRunCreated: (run: PlaybookRun) => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 24,
|
||||
},
|
||||
contentContainer: {
|
||||
gap: 16,
|
||||
},
|
||||
channelInput: {
|
||||
marginLeft: 40, // Align with radio button text
|
||||
},
|
||||
channelTypeSelectorSection: {
|
||||
gap: 16,
|
||||
marginLeft: 40,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CLOSE_BUTTON_ID = 'close-start-a-run';
|
||||
const CREATE_BUTTON_ID = 'create-run';
|
||||
|
||||
async function makeLeftButton(theme: Theme): Promise<OptionsTopBarButton> {
|
||||
return {
|
||||
id: CLOSE_BUTTON_ID,
|
||||
icon: await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor),
|
||||
testID: 'start_a_run.close.button',
|
||||
};
|
||||
}
|
||||
|
||||
function makeRightButton(theme: Theme, intl: IntlShape, enabled: boolean): OptionsTopBarButton {
|
||||
return {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
id: CREATE_BUTTON_ID,
|
||||
text: intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}),
|
||||
showAsAction: 'always',
|
||||
testID: 'start_a_run.create.button',
|
||||
enabled,
|
||||
};
|
||||
}
|
||||
|
||||
function StartARun({
|
||||
componentId,
|
||||
playbook,
|
||||
currentUserId,
|
||||
currentTeamId,
|
||||
onRunCreated,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const [runName, setRunName] = useState(() => {
|
||||
if (playbook?.channel_mode === 'create_new_channel') {
|
||||
return playbook.channel_name_template || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const [runDescription, setRunDescription] = useState(() => {
|
||||
if (playbook?.run_summary_template_enabled) {
|
||||
return playbook.run_summary_template || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const [channelOption, setChannelOption] = useState<ChannelOption>('existing');
|
||||
const [channelId, setChannelId] = useState<string | undefined>(undefined);
|
||||
const [createPublicChannel, setCreatePublicChannel] = useState(false);
|
||||
|
||||
const canSave = Boolean(runName.trim());
|
||||
|
||||
const handleStartRun = useCallback(async () => {
|
||||
if (!runName.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await createPlaybookRun(serverUrl, playbook.id, currentUserId, currentTeamId, runName.trim(), runDescription.trim(), channelId, channelOption === 'new' ? createPublicChannel : undefined);
|
||||
if (res.error || !res.data) {
|
||||
logDebug('error on createPlaybookRun', getFullErrorMessage(res.error));
|
||||
showPlaybookErrorSnackbar();
|
||||
return;
|
||||
}
|
||||
await popTopScreen(componentId);
|
||||
onRunCreated(res.data);
|
||||
}, [runName, serverUrl, playbook.id, currentUserId, currentTeamId, runDescription, channelId, channelOption, createPublicChannel, componentId, onRunCreated]);
|
||||
|
||||
useEffect(() => {
|
||||
async function asyncWrapper() {
|
||||
const leftButton = await makeLeftButton(theme);
|
||||
const rightButton = makeRightButton(theme, intl, canSave);
|
||||
|
||||
setButtons(
|
||||
componentId,
|
||||
{
|
||||
leftButtons: [leftButton],
|
||||
rightButtons: [rightButton],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
asyncWrapper();
|
||||
}, [componentId, theme, intl, canSave]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
popTopScreen(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
useNavButtonPressed(CREATE_BUTTON_ID, componentId, handleStartRun, [handleStartRun]);
|
||||
useNavButtonPressed(CLOSE_BUTTON_ID, componentId, close, [close]);
|
||||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
const existingOptionAction = useCallback(() => {
|
||||
setChannelOption('existing');
|
||||
}, []);
|
||||
const newOptionAction = useCallback(() => {
|
||||
setChannelOption('new');
|
||||
}, []);
|
||||
const publicChannelOptionAction = useCallback(() => {
|
||||
setCreatePublicChannel(true);
|
||||
}, []);
|
||||
const privateChannelOptionAction = useCallback(() => {
|
||||
setCreatePublicChannel(false);
|
||||
}, []);
|
||||
|
||||
const onChannelSelected = useCallback((value: SelectedDialogOption) => {
|
||||
if (Array.isArray(value)) {
|
||||
// Multiselect case, should never happen
|
||||
logDebug('on channel selected returned an array, this should never happen', value);
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
// Undefined case, should never happen
|
||||
logDebug('on channel selected returned undefined, this should never happen');
|
||||
return;
|
||||
}
|
||||
setChannelId(value.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
>
|
||||
<FloatingTextInput
|
||||
label={intl.formatMessage({
|
||||
id: 'playbooks.start_run.run_name_label',
|
||||
defaultMessage: 'Run name',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'playbooks.start_run.run_name_placeholder',
|
||||
defaultMessage: 'Add a name for your run',
|
||||
})}
|
||||
value={runName}
|
||||
onChangeText={setRunName}
|
||||
theme={theme}
|
||||
testID='start_run.run_name_input'
|
||||
error={runName.trim() ? undefined : intl.formatMessage({
|
||||
id: 'playbooks.start_run.run_name_error',
|
||||
defaultMessage: 'Please add a name for this run',
|
||||
})}
|
||||
/>
|
||||
<FloatingTextInput
|
||||
label={intl.formatMessage({
|
||||
id: 'playbooks.start_run.run_description_label',
|
||||
defaultMessage: 'Run description',
|
||||
})}
|
||||
value={runDescription}
|
||||
onChangeText={setRunDescription}
|
||||
multiline={true}
|
||||
multilineInputHeight={100}
|
||||
theme={theme}
|
||||
testID='start_run.run_description_input'
|
||||
/>
|
||||
|
||||
<OptionItem
|
||||
label={intl.formatMessage({
|
||||
id: 'playbooks.start_run.link_existing_channel',
|
||||
defaultMessage: 'Link to an existing channel',
|
||||
})}
|
||||
type='radio'
|
||||
selected={channelOption === 'existing'}
|
||||
action={existingOptionAction}
|
||||
testID='start_run.existing_channel_option'
|
||||
/>
|
||||
{channelOption === 'existing' && (
|
||||
<View style={styles.channelInput}>
|
||||
<FloatingAutocompleteSelector
|
||||
label={intl.formatMessage({
|
||||
id: 'playbooks.start_run.channel_label',
|
||||
defaultMessage: 'Channel',
|
||||
})}
|
||||
dataSource='channels'
|
||||
selected={channelId}
|
||||
onSelected={onChannelSelected}
|
||||
testID='start_run.existing_channel_selector'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<OptionItem
|
||||
label={intl.formatMessage({
|
||||
id: 'playbooks.start_run.create_new_channel',
|
||||
defaultMessage: 'Create a new channel',
|
||||
})}
|
||||
type='radio'
|
||||
selected={channelOption === 'new'}
|
||||
action={newOptionAction}
|
||||
testID='start_run.new_channel_option'
|
||||
/>
|
||||
{channelOption === 'new' && (
|
||||
<View style={styles.channelTypeSelectorSection}>
|
||||
<OptionItem
|
||||
label={intl.formatMessage({
|
||||
id: 'playbooks.start_run.create_new_channel.public_channel',
|
||||
defaultMessage: 'Public channel',
|
||||
})}
|
||||
type='radio'
|
||||
selected={createPublicChannel}
|
||||
action={publicChannelOptionAction}
|
||||
testID='start_run.new_channel_public_option'
|
||||
/>
|
||||
<OptionItem
|
||||
label={intl.formatMessage({
|
||||
id: 'playbooks.start_run.create_new_channel.private_channel',
|
||||
defaultMessage: 'Private channel',
|
||||
})}
|
||||
type='radio'
|
||||
selected={!createPublicChannel}
|
||||
action={privateChannelOptionAction}
|
||||
testID='start_run.new_channel_private_option'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default StartARun;
|
||||
30
app/products/playbooks/types/api.d.ts
vendored
30
app/products/playbooks/types/api.d.ts
vendored
|
|
@ -147,3 +147,33 @@ type PlaybookRunMetadata = {
|
|||
total_posts: number;
|
||||
followers: string[];
|
||||
}
|
||||
|
||||
type Playbook = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
team_id: string;
|
||||
create_public_playbook_run: boolean;
|
||||
delete_at: number;
|
||||
run_summary_template_enabled: boolean;
|
||||
run_summary_template: string;
|
||||
channel_name_template: string;
|
||||
channel_mode: string;
|
||||
public: boolean;
|
||||
default_owner_id: string;
|
||||
default_owner_enabled: boolean;
|
||||
num_stages: number;
|
||||
num_steps: number;
|
||||
num_runs: number;
|
||||
num_actions: number;
|
||||
last_run_at: number;
|
||||
members: PlaybookMember[];
|
||||
default_playbook_member_role: string;
|
||||
active_runs: number;
|
||||
}
|
||||
|
||||
type PlaybookMember = {
|
||||
user_id: string;
|
||||
roles: string[];
|
||||
scheme_roles?: string[];
|
||||
}
|
||||
|
|
|
|||
17
app/products/playbooks/types/client.d.ts
vendored
17
app/products/playbooks/types/client.d.ts
vendored
|
|
@ -36,6 +36,23 @@ type FetchPlaybookRunsParams = {
|
|||
since?: number;
|
||||
}
|
||||
|
||||
type FetchPlaybooksParams = {
|
||||
team_id: string;
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
sort?: 'title' | 'stages' | 'steps' | 'runs' | 'last_run_at' | 'active_runs';
|
||||
direction?: 'asc' | 'desc';
|
||||
search_term?: string;
|
||||
with_archived?: boolean;
|
||||
}
|
||||
|
||||
type FetchPlaybooksReturn = {
|
||||
total_count: number;
|
||||
page_count: number;
|
||||
has_more: boolean;
|
||||
items: Playbook[];
|
||||
}
|
||||
|
||||
type PostStatusUpdatePayload = {
|
||||
message: string;
|
||||
reminder?: number;
|
||||
|
|
|
|||
|
|
@ -1004,6 +1004,7 @@
|
|||
"playbooks.checklist_item.skipped": "Skipped",
|
||||
"playbooks.checklist_item.task_rendered_conditionally": "Task rendered conditionally",
|
||||
"playbooks.checklist_item.task_rendered_conditionally_explanation": "This task was rendered conditionally based on",
|
||||
"playbooks.create_run.select_playbook.no_results": "No Results",
|
||||
"playbooks.due_date.date_at_time": "{date} at {time}",
|
||||
"playbooks.due_date.none": "None",
|
||||
"playbooks.edit_command.label": "Command",
|
||||
|
|
@ -1069,15 +1070,35 @@
|
|||
"playbooks.retrospective_not_available.description": "Only Playbook Runs are available on mobile. To fill the Run Retrospective, please use the desktop or web app.",
|
||||
"playbooks.retrospective_not_available.ok": "OK",
|
||||
"playbooks.retrospective_not_available.title": "Playbooks Run Retrospective not available",
|
||||
"playbooks.row.last_used": "Last used {time}",
|
||||
"playbooks.row.never_used": "Never used",
|
||||
"playbooks.row.no_runs": "No runs in progress",
|
||||
"playbooks.row.runs": "{count} {count, plural, one {run} other {runs}} in progress",
|
||||
"playbooks.runs.finished.description": "When a run in this channel finishes, you’ll see it here.",
|
||||
"playbooks.runs.finished.title": "No finished runs",
|
||||
"playbooks.runs.in_progress.description": "When a run starts in this channel, you’ll see it here.",
|
||||
"playbooks.runs.in_progress.title": "No in progress runs",
|
||||
"playbooks.runs.show_more": "Show More",
|
||||
"playbooks.runs.start_a_new_run": "Start a new run",
|
||||
"playbooks.select_date.title": "Due date",
|
||||
"playbooks.select_playbook.in_this_channel": "In This Channel",
|
||||
"playbooks.select_playbook.other_playbooks": "Other Playbooks",
|
||||
"playbooks.select_playbook.subtitle": "Select a playbook",
|
||||
"playbooks.select_playbook.title": "Start a run",
|
||||
"playbooks.select_playbook.your_playbooks": "Your Playbooks",
|
||||
"playbooks.select_user.no_assignee": "No Assignee",
|
||||
"playbooks.select_user.not_participants": "NOT PARTICIPATING",
|
||||
"playbooks.select_user.participants": "RUN PARTICIPANTS",
|
||||
"playbooks.start_a_run.title": "Start a run",
|
||||
"playbooks.start_run.channel_label": "Channel",
|
||||
"playbooks.start_run.create_new_channel": "Create a new channel",
|
||||
"playbooks.start_run.create_new_channel.private_channel": "Private channel",
|
||||
"playbooks.start_run.create_new_channel.public_channel": "Public channel",
|
||||
"playbooks.start_run.link_existing_channel": "Link to an existing channel",
|
||||
"playbooks.start_run.run_description_label": "Run description",
|
||||
"playbooks.start_run.run_name_error": "Please add a name for this run",
|
||||
"playbooks.start_run.run_name_label": "Run name",
|
||||
"playbooks.start_run.run_name_placeholder": "Add a name for your run",
|
||||
"playbooks.status_update_post.num_tasks": "**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked",
|
||||
"playbooks.status_update_post.participants": "{numParticipants, number} {numParticipants, plural, =1 {participant} other {participants}}",
|
||||
"playbooks.status_update_post.update": "@{authorUsername} posted an update for [{runName}]({link})",
|
||||
|
|
|
|||
Loading…
Reference in a new issue