diff --git a/app/components/option_item/index.tsx b/app/components/option_item/index.tsx
index 1f0514a0b..fcc447754 100644
--- a/app/components/option_item/index.tsx
+++ b/app/components/option_item/index.tsx
@@ -49,6 +49,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
actionContainer: {
flexDirection: 'row',
alignItems: 'center',
+ justifyContent: 'flex-end',
},
container: {
flexDirection: 'row',
@@ -97,6 +98,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
color: theme.centerChannelColor,
...typography('Body', 200),
},
+ shrink: {
+ flexShrink: 1,
+ },
};
});
@@ -184,9 +188,9 @@ const OptionItem = ({
]);
const actionContainerStyle = useMemo(() => {
- const extraStyle = longInfo ? {maxWidth: 300} : {};
+ const extraStyle = longInfo ? styles.shrink : {};
return [styles.actionContainer, extraStyle];
- }, [longInfo, styles.actionContainer]);
+ }, [longInfo, styles.actionContainer, styles.shrink]);
let actionComponent;
let radioComponent;
@@ -277,6 +281,15 @@ const OptionItem = ({
{info}
);
+ if (actionComponent) {
+ // Wrap the text into another view to properly calculate
+ // the space available.
+ infoComponent = (
+
+ {infoComponent}
+
+ );
+ }
}
const component = (
diff --git a/app/products/playbooks/actions/remote/checklist.test.ts b/app/products/playbooks/actions/remote/checklist.test.ts
index 4a71516e1..0fbad9c9e 100644
--- a/app/products/playbooks/actions/remote/checklist.test.ts
+++ b/app/products/playbooks/actions/remote/checklist.test.ts
@@ -5,7 +5,7 @@ import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {updateChecklistItem as localUpdateChecklistItem} from '@playbooks/actions/local/checklist';
-import {updateChecklistItem, runChecklistItem} from './checklist';
+import {updateChecklistItem, runChecklistItem, skipChecklistItem, restoreChecklistItem} from './checklist';
const serverUrl = 'baseHandler.test.com';
@@ -17,6 +17,8 @@ const itemNumber = 1;
const mockClient = {
setChecklistItemState: jest.fn(),
runChecklistItemSlashCommand: jest.fn(),
+ skipChecklistItem: jest.fn(),
+ restoreChecklistItem: jest.fn(),
};
jest.mock('@playbooks/actions/local/checklist');
@@ -49,15 +51,6 @@ describe('checklist', () => {
expect(localUpdateChecklistItem).not.toHaveBeenCalled();
});
- it('should handle API error response', async () => {
- mockClient.setChecklistItemState.mockResolvedValueOnce({error: 'API error'});
-
- const result = await updateChecklistItem(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 'closed');
- expect(result).toBeDefined();
- expect(result.error).toBe('API error');
- expect(localUpdateChecklistItem).not.toHaveBeenCalled();
- });
-
it('should update checklist item successfully', async () => {
mockClient.setChecklistItemState.mockResolvedValueOnce({});
@@ -97,4 +90,66 @@ describe('checklist', () => {
expect(mockClient.runChecklistItemSlashCommand).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber);
});
});
+
+ describe('skipChecklistItem', () => {
+ it('should handle client error', async () => {
+ jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
+
+ const result = await skipChecklistItem(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber);
+ expect(result).toBeDefined();
+ expect(result.error).toBeDefined();
+ expect(mockClient.skipChecklistItem).not.toHaveBeenCalled();
+ expect(localUpdateChecklistItem).not.toHaveBeenCalled();
+ });
+
+ it('should handle API exception', async () => {
+ mockClient.skipChecklistItem.mockImplementationOnce(throwFunc);
+
+ const result = await skipChecklistItem(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber);
+ expect(result).toBeDefined();
+ expect(result.error).toBeDefined();
+ expect(mockClient.skipChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber);
+ expect(localUpdateChecklistItem).not.toHaveBeenCalled();
+ });
+
+ it('should skip checklist item successfully', async () => {
+ mockClient.skipChecklistItem.mockResolvedValueOnce({});
+
+ const result = await skipChecklistItem(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber);
+ expect(result).toBeDefined();
+ expect(result.error).toBeUndefined();
+ expect(result.data).toBe(true);
+ expect(mockClient.skipChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber);
+ expect(localUpdateChecklistItem).toHaveBeenCalledWith(serverUrl, itemId, 'skipped');
+ });
+ });
+
+ describe('restoreChecklistItem', () => {
+ it('should handle client error', async () => {
+ jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
+
+ const result = await restoreChecklistItem(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber);
+ expect(result).toBeDefined();
+ expect(result.error).toBeDefined();
+ });
+
+ it('should handle API exception', async () => {
+ mockClient.restoreChecklistItem.mockImplementationOnce(throwFunc);
+
+ const result = await restoreChecklistItem(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber);
+ expect(result).toBeDefined();
+ expect(result.error).toBeDefined();
+ });
+
+ it('should restore checklist item successfully', async () => {
+ mockClient.restoreChecklistItem.mockResolvedValueOnce({});
+
+ const result = await restoreChecklistItem(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber);
+ expect(result).toBeDefined();
+ expect(result.error).toBeUndefined();
+ expect(result.data).toBe(true);
+ expect(mockClient.restoreChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber);
+ expect(localUpdateChecklistItem).toHaveBeenCalledWith(serverUrl, itemId, '');
+ });
+ });
});
diff --git a/app/products/playbooks/actions/remote/checklist.ts b/app/products/playbooks/actions/remote/checklist.ts
index 480d9eb51..476d75f9a 100644
--- a/app/products/playbooks/actions/remote/checklist.ts
+++ b/app/products/playbooks/actions/remote/checklist.ts
@@ -18,14 +18,11 @@ export const updateChecklistItem = async (
try {
const client = NetworkManager.getClient(serverUrl);
- const res = await client.setChecklistItemState(playbookRunId, checklistNumber, itemNumber, state);
- if (res.error) {
- return {error: res.error};
- }
+ await client.setChecklistItemState(playbookRunId, checklistNumber, itemNumber, state);
await localUpdateChecklistItem(serverUrl, itemId, state);
return {data: true};
} catch (error) {
- logDebug('error on fetchPlaybookRunsForChannel', getFullErrorMessage(error));
+ logDebug('error on updateChecklistItem', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
@@ -47,3 +44,43 @@ export const runChecklistItem = async (
return {error};
}
};
+
+export const skipChecklistItem = async (
+ serverUrl: string,
+ playbookRunId: string,
+ itemId: string,
+ checklistNumber: number,
+ itemNumber: number,
+) => {
+ try {
+ const client = NetworkManager.getClient(serverUrl);
+ await client.skipChecklistItem(playbookRunId, checklistNumber, itemNumber);
+
+ await localUpdateChecklistItem(serverUrl, itemId, 'skipped');
+ return {data: true};
+ } catch (error) {
+ logDebug('error on skipChecklistItem', getFullErrorMessage(error));
+ forceLogoutIfNecessary(serverUrl, error);
+ return {error};
+ }
+};
+
+export const restoreChecklistItem = async (
+ serverUrl: string,
+ playbookRunId: string,
+ itemId: string,
+ checklistNumber: number,
+ itemNumber: number,
+) => {
+ try {
+ const client = NetworkManager.getClient(serverUrl);
+ await client.restoreChecklistItem(playbookRunId, checklistNumber, itemNumber);
+
+ await localUpdateChecklistItem(serverUrl, itemId, '');
+ return {data: true};
+ } catch (error) {
+ logDebug('error on restoreChecklistItem', getFullErrorMessage(error));
+ forceLogoutIfNecessary(serverUrl, error);
+ return {error};
+ }
+};
diff --git a/app/products/playbooks/client/rest.test.ts b/app/products/playbooks/client/rest.test.ts
index 75b48763b..bb9b284f4 100644
--- a/app/products/playbooks/client/rest.test.ts
+++ b/app/products/playbooks/client/rest.test.ts
@@ -172,3 +172,84 @@ describe('runChecklistItemSlashCommand', () => {
await expect(client.runChecklistItemSlashCommand(playbookRunId, checklistNumber, itemNumber)).rejects.toThrow('Network error');
});
});
+
+describe('skipChecklistItem', () => {
+ test('should skip checklist item successfully', async () => {
+ const playbookRunID = 'run123';
+ const checklistNum = 1;
+ const itemNum = 2;
+ const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/skip`;
+ const expectedOptions = {method: 'put', body: ''};
+
+ jest.mocked(client.doFetch).mockResolvedValue(undefined);
+
+ await client.skipChecklistItem(playbookRunID, checklistNum, itemNum);
+
+ expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
+ });
+
+ test('should skip checklist item with zero indices', async () => {
+ const playbookRunID = 'run123';
+ const checklistNum = 0;
+ const itemNum = 0;
+ const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/skip`;
+ const expectedOptions = {method: 'put', body: ''};
+
+ jest.mocked(client.doFetch).mockResolvedValue(undefined);
+
+ await client.skipChecklistItem(playbookRunID, checklistNum, itemNum);
+
+ expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
+ });
+
+ test('should handle error when skipping checklist item', async () => {
+ const playbookRunID = 'run123';
+ const checklistNum = 1;
+ const itemNum = 2;
+
+ jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
+
+ await expect(client.skipChecklistItem(playbookRunID, checklistNum, itemNum)).rejects.toThrow('Network error');
+ });
+});
+
+describe('restoreChecklistItem', () => {
+ test('should restore checklist item successfully', async () => {
+ const playbookRunID = 'run123';
+ const checklistNum = 1;
+ const itemNum = 2;
+ const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/restore`;
+ const expectedOptions = {method: 'put', body: ''};
+
+ jest.mocked(client.doFetch).mockResolvedValue(undefined);
+
+ await client.restoreChecklistItem(playbookRunID, checklistNum, itemNum);
+
+ expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
+ });
+
+ test('should restore checklist item with zero indices', async () => {
+ const playbookRunID = 'run123';
+ const checklistNum = 0;
+ const itemNum = 0;
+ const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/restore`;
+ const expectedOptions = {method: 'put', body: ''};
+
+ jest.mocked(client.doFetch).mockResolvedValue(undefined);
+
+ await client.restoreChecklistItem(playbookRunID, checklistNum, itemNum);
+
+ expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
+ });
+
+ test('should handle error when restoring checklist item', async () => {
+ const playbookRunID = 'run123';
+ const checklistNum = 1;
+ const itemNum = 2;
+
+ jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
+
+ await expect(client.restoreChecklistItem(playbookRunID, checklistNum, itemNum)).rejects.toThrow('Network error');
+ });
+});
+
diff --git a/app/products/playbooks/client/rest.ts b/app/products/playbooks/client/rest.ts
index c3f7f657a..597c24f11 100644
--- a/app/products/playbooks/client/rest.ts
+++ b/app/products/playbooks/client/rest.ts
@@ -9,16 +9,16 @@ export interface ClientPlaybooksMix {
// Playbook Runs
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise;
-
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise;
// Run Management
// finishRun: (playbookRunId: string) => Promise;
// Checklist Management
- setChecklistItemState: (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => Promise;
+ setChecklistItemState: (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => Promise;
+ skipChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise;
+ restoreChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise;
- // skipChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise;
// skipChecklist: (playbookRunID: string, checklistNum: number) => Promise;
// Slash Commands
@@ -85,12 +85,19 @@ const ClientPlaybooks = >(superclass: TBas
}
};
- // skipChecklistItem = async (playbookRunID: string, checklistNum: number, itemNum: number) => {
- // await this.doFetch(
- // `${this.getPlaybookRunRoute(playbookRunID)}/checklists/${checklistNum}/item/${itemNum}/skip`,
- // {method: 'put', body: ''},
- // );
- // };
+ skipChecklistItem = async (playbookRunID: string, checklistNum: number, itemNum: number) => {
+ await this.doFetch(
+ `${this.getPlaybookRunRoute(playbookRunID)}/checklists/${checklistNum}/item/${itemNum}/skip`,
+ {method: 'put', body: ''},
+ );
+ };
+
+ restoreChecklistItem = async (playbookRunID: string, checklistNum: number, itemNum: number) => {
+ await this.doFetch(
+ `${this.getPlaybookRunRoute(playbookRunID)}/checklists/${checklistNum}/item/${itemNum}/restore`,
+ {method: 'put', body: ''},
+ );
+ };
// skipChecklist = async (playbookRunID: string, checklistNum: number) => {
// await this.doFetch(
diff --git a/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.test.tsx b/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.test.tsx
index 458795a88..af91ddeb9 100644
--- a/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.test.tsx
+++ b/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.test.tsx
@@ -1,21 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {act, waitFor} from '@testing-library/react-native';
+import {act, fireEvent, waitFor} from '@testing-library/react-native';
import React, {type ComponentProps} from 'react';
import BaseChip from '@components/chips/base_chip';
import UserChip from '@components/chips/user_chip';
import {Preferences} from '@constants';
import {useServerUrl} from '@context/server';
-import {runChecklistItem, updateChecklistItem} from '@playbooks/actions/remote/checklist';
-import {openUserProfileModal, popTo} from '@screens/navigation';
+import {runChecklistItem, skipChecklistItem, updateChecklistItem} from '@playbooks/actions/remote/checklist';
+import {bottomSheet, openUserProfileModal, popTo} from '@screens/navigation';
import {renderWithIntl} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
import Checkbox from './checkbox';
import ChecklistItem from './checklist_item';
+import ChecklistItemBottomSheet from './checklist_item_bottom_sheet';
const serverUrl = 'some.server.url';
jest.mock('@context/server');
@@ -30,6 +31,9 @@ jest.mocked(UserChip).mockImplementation((props) => React.createElement('UserChi
jest.mock('@components/chips/base_chip');
jest.mocked(BaseChip).mockImplementation((props) => React.createElement('BaseChip', {...props, testID: 'base-chip-component'}));
+jest.mock('./checklist_item_bottom_sheet');
+jest.mocked(ChecklistItemBottomSheet).mockImplementation((props) => React.createElement('ChecklistItemBottomSheet', {...props, testID: 'checklist-item-bottom-sheet-component'}));
+
jest.mock('@playbooks/actions/remote/checklist');
jest.mock('@utils/snack_bar');
@@ -345,4 +349,47 @@ describe('ChecklistItem', () => {
expect(popTo).not.toHaveBeenCalled();
});
});
+
+ it('opens the bottom sheet when the item is pressed', async () => {
+ const props = getBaseProps();
+
+ const {getByText} = renderWithIntl();
+
+ const item = getByText('Test Item');
+ act(() => {
+ fireEvent.press(item);
+ });
+
+ expect(bottomSheet).toHaveBeenCalled();
+ const args = jest.mocked(bottomSheet).mock.calls[0][0] as Parameters[0];
+ expect(args.title).toBe('Task Details');
+ expect(args.snapPoints).toEqual([1, 354, '80%']);
+ expect(args.theme).toBe(Preferences.THEMES.denim);
+ expect(args.closeButtonId).toBe('close-checklist-item');
+
+ const BottomSheetComponent = args.renderContent;
+ const {getByTestId} = renderWithIntl();
+ const bottomSheetRenderedComponent = getByTestId('checklist-item-bottom-sheet-component');
+ expect(bottomSheetRenderedComponent.props.item).toBe(props.item);
+ expect(bottomSheetRenderedComponent.props.assignee).toBe(props.assignee);
+ expect(bottomSheetRenderedComponent.props.teammateNameDisplay).toBe(props.teammateNameDisplay);
+
+ bottomSheetRenderedComponent.props.onCheck();
+
+ await waitFor(() => {
+ expect(updateChecklistItem).toHaveBeenCalledWith(serverUrl, props.playbookRunId, props.item.id, props.checklistNumber, props.itemNumber, 'closed');
+ });
+
+ bottomSheetRenderedComponent.props.onSkip();
+
+ await waitFor(() => {
+ expect(skipChecklistItem).toHaveBeenCalledWith(serverUrl, props.playbookRunId, props.item.id, props.checklistNumber, props.itemNumber);
+ });
+
+ bottomSheetRenderedComponent.props.onRunCommand();
+
+ await waitFor(() => {
+ expect(runChecklistItem).toHaveBeenCalledWith(serverUrl, props.playbookRunId, props.checklistNumber, props.itemNumber);
+ });
+ });
});
diff --git a/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.tsx b/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.tsx
index a07e5e2e7..20b6607a5 100644
--- a/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.tsx
+++ b/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item.tsx
@@ -9,17 +9,19 @@ import BaseChip from '@components/chips/base_chip';
import UserChip from '@components/chips/user_chip';
import CompassIcon from '@components/compass_icon';
import {getFriendlyDate} from '@components/friendly_date';
+import PressableOpacity from '@components/pressable_opacity';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
-import {runChecklistItem, updateChecklistItem} from '@playbooks/actions/remote/checklist';
+import {restoreChecklistItem, runChecklistItem, skipChecklistItem, updateChecklistItem} from '@playbooks/actions/remote/checklist';
import {isDueSoon, isOverdue} from '@playbooks/utils/run';
-import {openUserProfileModal, popTo} from '@screens/navigation';
+import {bottomSheet, openUserProfileModal, popTo} from '@screens/navigation';
import {logDebug} from '@utils/log';
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
import Checkbox from './checkbox';
+import ChecklistItemBottomSheet, {BOTTOM_SHEET_HEIGHT} from './checklist_item_bottom_sheet';
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
import type UserModel from '@typings/database/models/servers/user';
@@ -137,6 +139,23 @@ const ChecklistItem = ({
setIsChecking(false);
}, [isChecking, serverUrl, playbookRunId, item.id, checklistNumber, itemNumber, checked]);
+ const toggleSkipped = useCallback(async () => {
+ if (isChecking) {
+ return;
+ }
+ setIsChecking(true);
+ let res;
+ if (skipped) {
+ res = await restoreChecklistItem(serverUrl, playbookRunId, item.id, checklistNumber, itemNumber);
+ } else {
+ res = await skipChecklistItem(serverUrl, playbookRunId, item.id, checklistNumber, itemNumber);
+ }
+ if (res.error) {
+ showPlaybookErrorSnackbar();
+ }
+ setIsChecking(false);
+ }, [isChecking, serverUrl, playbookRunId, item.id, checklistNumber, itemNumber, skipped]);
+
const chipIconStyle = useMemo(() => {
return [
styles.chipIcon,
@@ -172,18 +191,44 @@ const ChecklistItem = ({
}
}
+ const renderBottomSheet = useCallback(() => (
+
+ ), [assignee, executeCommand, item, teammateNameDisplay, toggleChecked, toggleSkipped, isDisabled]);
+
+ const onPress = useCallback(() => {
+ const initialHeight = BOTTOM_SHEET_HEIGHT.base + (isDisabled ? 0 : BOTTOM_SHEET_HEIGHT.actionButtons);
+ bottomSheet({
+ title: intl.formatMessage({id: 'playbook_run.checklist.taskDetails', defaultMessage: 'Task Details'}),
+ renderContent: renderBottomSheet,
+ snapPoints: [1, initialHeight, '80%'],
+ theme,
+ closeButtonId: 'close-checklist-item',
+ scrollable: true,
+ });
+ }, [intl, isDisabled, renderBottomSheet, theme]);
+
return (
{checkbox}
-
- {item.title}
- {item.description && (
- {item.description}
- )}
-
+
+
+ {item.title}
+ {Boolean(item.description) && (
+ {item.description}
+ )}
+
+
{(assignee || dueDate || (item.command)) && (
@@ -210,7 +255,7 @@ const ChecklistItem = ({
/>
)}
- {item.command && (
+ {Boolean(item.command) && (
({
+ useIsTablet: jest.fn(),
+}));
+
+jest.mock('@gorhom/bottom-sheet', () => ({
+ BottomSheetScrollView: jest.fn(),
+}));
+jest.mocked(BottomSheetScrollView).mockImplementation((props: any) => ); // casting to any to avoid type errors
+
+jest.mock('@components/option_box');
+jest.mocked(OptionBox).mockImplementation((props) => React.createElement('OptionBox', props));
+
+jest.mock('@components/option_item');
+jest.mocked(OptionItem).mockImplementation((props) => React.createElement('OptionItem', props));
+
+describe('ChecklistItemBottomSheet', () => {
+ const mockOnCheck = jest.fn();
+ const mockOnSkip = jest.fn();
+ const mockOnRunCommand = jest.fn();
+ const mockTeammateNameDisplay = 'username';
+
+ const mockAssignee = TestHelper.fakeUserModel({
+ id: 'user-1',
+ username: 'testuser',
+ firstName: 'Test',
+ lastName: 'User',
+ });
+
+ const mockItem = TestHelper.fakePlaybookChecklistItemModel({
+ id: 'item-1',
+ title: 'Test Checklist Item',
+ description: 'This is a test description',
+ state: '',
+ command: 'test command',
+ dueDate: 0,
+ commandLastRun: 0,
+ });
+
+ const mockApiItem = TestHelper.fakePlaybookChecklistItem('checklistId', {
+ id: 'item-2',
+ title: 'API Checklist Item',
+ description: 'This is an API item description',
+ state: 'closed',
+ command: 'api command',
+ due_date: 1640995200000, // 2022-01-01
+ command_last_run: 1640995200000,
+ });
+
+ function getBaseProps(): ComponentProps {
+ return {
+ item: mockItem,
+ assignee: mockAssignee,
+ onCheck: mockOnCheck,
+ onSkip: mockOnSkip,
+ onRunCommand: mockOnRunCommand,
+ teammateNameDisplay: mockTeammateNameDisplay,
+ isDisabled: false,
+ };
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.mocked(useIsTablet).mockReturnValue(false);
+ });
+
+ it('renders correctly with all props', () => {
+ const props = getBaseProps();
+ const {getByTestId, getByText} = renderWithIntl();
+
+ expect(getByText('Test Checklist Item')).toBeVisible();
+ expect(getByText('This is a test description')).toBeVisible();
+ expect(getByTestId('checklist_item.check_button')).toBeVisible();
+ expect(getByTestId('checklist_item.skip_button')).toBeVisible();
+ expect(getByTestId('checklist_item.run_command_button')).toBeVisible();
+ expect(getByTestId('checklist_item.assignee')).toBeVisible();
+ expect(getByTestId('checklist_item.due_date')).toBeVisible();
+ expect(getByTestId('checklist_item.command')).toBeVisible();
+ });
+
+ it('renders correctly without description', () => {
+ const props = getBaseProps();
+ props.item.description = '';
+ const {getByText, queryByText} = renderWithIntl();
+
+ expect(getByText('Test Checklist Item')).toBeVisible();
+ expect(queryByText('This is a test description')).toBeNull();
+ });
+
+ it('renders correctly without assignee', () => {
+ const props = getBaseProps();
+ props.assignee = undefined;
+ const {getByTestId} = renderWithIntl();
+
+ const assigneeOption = getByTestId('checklist_item.assignee');
+ expect(assigneeOption.props.info).toBe('None');
+ });
+
+ it('renders correctly with API item type', () => {
+ const props = getBaseProps();
+ props.item = mockApiItem;
+ const {getByText, getByTestId} = renderWithIntl();
+
+ expect(getByText('API Checklist Item')).toBeVisible();
+ expect(getByText('This is an API item description')).toBeVisible();
+ expect(getByTestId('checklist_item.check_button')).toBeVisible();
+ expect(getByTestId('checklist_item.skip_button')).toBeVisible();
+ expect(getByTestId('checklist_item.run_command_button')).toBeVisible();
+ expect(getByTestId('checklist_item.assignee')).toBeVisible();
+ expect(getByTestId('checklist_item.due_date')).toBeVisible();
+ expect(getByTestId('checklist_item.command')).toBeVisible();
+ });
+
+ it('handles check action correctly', async () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const checkButton = getByTestId('checklist_item.check_button');
+
+ await act(async () => {
+ fireEvent.press(checkButton);
+ });
+
+ expect(mockOnCheck).toHaveBeenCalledTimes(1);
+ expect(dismissBottomSheet).toHaveBeenCalledTimes(1);
+ });
+
+ it('handles skip action correctly', async () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const skipButton = getByTestId('checklist_item.skip_button');
+
+ await act(async () => {
+ fireEvent.press(skipButton);
+ });
+
+ expect(mockOnSkip).toHaveBeenCalledTimes(1);
+ expect(dismissBottomSheet).toHaveBeenCalledTimes(1);
+ });
+
+ it('handles run command action correctly', async () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const runCommandButton = getByTestId('checklist_item.run_command_button');
+
+ await act(async () => {
+ fireEvent.press(runCommandButton);
+ });
+
+ expect(mockOnRunCommand).toHaveBeenCalledTimes(1);
+ expect(dismissBottomSheet).toHaveBeenCalledTimes(1);
+ });
+
+ it('displays correct button states for checked item', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ state: 'closed',
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const checkButton = getByTestId('checklist_item.check_button');
+ const skipButton = getByTestId('checklist_item.skip_button');
+ const runCommandButton = getByTestId('checklist_item.run_command_button');
+
+ expect(checkButton.props.isActive).toBe(true);
+ expect(skipButton.props.isActive).toBe(false);
+ expect(runCommandButton.props.isActive).toBe(false);
+ });
+
+ it('displays correct button states for skipped item', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ state: 'skipped',
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const checkButton = getByTestId('checklist_item.check_button');
+ const skipButton = getByTestId('checklist_item.skip_button');
+ const runCommandButton = getByTestId('checklist_item.run_command_button');
+
+ expect(checkButton.props.isActive).toBe(false);
+ expect(skipButton.props.isActive).toBe(true);
+ expect(runCommandButton.props.isActive).toBe(false);
+ });
+
+ it('displays correct button states for command run item', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ commandLastRun: Date.now(),
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const checkButton = getByTestId('checklist_item.check_button');
+ const skipButton = getByTestId('checklist_item.skip_button');
+ const runCommandButton = getByTestId('checklist_item.run_command_button');
+
+ expect(checkButton.props.isActive).toBe(false);
+ expect(skipButton.props.isActive).toBe(false);
+ expect(runCommandButton.props.isActive).toBe(true);
+ });
+
+ it('displays correct button states for API item with command run', () => {
+ const props = getBaseProps();
+ props.item = mockApiItem;
+ const {getByTestId} = renderWithIntl();
+
+ const checkButton = getByTestId('checklist_item.check_button');
+ const skipButton = getByTestId('checklist_item.skip_button');
+ const runCommandButton = getByTestId('checklist_item.run_command_button');
+
+ expect(checkButton.props.isActive).toBe(true); // state is 'closed'
+ expect(skipButton.props.isActive).toBe(false);
+ expect(runCommandButton.props.isActive).toBe(true); // command_last_run exists
+ });
+
+ it('displays correct assignee information', () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const assigneeItem = getByTestId('checklist_item.assignee');
+ expect(assigneeItem.props.info).toEqual({
+ user: mockAssignee,
+ onPress: expect.any(Function),
+ teammateNameDisplay: mockTeammateNameDisplay,
+ location: 'PlaybookRun',
+ });
+ });
+
+ it('displays correct due date information', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ dueDate: 1640995200000, // 2022-01-01
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const dueDateItem = getByTestId('checklist_item.due_date');
+ expect(dueDateItem.props.info).toBe('Saturday, January 1');
+ });
+
+ it('displays correct due date when within a day', () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2022-01-01').getTime());
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ dueDate: 1640995200000, // 2022-01-01
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const dueDateItem = getByTestId('checklist_item.due_date');
+ expect(dueDateItem.props.info).toBe('Saturday, January 1 at 12:00 AM');
+
+ jest.useRealTimers();
+ });
+
+ it('displays "None" for due date when no due date is set', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ dueDate: 0,
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const dueDateItem = getByTestId('checklist_item.due_date');
+ expect(dueDateItem.props.info).toBe('None');
+ });
+
+ it('displays correct command information', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ command: 'test command',
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const commandItem = getByTestId('checklist_item.command');
+ expect(commandItem.props.info).toBe('test command');
+ });
+
+ it('displays "None" for command when no command is set', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ command: null,
+ });
+ const {getByTestId} = renderWithIntl();
+
+ const commandItem = getByTestId('checklist_item.command');
+ expect(commandItem.props.info).toBe('None');
+ });
+
+ it('handles user profile modal opening correctly', async () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const assigneeItem = getByTestId('checklist_item.assignee');
+ const onPress = assigneeItem.props.info.onPress;
+
+ await act(async () => {
+ onPress('user-1');
+ });
+
+ expect(openUserProfileModal).toHaveBeenCalledWith(
+ expect.anything(), // intl
+ expect.anything(), // theme
+ {
+ userId: 'user-1',
+ location: 'PlaybookRun',
+ },
+ );
+ });
+
+ it('displays correct assignee label and icon', () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const assigneeItem = getByTestId('checklist_item.assignee');
+ expect(assigneeItem.props.label).toBe('Assignee');
+ expect(assigneeItem.props.icon).toBe('account-multiple-plus-outline');
+ });
+
+ it('displays correct due date label and icon', () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const dueDateItem = getByTestId('checklist_item.due_date');
+ expect(dueDateItem.props.label).toBe('Due date');
+ expect(dueDateItem.props.icon).toBe('calendar-outline');
+ });
+
+ it('displays correct command label and icon', () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const commandItem = getByTestId('checklist_item.command');
+ expect(commandItem.props.label).toBe('Command');
+ expect(commandItem.props.icon).toBe('slash-forward');
+ });
+
+ it('displays correct button icons', () => {
+ const props = getBaseProps();
+ const {getByTestId} = renderWithIntl();
+
+ const checkButton = getByTestId('checklist_item.check_button');
+ const skipButton = getByTestId('checklist_item.skip_button');
+ const runCommandButton = getByTestId('checklist_item.run_command_button');
+
+ expect(checkButton.props.iconName).toBe('check');
+ expect(skipButton.props.iconName).toBe('close');
+ expect(runCommandButton.props.iconName).toBe('slash-forward');
+ });
+
+ it('does not render action buttons when isDisabled is true', () => {
+ const props = getBaseProps();
+ props.isDisabled = true;
+ const {queryByTestId} = renderWithIntl();
+
+ expect(queryByTestId('checklist_item.check_button')).toBeNull();
+ expect(queryByTestId('checklist_item.skip_button')).toBeNull();
+ expect(queryByTestId('checklist_item.run_command_button')).toBeNull();
+ });
+
+ it('does not render command when command is undefined', () => {
+ const props = getBaseProps();
+ props.item = TestHelper.fakePlaybookChecklistItemModel({
+ ...props.item,
+ command: undefined,
+ });
+ const {queryByTestId} = renderWithIntl();
+
+ expect(queryByTestId('checklist_item.run_command_button')).toBeNull();
+ });
+});
diff --git a/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item_bottom_sheet.tsx b/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item_bottom_sheet.tsx
new file mode 100644
index 000000000..d74dd6f09
--- /dev/null
+++ b/app/products/playbooks/screens/playbook_run/checklist/checklist_item/checklist_item_bottom_sheet.tsx
@@ -0,0 +1,274 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useMemo, type ComponentProps} from 'react';
+import {defineMessages, useIntl, type IntlShape} from 'react-intl';
+import {View, Text} from 'react-native';
+
+import MenuDivider from '@components/menu_divider';
+import OptionBox from '@components/option_box';
+import OptionItem, {ITEM_HEIGHT} from '@components/option_item';
+import {useTheme} from '@context/theme';
+import {dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
+import {toMilliseconds} from '@utils/datetime';
+import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+import Checkbox from './checkbox';
+
+import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
+import type UserModel from '@typings/database/models/servers/user';
+
+const messages = defineMessages({
+ check: {
+ id: 'playbooks.checklist_item.check',
+ defaultMessage: 'Check',
+ },
+ checked: {
+ id: 'playbooks.checklist_item.checked',
+ defaultMessage: 'Checked',
+ },
+ skip: {
+ id: 'playbooks.checklist_item.skip',
+ defaultMessage: 'Skip',
+ },
+ skipped: {
+ id: 'playbooks.checklist_item.skipped',
+ defaultMessage: 'Skipped',
+ },
+ runCommand: {
+ id: 'playbooks.checklist_item.run_command',
+ defaultMessage: 'Run command',
+ },
+ rerunCommand: {
+ id: 'playbooks.checklist_item.rerun_command',
+ defaultMessage: 'Rerun command',
+ },
+ assignee: {
+ id: 'playbooks.checklist_item.assignee',
+ defaultMessage: 'Assignee',
+ },
+ dueDate: {
+ id: 'playbooks.checklist_item.due_date',
+ defaultMessage: 'Due date',
+ },
+ command: {
+ id: 'playbooks.checklist_item.command',
+ defaultMessage: 'Command',
+ },
+ none: {
+ id: 'playbooks.checklist_item.none',
+ defaultMessage: 'None',
+ },
+ dateAtTime: {
+ id: 'playbooks.checklist_item.date_at_time',
+ defaultMessage: '{date} at {time}',
+ },
+});
+
+const ACTION_BUTTON_HEIGHT = 62;
+const N_OPTIONS = 3;
+const OPTIONS_GAP = 8;
+const SCROLL_CONTENT_GAP = 12;
+const TITLE_LINE_HEIGHT = 24; // From typography 300
+const BODY_LINE_HEIGHT = 24; // From typography 200
+const BODY_LINES_COUNT = 3;
+
+export const BOTTOM_SHEET_HEIGHT = {
+ base: (N_OPTIONS * ITEM_HEIGHT) + (OPTIONS_GAP * (N_OPTIONS - 1)) + (SCROLL_CONTENT_GAP * 2) + TITLE_LINE_HEIGHT + (BODY_LINE_HEIGHT * BODY_LINES_COUNT),
+ actionButtons: ACTION_BUTTON_HEIGHT + SCROLL_CONTENT_GAP,
+};
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
+ container: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg,
+ gap: SCROLL_CONTENT_GAP,
+ },
+ checkboxContainer: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: 12,
+ },
+ taskTitle: {
+ ...typography('Body', 300, 'Regular'),
+ color: theme.centerChannelColor,
+ },
+ taskDescription: {
+ ...typography('Body', 200, 'Regular'),
+ color: changeOpacity(theme.centerChannelColor, 0.72),
+ },
+ actionButtonsContainer: {
+ flexDirection: 'row',
+ gap: 12,
+ height: ACTION_BUTTON_HEIGHT,
+ },
+ taskDetailsContainer: {
+ gap: OPTIONS_GAP,
+ },
+ flex: {
+ flex: 1,
+ },
+}));
+
+type Props = {
+ item: PlaybookChecklistItemModel | PlaybookChecklistItem;
+ assignee?: UserModel;
+ onCheck: () => void;
+ onSkip: () => void;
+ onRunCommand: () => void;
+ teammateNameDisplay: string;
+ isDisabled: boolean;
+};
+
+function getDueDateInfo(intl: IntlShape, dueDate: number | undefined) {
+ if (!dueDate) {
+ return intl.formatMessage(messages.none);
+ }
+ const dateObject = new Date(dueDate);
+ const dateString = dateObject.toLocaleDateString(intl.locale, {month: 'long', day: 'numeric', weekday: 'long'});
+ if (Math.abs(dueDate - Date.now()) < toMilliseconds({days: 1})) {
+ const timeString = dateObject.toLocaleTimeString(intl.locale, {hour: '2-digit', minute: '2-digit'});
+ return intl.formatMessage(messages.dateAtTime, {date: dateString, time: timeString});
+ }
+ return dateString;
+}
+
+const ChecklistItemBottomSheet = ({
+ item,
+ assignee,
+ onCheck,
+ onSkip,
+ onRunCommand,
+ teammateNameDisplay,
+ isDisabled,
+}: Props) => {
+ const theme = useTheme();
+ const styles = getStyleSheet(theme);
+ const intl = useIntl();
+
+ const dueDate = 'dueDate' in item ? item.dueDate : item.due_date;
+ const isChecked = item.state === 'closed';
+ const isSkipped = item.state === 'skipped';
+ const isCommandRun = Boolean('commandLastRun' in item ? item.commandLastRun : item.command_last_run);
+
+ const handleCheck = useCallback(async () => {
+ await dismissBottomSheet();
+ onCheck?.();
+ }, [onCheck]);
+
+ const handleSkip = useCallback(async () => {
+ await dismissBottomSheet();
+ onSkip?.();
+ }, [onSkip]);
+
+ const handleRunCommand = useCallback(async () => {
+ await dismissBottomSheet();
+ onRunCommand?.();
+ }, [onRunCommand]);
+
+ const renderActionButtons = () => (
+
+
+
+ {Boolean(item.command) && (
+
+ )}
+
+ );
+
+ const onUserChipPress = useCallback((userId: string) => {
+ openUserProfileModal(intl, theme, {
+ userId,
+ location: 'PlaybookRun',
+ });
+ }, [intl, theme]);
+
+ const assigneeInfo: ComponentProps['info'] = useMemo(() => {
+ if (!assignee) {
+ return intl.formatMessage(messages.none);
+ }
+ return {
+ user: assignee,
+ onPress: onUserChipPress,
+ teammateNameDisplay,
+ location: 'PlaybookRun',
+ };
+ }, [assignee, intl, onUserChipPress, teammateNameDisplay]);
+
+ const renderTaskDetails = () => (
+
+
+
+
+
+ );
+
+ return (
+
+
+
+
+
+ {item.title}
+
+ {Boolean(item.description) && (
+
+ {item.description}
+
+ )}
+
+
+
+ {!isDisabled && renderActionButtons()}
+ {renderTaskDetails()}
+
+ );
+};
+
+export default ChecklistItemBottomSheet;
diff --git a/app/screens/bottom_sheet/index.tsx b/app/screens/bottom_sheet/index.tsx
index b6ea42eda..de8cdd072 100644
--- a/app/screens/bottom_sheet/index.tsx
+++ b/app/screens/bottom_sheet/index.tsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import BottomSheetM, {BottomSheetBackdrop, BottomSheetView, type BottomSheetBackdropProps} from '@gorhom/bottom-sheet';
+import BottomSheetM, {BottomSheetBackdrop, BottomSheetScrollView, BottomSheetView, type BottomSheetBackdropProps} from '@gorhom/bottom-sheet';
import React, {type ReactNode, useCallback, useEffect, useMemo, useRef} from 'react';
-import {DeviceEventEmitter, type Handle, InteractionManager, Keyboard, type StyleProp, View, type ViewStyle} from 'react-native';
+import {DeviceEventEmitter, type Handle, InteractionManager, Keyboard, ScrollView, type StyleProp, View, type ViewStyle} from 'react-native';
import {ReduceMotion, useReducedMotion, type WithSpringConfig} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
@@ -36,6 +36,7 @@ type Props = {
snapPoints?: Array;
enableDynamicSizing?: boolean;
testID?: string;
+ scrollable?: boolean;
}
const PADDING_TOP_MOBILE = 20;
@@ -98,6 +99,7 @@ const BottomSheet = ({
snapPoints = [1, '50%', '80%'],
testID,
enableDynamicSizing = false,
+ scrollable = false,
}: Props) => {
const reducedMotion = useReducedMotion();
const sheetRef = useRef(null);
@@ -150,7 +152,7 @@ const BottomSheet = ({
} else {
close();
}
- }, []);
+ }, [close]);
const handleChange = useCallback((index: number) => {
timeoutRef.current = setTimeout(() => {
@@ -163,7 +165,7 @@ const BottomSheet = ({
if (index <= 0) {
close();
}
- }, []);
+ }, [close]);
useAndroidHardwareBackHandler(componentId, handleClose);
useNavButtonPressed(closeButtonId || '', componentId, close, [close]);
@@ -217,6 +219,25 @@ const BottomSheet = ({
);
}
+ let content;
+ if (scrollable) {
+ const Scroll = isTablet ? ScrollView : BottomSheetScrollView;
+ content = (
+
+ {renderContainerContent()}
+
+ );
+ } else {
+ content = (
+
+ {renderContainerContent()}
+
+ );
+ }
+
return (
-
- {renderContainerContent()}
-
+ {content}
);
};
diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts
index f4c8210c4..2fd6de665 100644
--- a/app/screens/navigation.ts
+++ b/app/screens/navigation.ts
@@ -807,9 +807,10 @@ type BottomSheetArgs = {
snapPoints: Array;
theme: Theme;
title: string;
+ scrollable?: boolean;
}
-export function bottomSheet({title, renderContent, footerComponent, snapPoints, initialSnapIndex = 1, theme, closeButtonId}: BottomSheetArgs) {
+export function bottomSheet({title, renderContent, footerComponent, snapPoints, initialSnapIndex = 1, theme, closeButtonId, scrollable = false}: BottomSheetArgs) {
if (isTablet()) {
showModal(Screens.BOTTOM_SHEET, title, {
closeButtonId,
@@ -817,6 +818,7 @@ export function bottomSheet({title, renderContent, footerComponent, snapPoints,
renderContent,
footerComponent,
snapPoints,
+ scrollable,
}, bottomSheetModalOptions(theme, closeButtonId));
} else {
showModalOverCurrentContext(Screens.BOTTOM_SHEET, {
@@ -824,6 +826,7 @@ export function bottomSheet({title, renderContent, footerComponent, snapPoints,
renderContent,
footerComponent,
snapPoints,
+ scrollable,
}, bottomSheetModalOptions(theme));
}
}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index a7aad817c..5f962ce92 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -978,11 +978,23 @@
"pinned_messages.empty.title": "No pinned messages yet",
"playbook_run.checklist.dueIn": "Due {dueDate}",
"playbook_run.checklist.rerunCommand": "{command} (Rerun)",
+ "playbook_run.checklist.taskDetails": "Task Details",
"playbook_run.out_of_date_header.message": "Unable to connect to server. Content may be out of date. Last updated {lastUpdated}.",
"playbook.last_updated": "Last update {date}",
"playbook.participants": "Run Participants",
"playbook.runs.finished": "Finished",
"playbook.runs.in-progress": "In Progress",
+ "playbooks.checklist_item.assignee": "Assignee",
+ "playbooks.checklist_item.check": "Check",
+ "playbooks.checklist_item.checked": "Checked",
+ "playbooks.checklist_item.command": "Command",
+ "playbooks.checklist_item.date_at_time": "{date} at {time}",
+ "playbooks.checklist_item.due_date": "Due date",
+ "playbooks.checklist_item.none": "None",
+ "playbooks.checklist_item.rerun_command": "Rerun command",
+ "playbooks.checklist_item.run_command": "Run command",
+ "playbooks.checklist_item.skip": "Skip",
+ "playbooks.checklist_item.skipped": "Skipped",
"playbooks.fetch_error.description": "You don't have permission to view this run, or it may no longer exist.",
"playbooks.fetch_error.OK": "Okay",
"playbooks.fetch_error.title": "Unable to open Run",
diff --git a/test/setup.ts b/test/setup.ts
index 23204381e..5f18bb291 100644
--- a/test/setup.ts
+++ b/test/setup.ts
@@ -426,6 +426,7 @@ jest.mock('@screens/navigation', () => ({
dismissBottomSheet: jest.fn(),
openUserProfileModal: jest.fn(),
popTo: jest.fn(),
+ bottomSheet: jest.fn(),
}));
jest.mock('@mattermost/react-native-emm', () => ({