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 57228bd0e..ace179ba2 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 @@ -6,7 +6,6 @@ import {type IntlShape, useIntl} from 'react-intl'; import {Text, View, type StyleProp, type TextStyle, type ViewStyle} from 'react-native'; import CompassIcon from '@components/compass_icon'; -import TouchableWithFeedback from '@components/touchable_with_feedback'; import {Screens, View as ViewConstants} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; @@ -48,6 +47,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', + flex: 1, }, dropdownPlaceholder: { color: changeOpacity(theme.centerChannelColor, 0.5), @@ -174,14 +174,14 @@ function AutoCompleteSelector({ }); }, [dataSource, teammateNameDisplay, intl, options, selected, serverUrl]); - const touchableStyle = useMemo(() => { - const res: StyleProp = [{flex: 1}]; + const inputStyle = useMemo(() => { + const res: StyleProp = [style.input]; if (disabled) { res.push(style.disabled); } return res; - }, [disabled, style.disabled]); + }, [disabled, style.disabled, style.input]); const dropdownTextStyle = useMemo(() => { const res: StyleProp = [style.dropdownText]; @@ -200,30 +200,24 @@ function AutoCompleteSelector({ error={errorText} hideErrorIcon={true} theme={theme} + focus={goToSelectorScreen} focused={false} focusedLabel={focusedLabel} editable={!disabled} testID={testID} > - - - - {itemText || placeholder} - - - - + + + {itemText || placeholder} + + + ); } diff --git a/app/components/floating_input/floating_input_container.tsx b/app/components/floating_input/floating_input_container.tsx index e1bc3d10e..6e6edb2f8 100644 --- a/app/components/floating_input/floating_input_container.tsx +++ b/app/components/floating_input/floating_input_container.tsx @@ -189,7 +189,7 @@ const FloatingInputContainer = ({ paddingHorizontal: focusedLabel || inputText ? 4 : 0, color, }; - }); + }, [styles, theme, focusedLabel, hasValue, shouldShowError, positions]); return ( => { +export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOnly = false) => { try { const client = NetworkManager.getClient(serverUrl); const run = await client.fetchPlaybookRun(runId); @@ -102,7 +102,7 @@ export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOn } } - return {runs: [run]}; + return {run}; } catch (error) { logDebug('error on fetchPlaybookRun', getFullErrorMessage(error)); forceLogoutIfNecessary(serverUrl, error); @@ -110,6 +110,18 @@ export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOn } }; +export const fetchPlaybookRunMetadata = async (serverUrl: string, runId: string) => { + try { + const client = NetworkManager.getClient(serverUrl); + const metadata = await client.fetchPlaybookRunMetadata(runId); + return {metadata}; + } catch (error) { + logDebug('error on fetchPlaybookRunMetadata', getFullErrorMessage(error)); + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; + export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId: string) => { try { const client = NetworkManager.getClient(serverUrl); @@ -124,6 +136,16 @@ export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId } }; +export const postStatusUpdate = async (serverUrl: string, playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => { + try { + const client = NetworkManager.getClient(serverUrl); + await client.postStatusUpdate(playbookRunID, payload, ids); + } catch (error) { + logDebug('error on postStatusUpdate', getFullErrorMessage(error)); + forceLogoutIfNecessary(serverUrl, error); + } +}; + export const finishRun = async (serverUrl: string, playbookRunId: string) => { try { const client = NetworkManager.getClient(serverUrl); diff --git a/app/products/playbooks/client/rest.ts b/app/products/playbooks/client/rest.ts index 539b81f60..9f7e90a3f 100644 --- a/app/products/playbooks/client/rest.ts +++ b/app/products/playbooks/client/rest.ts @@ -10,10 +10,13 @@ export interface ClientPlaybooksMix { // Playbook Runs fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise; fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise; + fetchPlaybookRunMetadata: (id: string) => Promise; setOwner: (playbookRunId: string, ownerId: string) => Promise; // Run Management + // finishRun: (playbookRunId: string) => Promise; finishRun: (playbookRunId: string) => Promise; + postStatusUpdate: (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => Promise; // Checklist Management setChecklistItemState: (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => Promise; @@ -65,6 +68,13 @@ const ClientPlaybooks = >(superclass: TBas ); }; + fetchPlaybookRunMetadata = async (id: string) => { + return this.doFetch( + `${this.getPlaybookRunRoute(id)}/metadata`, + {method: 'get'}, + ); + }; + setOwner = async (playbookRunId: string, ownerId: string) => { const data = await this.doFetch( `${this.getPlaybookRunRoute(playbookRunId)}/owner`, @@ -81,6 +91,26 @@ const ClientPlaybooks = >(superclass: TBas ); }; + postStatusUpdate = async (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => { + const body = { + type: 'dialog_submission', + callback_id: '', + state: '', + cancelled: false, + ...ids, + submission: { + message: payload.message, + reminder: payload.reminder?.toFixed() ?? '', + finish_run: payload.finishRun, + }, + }; + + await this.doFetch( + `${this.getPlaybookRunRoute(playbookRunID)}/update-status-dialog`, + {method: 'post', body}, + ); + }; + // Checklist Management setChecklistItemState = async (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => { await this.doFetch( diff --git a/app/products/playbooks/constants/screens.ts b/app/products/playbooks/constants/screens.ts index 5ff988c42..2f5c81d86 100644 --- a/app/products/playbooks/constants/screens.ts +++ b/app/products/playbooks/constants/screens.ts @@ -4,6 +4,7 @@ export const PLAYBOOKS_RUNS = 'PlaybookRuns'; export const PLAYBOOK_RUN = 'PlaybookRun'; export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand'; +export const PLAYBOOK_POST_UPDATE = 'PlaybookPostUpdate'; export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser'; export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate'; @@ -11,6 +12,7 @@ export default { PLAYBOOKS_RUNS, PLAYBOOK_RUN, PLAYBOOK_EDIT_COMMAND, + PLAYBOOK_POST_UPDATE, PLAYBOOK_SELECT_USER, PLAYBOOKS_SELECT_DATE, -}; +} as const; diff --git a/app/products/playbooks/screens/index.test.tsx b/app/products/playbooks/screens/index.test.tsx index 42245cc1d..bdef64703 100644 --- a/app/products/playbooks/screens/index.test.tsx +++ b/app/products/playbooks/screens/index.test.tsx @@ -10,6 +10,7 @@ import {render} from '@test/intl-test-helper'; import EditCommand from './edit_command'; import PlaybookRun from './playbook_run'; import PlaybookRuns from './playbooks_runs'; +import PostUpdate from './post_update'; import SelectDate from './select_date'; import SelectUser from './select_user'; @@ -52,6 +53,12 @@ jest.mock('@playbooks/screens/select_date', () => ({ })); jest.mocked(SelectDate).mockImplementation((props) => {Screens.PLAYBOOKS_SELECT_DATE}); +jest.mock('@playbooks/screens/post_update', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mocked(PostUpdate).mockImplementation((props) => {Screens.PLAYBOOK_POST_UPDATE}); + describe('Screen Registration', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/app/products/playbooks/screens/index.tsx b/app/products/playbooks/screens/index.tsx index f67cf336c..ec387c75d 100644 --- a/app/products/playbooks/screens/index.tsx +++ b/app/products/playbooks/screens/index.tsx @@ -12,6 +12,8 @@ export function loadPlaybooksScreen(screenName: string | number) { return withServerDatabase(require('@playbooks/screens/playbook_run').default); case Screens.PLAYBOOK_EDIT_COMMAND: return withServerDatabase(require('@playbooks/screens/edit_command').default); + case Screens.PLAYBOOK_POST_UPDATE: + return withServerDatabase(require('@playbooks/screens/post_update').default); case Screens.PLAYBOOK_SELECT_USER: return withServerDatabase(require('@playbooks/screens/select_user').default); case Screens.PLAYBOOKS_SELECT_DATE: diff --git a/app/products/playbooks/screens/navigation.ts b/app/products/playbooks/screens/navigation.ts index cb91e28e4..3e5a20ea1 100644 --- a/app/products/playbooks/screens/navigation.ts +++ b/app/products/playbooks/screens/navigation.ts @@ -26,6 +26,11 @@ export async function goToPlaybookRun(intl: IntlShape, playbookRunId: string, pl goToScreen(Screens.PLAYBOOK_RUN, title, {playbookRunId, playbookRun}, {}); } +export async function goToPostUpdate(intl: IntlShape, playbookRunId: string) { + const title = intl.formatMessage({id: 'playbooks.post_update.title', defaultMessage: 'Post update'}); + goToScreen(Screens.PLAYBOOK_POST_UPDATE, title, {playbookRunId}, {}); +} + function getSubtitleOptions(theme: Theme, runName: string): RNNOptions { return { topBar: { diff --git a/app/products/playbooks/screens/playbook_run/playbook_run.tsx b/app/products/playbooks/screens/playbook_run/playbook_run.tsx index 3c32b2c7b..c2a4624d1 100644 --- a/app/products/playbooks/screens/playbook_run/playbook_run.tsx +++ b/app/products/playbooks/screens/playbook_run/playbook_run.tsx @@ -337,6 +337,8 @@ export default function PlaybookRun({ diff --git a/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx b/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx index 98b8efcd5..77e021b61 100644 --- a/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx +++ b/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx @@ -32,6 +32,8 @@ describe('StatusUpdateIndicator', () => { , ); @@ -48,6 +50,8 @@ describe('StatusUpdateIndicator', () => { , ); @@ -64,6 +68,8 @@ describe('StatusUpdateIndicator', () => { , ); diff --git a/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx b/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx index c9c7c2f3a..e956100ad 100644 --- a/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx +++ b/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx @@ -1,19 +1,24 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {defineMessages, useIntl} from 'react-intl'; import {View, Text} from 'react-native'; +import Button from '@components/button'; import CompassIcon from '@components/compass_icon'; import FriendlyDate from '@components/friendly_date'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; +import {goToPostUpdate} from '../navigation'; + type StatusUpdateIndicatorProps = { isFinished: boolean; + isParticipant: boolean; timestamp: number; + playbookRunId: string; } const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ @@ -27,6 +32,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ borderColor: changeOpacity(theme.centerChannelColor, 0.16), gap: 8, }, + due: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + flex: 1, + }, + dueTextContainer: { + flex: 1, + }, icon: { fontSize: 24, color: changeOpacity(theme.centerChannelColor, 0.72), @@ -56,17 +70,24 @@ const messages = defineMessages({ id: 'playbooks.playbook_run.status_update_finished', defaultMessage: 'Run finished {time}', }, + update: { + id: 'playbooks.playbook_run.status_update', + defaultMessage: 'Post update', + }, }); const StatusUpdateIndicator = ({ isFinished, timestamp, + isParticipant, + playbookRunId, }: StatusUpdateIndicatorProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); const intl = useIntl(); const values = {time: }; + const readOnly = !isParticipant || isFinished; let message = messages.updateDue; if (isFinished) { @@ -88,16 +109,35 @@ const StatusUpdateIndicator = ({ ]; }, [styles.icon, styles.overdueText, isFinished, timestamp]); + const onUpdatePress = useCallback(async () => { + if (readOnly) { + return; + } + + await goToPostUpdate(intl, playbookRunId); + }, [intl, playbookRunId, readOnly]); + const icon = isFinished ? 'flag-checkered' : 'clock-outline'; return ( - + + + + {intl.formatMessage(message, values)} + + + +