From 468b271928c0b7eaa347f5e0ab5c062cea02d0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 13 Oct 2025 21:35:06 +0200 Subject: [PATCH] Add create run (#9203) * Add playbook start a run * UI fixes * Add i18n * Address feedback * Fix typo --- .../floating_autocomplete_selector.tsx | 6 +- .../playbooks/actions/remote/playbooks.ts | 19 + app/products/playbooks/actions/remote/runs.ts | 21 ++ app/products/playbooks/client/rest.ts | 40 ++- app/products/playbooks/constants/screens.ts | 4 + app/products/playbooks/screens/index.test.tsx | 14 + app/products/playbooks/screens/index.tsx | 4 + app/products/playbooks/screens/navigation.ts | 28 ++ .../screens/playbooks_runs/playbook_runs.tsx | 40 ++- .../screens/select_playbook/index.ts | 35 ++ .../screens/select_playbook/playbook_row.tsx | 116 ++++++ .../select_playbook/select_playbook.tsx | 339 ++++++++++++++++++ .../playbooks/screens/start_a_run/index.ts | 19 + .../screens/start_a_run/start_a_run.tsx | 280 +++++++++++++++ app/products/playbooks/types/api.d.ts | 30 ++ app/products/playbooks/types/client.d.ts | 17 + assets/base/i18n/en.json | 21 ++ 17 files changed, 1022 insertions(+), 11 deletions(-) create mode 100644 app/products/playbooks/actions/remote/playbooks.ts create mode 100644 app/products/playbooks/screens/select_playbook/index.ts create mode 100644 app/products/playbooks/screens/select_playbook/playbook_row.tsx create mode 100644 app/products/playbooks/screens/select_playbook/select_playbook.tsx create mode 100644 app/products/playbooks/screens/start_a_run/index.ts create mode 100644 app/products/playbooks/screens/start_a_run/start_a_run.tsx diff --git a/app/components/floating_input/floating_autocomplete_selector/floating_autocomplete_selector.tsx b/app/components/floating_input/floating_autocomplete_selector/floating_autocomplete_selector.tsx index ace179ba2..870cc285a 100644 --- a/app/components/floating_input/floating_autocomplete_selector/floating_autocomplete_selector.tsx +++ b/app/components/floating_input/floating_autocomplete_selector/floating_autocomplete_selector.tsx @@ -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 = [style.input]; diff --git a/app/products/playbooks/actions/remote/playbooks.ts b/app/products/playbooks/actions/remote/playbooks.ts new file mode 100644 index 000000000..1db474e9f --- /dev/null +++ b/app/products/playbooks/actions/remote/playbooks.ts @@ -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}; + } +} diff --git a/app/products/playbooks/actions/remote/runs.ts b/app/products/playbooks/actions/remote/runs.ts index ecff9f252..23d9423d8 100644 --- a/app/products/playbooks/actions/remote/runs.ts +++ b/app/products/playbooks/actions/remote/runs.ts @@ -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); diff --git a/app/products/playbooks/client/rest.ts b/app/products/playbooks/client/rest.ts index c923a0446..384b89f09 100644 --- a/app/products/playbooks/client/rest.ts +++ b/app/products/playbooks/client/rest.ts @@ -7,6 +7,9 @@ import type ClientBase from '@client/rest/base'; export interface ClientPlaybooksMix { + // Playbooks + fetchPlaybooks: (params: FetchPlaybooksParams) => Promise; + // Playbook Runs fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise; fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise; @@ -14,8 +17,8 @@ export interface ClientPlaybooksMix { setOwner: (playbookRunId: string, ownerId: string) => Promise; // Run Management - // finishRun: (playbookRunId: string) => Promise; finishRun: (playbookRunId: string) => Promise; + createPlaybookRun: (playbook_id: string, owner_user_id: string, team_id: string, name: string, description: string, channel_id?: string, create_public_run?: boolean) => Promise; postStatusUpdate: (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => Promise; // Checklist Management @@ -46,6 +49,17 @@ const ClientPlaybooks = >(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 = >(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', diff --git a/app/products/playbooks/constants/screens.ts b/app/products/playbooks/constants/screens.ts index 912d16dbd..43365b4d9 100644 --- a/app/products/playbooks/constants/screens.ts +++ b/app/products/playbooks/constants/screens.ts @@ -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; diff --git a/app/products/playbooks/screens/index.test.tsx b/app/products/playbooks/screens/index.test.tsx index 09e2a967e..ca432f498 100644 --- a/app/products/playbooks/screens/index.test.tsx +++ b/app/products/playbooks/screens/index.test.tsx @@ -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) => {Screens.PLAYBOOKS_SELECT_DATE}); +jest.mock('@playbooks/screens/start_a_run', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mocked(StartARun).mockImplementation((props) => {Screens.PLAYBOOKS_START_A_RUN}); + +jest.mock('@playbooks/screens/select_playbook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mocked(SelectPlaybook).mockImplementation((props) => {Screens.PLAYBOOKS_SELECT_PLAYBOOK}); + jest.mock('@playbooks/screens/participant_playbooks', () => ({ __esModule: true, default: jest.fn(), diff --git a/app/products/playbooks/screens/index.tsx b/app/products/playbooks/screens/index.tsx index e36dc4cc9..48eee52e4 100644 --- a/app/products/playbooks/screens/index.tsx +++ b/app/products/playbooks/screens/index.tsx @@ -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; } diff --git a/app/products/playbooks/screens/navigation.ts b/app/products/playbooks/screens/navigation.ts index 5ed1277a6..bec4b72fd 100644 --- a/app/products/playbooks/screens/navigation.ts +++ b/app/products/playbooks/screens/navigation.ts @@ -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), + }, + }, + }); +} diff --git a/app/products/playbooks/screens/playbooks_runs/playbook_runs.tsx b/app/products/playbooks/screens/playbooks_runs/playbook_runs.tsx index 6cc107bbd..7f0b9acd3 100644 --- a/app/products/playbooks/screens/playbooks_runs/playbook_runs.tsx +++ b/app/products/playbooks/screens/playbooks_runs/playbook_runs.tsx @@ -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 = (); if (!isEmpty) { content = ( - + <> + + +