Add subtitles for playbook bottom sheet screens to add the run name (#9130) (#9174)

* Add subtitles for playbook bottom sheet screens to add the run name

* Add missing change

(cherry picked from commit f384ddb65d)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
Mattermost Build 2025-10-01 10:46:47 +03:00 committed by GitHub
parent 910aa154b6
commit a1fb84cf4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 145 additions and 47 deletions

View file

@ -6,7 +6,7 @@ import {goToScreen} from '@screens/navigation';
import TestHelper from '@test/test_helper';
import {changeOpacity} from '@utils/theme';
import {goToPlaybookRuns, goToPlaybookRun, goToSelectUser, goToSelectDate} from './navigation';
import {goToPlaybookRuns, goToPlaybookRun, goToEditCommand, goToSelectUser, goToSelectDate} from './navigation';
jest.mock('@screens/navigation', () => ({
goToScreen: jest.fn(),
@ -65,6 +65,38 @@ describe('Playbooks Navigation', () => {
});
});
describe('goToEditCommand', () => {
it('should navigate to edit command screen with correct parameters', async () => {
const command = 'test command';
const channelId = 'channel-id-1';
const updateCommand = jest.fn();
await goToEditCommand(mockIntl, Preferences.THEMES.denim, 'Run 1', command, channelId, updateCommand);
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
id: 'playbooks.edit_command.title',
defaultMessage: 'Slash command',
});
expect(goToScreen).toHaveBeenCalledWith(
Screens.PLAYBOOK_EDIT_COMMAND,
'Slash command',
{
savedCommand: command,
updateCommand,
channelId,
},
{
topBar: {
subtitle: {
text: 'Run 1',
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
},
},
},
);
});
});
describe('goToSelectUser', () => {
it('should navigate to select user screen with correct parameters when handleRemove is provided', async () => {
const title = 'Select User';
@ -73,7 +105,7 @@ describe('Playbooks Navigation', () => {
const handleSelect = jest.fn();
const handleRemove = jest.fn();
await goToSelectUser(title, participantIds, selected, handleSelect, handleRemove);
await goToSelectUser(Preferences.THEMES.denim, 'Run 1', title, participantIds, selected, handleSelect, handleRemove);
expect(goToScreen).toHaveBeenCalledWith(
Screens.PLAYBOOK_SELECT_USER,
@ -84,7 +116,14 @@ describe('Playbooks Navigation', () => {
handleSelect,
handleRemove,
},
{},
{
topBar: {
subtitle: {
text: 'Run 1',
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
},
},
},
);
});
});
@ -94,7 +133,7 @@ describe('Playbooks Navigation', () => {
const onSave = jest.fn();
const selectedDate = 1640995200000; // January 1, 2022
await goToSelectDate(mockIntl, onSave, selectedDate);
await goToSelectDate(mockIntl, Preferences.THEMES.denim, 'Run 1', onSave, selectedDate);
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
id: 'playbooks.select_date.title',
@ -107,14 +146,21 @@ describe('Playbooks Navigation', () => {
onSave,
selectedDate,
},
{},
{
topBar: {
subtitle: {
text: 'Run 1',
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
},
},
},
);
});
it('should navigate to select date screen without selected date', async () => {
const onSave = jest.fn();
await goToSelectDate(mockIntl, onSave, undefined);
await goToSelectDate(mockIntl, Preferences.THEMES.denim, 'Run 1', onSave, undefined);
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
id: 'playbooks.select_date.title',
@ -127,7 +173,14 @@ describe('Playbooks Navigation', () => {
onSave,
selectedDate: undefined,
},
{},
{
topBar: {
subtitle: {
text: 'Run 1',
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
},
},
},
);
});
});

View file

@ -6,6 +6,7 @@ import {getThemeFromState, goToScreen} from '@screens/navigation';
import {changeOpacity} from '@utils/theme';
import type {IntlShape} from 'react-intl';
import type {Options as RNNOptions} from 'react-native-navigation';
export function goToPlaybookRuns(intl: IntlShape, channelId: string, channelName: string) {
const theme = getThemeFromState();
@ -25,29 +26,63 @@ export async function goToPlaybookRun(intl: IntlShape, playbookRunId: string, pl
goToScreen(Screens.PLAYBOOK_RUN, title, {playbookRunId, playbookRun}, {});
}
function getSubtitleOptions(theme: Theme, runName: string): RNNOptions {
return {
topBar: {
subtitle: {
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
text: runName,
},
},
};
}
export async function goToEditCommand(
intl: IntlShape,
theme: Theme,
runName: string,
command: string | null,
channelId: string,
updateCommand: (command: string) => void,
) {
const title = intl.formatMessage({id: 'playbooks.edit_command.title', defaultMessage: 'Slash command'});
const options = getSubtitleOptions(theme, runName);
goToScreen(Screens.PLAYBOOK_EDIT_COMMAND, title, {
savedCommand: command,
updateCommand,
channelId,
}, options);
}
export async function goToSelectUser(
theme: Theme,
runName: string,
title: string,
participantIds: string[],
selected: string | undefined,
handleSelect: (user: UserProfile) => void,
handleRemove?: () => void,
) {
const options = getSubtitleOptions(theme, runName);
goToScreen(Screens.PLAYBOOK_SELECT_USER, title, {
participantIds,
selected,
handleSelect,
handleRemove,
}, {});
}, options);
}
export async function goToSelectDate(
intl: IntlShape,
theme: Theme,
runName: string,
onSave: (date: number | undefined) => void,
selectedDate: number | undefined,
) {
const options = getSubtitleOptions(theme, runName);
const title = intl.formatMessage({id: 'playbooks.select_date.title', defaultMessage: 'Due date'});
goToScreen(Screens.PLAYBOOKS_SELECT_DATE, title, {
onSave,
selectedDate,
}, {});
}, options);
}

View file

@ -10,16 +10,14 @@ import OptionBox from '@components/option_box';
import OptionItem from '@components/option_item';
import {useIsTablet} from '@hooks/device';
import {setAssignee, setChecklistItemCommand, setDueDate} from '@playbooks/actions/remote/checklist';
import {goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
import {dismissBottomSheet, goToScreen, openUserProfileModal} from '@screens/navigation';
import {goToEditCommand, goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
import {dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
import {renderWithIntl} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
import ChecklistItemBottomSheet from './checklist_item_bottom_sheet';
import type EditCommand from '@playbooks/screens/edit_command';
jest.mock('@hooks/device', () => ({
useIsTablet: jest.fn(),
}));
@ -81,6 +79,7 @@ describe('ChecklistItemBottomSheet', () => {
function getBaseProps(): ComponentProps<typeof ChecklistItemBottomSheet> {
return {
runId: 'run-1',
runName: 'Run 1',
checklistNumber: 1,
itemNumber: 1,
channelId: 'channel-1',
@ -400,17 +399,16 @@ describe('ChecklistItemBottomSheet', () => {
commandItem.props.action();
});
expect(goToScreen).toHaveBeenCalledWith(
'PlaybookEditCommand',
'Slash command',
{
savedCommand: 'test command',
updateCommand: expect.any(Function),
channelId: 'channel-1',
},
expect(goToEditCommand).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
'Run 1',
'test command',
'channel-1',
expect.any(Function),
);
const updateCommand = (jest.mocked(goToScreen).mock.calls[0][2] as ComponentProps<typeof EditCommand>).updateCommand;
const updateCommand = jest.mocked(goToEditCommand).mock.calls[0][5];
await act(async () => {
updateCommand('new command');
});
@ -440,11 +438,13 @@ describe('ChecklistItemBottomSheet', () => {
});
expect(goToSelectDate).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
'Run 1',
expect.any(Function),
1640995200000,
);
const setDate = jest.mocked(goToSelectDate).mock.calls[0][1];
const setDate = jest.mocked(goToSelectDate).mock.calls[0][3];
await act(async () => {
setDate(1672531200000);
});
@ -501,6 +501,8 @@ describe('ChecklistItemBottomSheet', () => {
});
expect(goToSelectUser).toHaveBeenCalledWith(
expect.anything(),
'Run 1',
'Assignee',
['user-1', 'user-2'],
'user-1',
@ -508,8 +510,8 @@ describe('ChecklistItemBottomSheet', () => {
expect.any(Function),
);
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
const handleRemove = jest.mocked(goToSelectUser).mock.calls[0][4];
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][5];
const handleRemove = jest.mocked(goToSelectUser).mock.calls[0][6];
await act(async () => {
handleSelect(TestHelper.fakeUser({id: 'user-1'}));
@ -552,8 +554,8 @@ describe('ChecklistItemBottomSheet', () => {
onPress('user-1');
});
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
const handleRemove = jest.mocked(goToSelectUser).mock.calls[0][4];
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][5];
const handleRemove = jest.mocked(goToSelectUser).mock.calls[0][6];
await act(async () => {
handleSelect(TestHelper.fakeUser({id: 'user-1'}));

View file

@ -11,9 +11,9 @@ import OptionItem, {ITEM_HEIGHT} from '@components/option_item';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {setAssignee, setChecklistItemCommand, setDueDate} from '@playbooks/actions/remote/checklist';
import {goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
import {goToEditCommand, goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
import {getDueDateString} from '@playbooks/utils/time';
import {dismissBottomSheet, goToScreen, openUserProfileModal} from '@screens/navigation';
import {dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
@ -114,6 +114,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
type Props = {
runId: string;
runName: string;
checklistNumber: number;
itemNumber: number;
channelId: string;
@ -130,6 +131,7 @@ type Props = {
const ChecklistItemBottomSheet = ({
runId,
runName,
checklistNumber,
itemNumber,
channelId,
@ -213,16 +215,8 @@ const ChecklistItemBottomSheet = ({
}, [checklistNumber, item.id, itemNumber, runId, serverUrl]);
const openEditCommandModal = useCallback(() => {
goToScreen(
'PlaybookEditCommand',
intl.formatMessage({id: 'playbooks.edit_command.title', defaultMessage: 'Slash command'}),
{
savedCommand: item.command,
updateCommand,
channelId,
},
);
}, [intl, item.command, updateCommand, channelId]);
goToEditCommand(intl, theme, runName, item.command, channelId, updateCommand);
}, [intl, theme, runName, item.command, channelId, updateCommand]);
const assigneeInfo: ComponentProps<typeof OptionItem>['info'] = useMemo(() => {
if (!assignee) {
@ -237,10 +231,10 @@ const ChecklistItemBottomSheet = ({
}, [assignee, intl, onUserChipPress, teammateNameDisplay]);
const openEditDateModal = useCallback(async () => {
goToSelectDate(intl, (date) => {
goToSelectDate(intl, theme, runName, (date) => {
setDueDate(serverUrl, runId, item.id, checklistNumber, itemNumber, date);
}, dueDate);
}, [intl, dueDate, serverUrl, runId, item.id, checklistNumber, itemNumber]);
}, [intl, theme, runName, dueDate, serverUrl, runId, item.id, checklistNumber, itemNumber]);
const handleSelect = useCallback(async (selected: UserProfile) => {
const res = await setAssignee(serverUrl, runId, item.id, checklistNumber, itemNumber, selected.id);
@ -258,13 +252,15 @@ const ChecklistItemBottomSheet = ({
const openUserSelector = useCallback(() => {
goToSelectUser(
theme,
runName,
intl.formatMessage(messages.assignee),
participantIds,
assignee?.id,
handleSelect,
handleRemove,
);
}, [assignee?.id, handleRemove, handleSelect, intl, participantIds]);
}, [assignee?.id, handleRemove, handleSelect, intl, participantIds, runName, theme]);
const renderTaskDetails = () => (
<View style={styles.taskDetailsContainer}>

View file

@ -84,7 +84,7 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
describe('with database model item', () => {
it('should render enhanced component with database model item', async () => {
const rawItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-1', assignee_id: 'user-1'});
const rawRun = TestHelper.fakePlaybookRun({id: 'run-1', participant_ids: ['user-1', 'user-2']});
const rawRun = TestHelper.fakePlaybookRun({id: 'run-1', participant_ids: ['user-1', 'user-2'], name: 'Run 1'});
await addRunToDatabase(rawRun);
await addItemToDatabase(rawItem);
await addUserToDatabase(TestHelper.fakeUser({id: 'user-1'}));
@ -103,6 +103,7 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
expect(bottomSheet).toHaveProp('currentUserTimezone', expect.objectContaining({useAutomaticTimezone: false, manualTimezone: 'America/New_York', automaticTimezone: 'America/New_York'}));
expect(bottomSheet).toHaveProp('participantIds', ['user-1', 'user-2']);
expect(bottomSheet).toHaveProp('runName', 'Run 1');
});
it('should handle missing run correctly', async () => {
@ -120,6 +121,7 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
expect(bottomSheet).toBeTruthy();
expect(bottomSheet).toHaveProp('participantIds', []);
expect(bottomSheet).toHaveProp('runName', '');
});
it('should handle assigneeId changes through observable', async () => {

View file

@ -40,6 +40,9 @@ const enhanced = withObservables(['item', 'runId'], ({item, runId, database}: Ow
participantIds: observePlaybookRunById(database, runId).pipe(
switchMap((run) => observeParticipantsIdsFromPlaybookModel(run, true)),
),
runName: observePlaybookRunById(database, runId).pipe(
switchMap((run) => of$(run?.name || '')),
),
};
}
@ -50,6 +53,7 @@ const enhanced = withObservables(['item', 'runId'], ({item, runId, database}: Ow
assignee,
currentUserTimezone,
participantIds: of$([]),
runName: of$(''),
};
});

View file

@ -217,6 +217,8 @@ describe('PlaybookRun', () => {
ownerChip.props.action.onPress();
expect(goToSelectUser).toHaveBeenCalledWith(
expect.anything(),
'Test Playbook Run',
'Owner',
[...props.participants.map((p) => p.id), props.owner!.id],
props.owner!.id,
@ -224,7 +226,7 @@ describe('PlaybookRun', () => {
);
expect(openUserProfileModal).not.toHaveBeenCalled();
let handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
let handleSelect = jest.mocked(goToSelectUser).mock.calls[0][5];
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
expect(setOwner).toHaveBeenCalledWith(
@ -241,6 +243,8 @@ describe('PlaybookRun', () => {
ownerChip.props.onPress();
expect(goToSelectUser).toHaveBeenCalledWith(
expect.anything(),
'Test Playbook Run',
'Owner',
[...props.participants.map((p) => p.id), props.owner!.id],
props.owner!.id,
@ -248,7 +252,7 @@ describe('PlaybookRun', () => {
);
expect(openUserProfileModal).not.toHaveBeenCalled();
handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
handleSelect = jest.mocked(goToSelectUser).mock.calls[0][5];
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
expect(setOwner).toHaveBeenCalledWith(
@ -268,7 +272,7 @@ describe('PlaybookRun', () => {
const ownerChip = getByTestId('user-chip');
ownerChip.props.onPress();
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][5];
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
await waitFor(() => {

View file

@ -200,12 +200,14 @@ export default function PlaybookRun({
}
goToSelectUser(
theme,
playbookRun?.name || '',
intl.formatMessage(messages.owner),
[...participants.map((p) => p.id), owner?.id || ''],
owner?.id,
handleSelectOwner,
);
}, [handleSelectOwner, intl, owner, participants]);
}, [handleSelectOwner, intl, owner, participants, playbookRun?.name, theme]);
const ownerAction = useMemo(() => {
if (readOnly) {