[MM-66610] Restore playbook run renaming (#9359)
* restore playbook run renaming * move rename function to modal * fix rebase * prevent saving a non-trimmed value * fix style * remove tests that are no longer needed
This commit is contained in:
parent
d51a4a6641
commit
c3a27a6bcf
10 changed files with 146 additions and 55 deletions
|
|
@ -374,4 +374,23 @@ describe('renameChecklist', () => {
|
|||
expect(updated).toBeDefined();
|
||||
expect(updated!.title).toBe(longTitle);
|
||||
});
|
||||
|
||||
it('should trim leading and trailing whitespace from title', async () => {
|
||||
const runId = 'runid';
|
||||
const checklist = {
|
||||
...TestHelper.createPlaybookChecklist(runId, 0, 0),
|
||||
run_id: runId,
|
||||
order: 0,
|
||||
};
|
||||
await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false});
|
||||
|
||||
const titleWithSpaces = ' Updated Checklist Title ';
|
||||
const {data, error} = await renameChecklist(serverUrl, checklist.id, titleWithSpaces);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistById(operator.database, checklist.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.title).toBe('Updated Checklist Title');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export async function renameChecklist(serverUrl: string, checklistId: string, ti
|
|||
|
||||
await database.write(async () => {
|
||||
checklist.update((c) => {
|
||||
c.title = title;
|
||||
c.title = title.trim();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -253,4 +253,20 @@ describe('renamePlaybookRun', () => {
|
|||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(longName);
|
||||
});
|
||||
|
||||
it('should trim leading and trailing whitespace from name', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const nameWithSpaces = ' Updated Run Name ';
|
||||
|
||||
const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, nameWithSpaces);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe('Updated Run Name');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export async function renamePlaybookRun(serverUrl: string, playbookRunId: string
|
|||
|
||||
await database.write(async () => {
|
||||
run.update((r) => {
|
||||
r.name = name;
|
||||
r.name = name.trim();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -545,9 +545,9 @@ describe('Playbooks Navigation', () => {
|
|||
describe('goToRenamePlaybookRun', () => {
|
||||
it('should navigate to rename playbook run screen with correct parameters', async () => {
|
||||
const currentTitle = 'Playbook Run Title';
|
||||
const onSave = jest.fn();
|
||||
const playbookRunId = 'run-id-123';
|
||||
|
||||
await goToRenamePlaybookRun(mockIntl, Preferences.THEMES.denim, currentTitle, onSave);
|
||||
await goToRenamePlaybookRun(mockIntl, Preferences.THEMES.denim, currentTitle, playbookRunId);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.playbook_run.rename.title',
|
||||
|
|
@ -558,7 +558,7 @@ describe('Playbooks Navigation', () => {
|
|||
'Rename playbook run',
|
||||
{
|
||||
currentTitle,
|
||||
onSave,
|
||||
playbookRunId,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -116,12 +116,12 @@ export async function goToRenamePlaybookRun(
|
|||
intl: IntlShape,
|
||||
theme: Theme,
|
||||
currentTitle: string,
|
||||
onSave: (newTitle: string) => void,
|
||||
playbookRunId: string,
|
||||
) {
|
||||
const title = intl.formatMessage({id: 'playbooks.playbook_run.rename.title', defaultMessage: 'Rename playbook run'});
|
||||
goToScreen(Screens.PLAYBOOK_RENAME_RUN, title, {
|
||||
currentTitle,
|
||||
onSave,
|
||||
playbookRunId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
import React, {act, type ComponentProps} from 'react';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import UserChip from '@components/chips/user_chip';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import UserAvatarsStack from '@components/user_avatars_stack';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
|
@ -17,7 +18,7 @@ import {fireEvent, renderWithEverything, waitFor} from '@test/intl-test-helper';
|
|||
import TestHelper from '@test/test_helper';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import {goToSelectUser} from '../navigation';
|
||||
import {goToRenamePlaybookRun, goToSelectUser} from '../navigation';
|
||||
|
||||
import ChecklistList from './checklist_list';
|
||||
import ErrorState from './error_state';
|
||||
|
|
@ -32,6 +33,14 @@ const serverUrl = 'some.server.url';
|
|||
|
||||
jest.mock('@utils/snack_bar');
|
||||
|
||||
jest.mock('@components/compass_icon', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(CompassIcon).mockImplementation(
|
||||
(props) => React.createElement('CompassIcon', {testID: 'compass-icon', ...props}) as any, // override the type since it is expecting a class component
|
||||
);
|
||||
|
||||
jest.mock('@context/server');
|
||||
jest.mocked(useServerUrl).mockReturnValue(serverUrl);
|
||||
|
||||
|
|
@ -66,12 +75,14 @@ jest.mocked(StatusUpdateIndicator).mockImplementation(
|
|||
);
|
||||
|
||||
jest.mock('../navigation', () => ({
|
||||
goToRenamePlaybookRun: jest.fn(),
|
||||
goToSelectUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@playbooks/actions/remote/runs', () => ({
|
||||
setOwner: jest.fn(),
|
||||
finishRun: jest.fn(),
|
||||
renamePlaybookRun: jest.fn(),
|
||||
setOwner: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@hooks/android_back_handler');
|
||||
|
|
@ -613,4 +624,37 @@ describe('PlaybookRun', () => {
|
|||
expect(queryByTestId('status-update-indicator')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders edit icon when not read only', () => {
|
||||
const props = getBaseProps();
|
||||
props.participants.push(TestHelper.fakeUserModel({id: props.currentUserId}));
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const editIcon = getByTestId('playbook-run.edit-icon');
|
||||
expect(editIcon).toBeTruthy();
|
||||
});
|
||||
|
||||
it('does not render edit icon when read only', () => {
|
||||
const props = getBaseProps();
|
||||
const {queryByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const editIcon = queryByTestId('playbook-run.edit-icon');
|
||||
expect(editIcon).toBeNull();
|
||||
});
|
||||
|
||||
it('handles edit icon press', () => {
|
||||
const props = getBaseProps();
|
||||
props.participants.push(TestHelper.fakeUserModel({id: props.currentUserId}));
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const editIcon = getByTestId('playbook-run.edit-icon');
|
||||
act(() => {
|
||||
fireEvent.press(editIcon);
|
||||
});
|
||||
expect(goToRenamePlaybookRun).toHaveBeenCalledWith(
|
||||
expect.anything(), // intl
|
||||
expect.anything(), // theme
|
||||
'Test Playbook Run',
|
||||
props.playbookRun!.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {View, Text, ScrollView, Alert} from 'react-native';
|
||||
import {View, Text, ScrollView, Alert, TouchableOpacity} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import Button from '@components/button';
|
||||
import UserChip from '@components/chips/user_chip';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Markdown from '@components/markdown';
|
||||
import Tag from '@components/tag';
|
||||
import UserAvatarsStack from '@components/user_avatars_stack';
|
||||
|
|
@ -22,7 +23,7 @@ import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
|||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import {goToSelectUser} from '../navigation';
|
||||
import {goToRenamePlaybookRun, goToSelectUser} from '../navigation';
|
||||
|
||||
import ChecklistList from './checklist_list';
|
||||
import ErrorState from './error_state';
|
||||
|
|
@ -258,25 +259,13 @@ export default function PlaybookRun({
|
|||
);
|
||||
}, [handleSelectOwner, intl, owner, participants, playbookRun?.name, theme]);
|
||||
|
||||
// this will be back once there is a rename function on the server side
|
||||
// const handleRename = useCallback(async (newName: string) => {
|
||||
// if (!playbookRun) {
|
||||
// return;
|
||||
// }
|
||||
const handleEditPress = useCallback(() => {
|
||||
if (!playbookRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
// const res = await renamePlaybookRun(serverUrl, playbookRun.id, newName);
|
||||
// if (res.error) {
|
||||
// showPlaybookErrorSnackbar();
|
||||
// }
|
||||
// }, [playbookRun, serverUrl]);
|
||||
|
||||
// const handleEditPress = useCallback(() => {
|
||||
// if (!playbookRun) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// goToRenamePlaybookRun(intl, theme, playbookRun.name, handleRename);
|
||||
// }, [intl, theme, playbookRun, handleRename]);
|
||||
goToRenamePlaybookRun(intl, theme, playbookRun.name, playbookRun.id);
|
||||
}, [intl, theme, playbookRun]);
|
||||
|
||||
const handleFinishRun = useCallback(() => {
|
||||
if (!playbookRun) {
|
||||
|
|
@ -333,13 +322,17 @@ export default function PlaybookRun({
|
|||
<View style={styles.titleAndDescription}>
|
||||
<View style={styles.titleRow}>
|
||||
<Text style={styles.title}>{playbookRun.name}</Text>
|
||||
{/* This will be back once there is a rename function on the server side
|
||||
<TouchableOpacity onPress={handleEditPress}>
|
||||
<CompassIcon
|
||||
name='pencil-outline'
|
||||
style={styles.editIcon}
|
||||
/>
|
||||
</TouchableOpacity> */}
|
||||
{!readOnly && (
|
||||
<TouchableOpacity
|
||||
onPress={handleEditPress}
|
||||
testID='playbook-run.edit-icon'
|
||||
>
|
||||
<CompassIcon
|
||||
name='pencil-outline'
|
||||
style={styles.editIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
{isFinished && (
|
||||
<Tag
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {Keyboard} from 'react-native';
|
|||
import {Preferences} from '@constants';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {renamePlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
|
||||
|
|
@ -32,11 +33,20 @@ jest.mock('@hooks/android_back_handler', () => jest.fn());
|
|||
jest.mock('@managers/security_manager', () => ({
|
||||
getShieldScreenId: jest.fn((id) => `shield-${id}`),
|
||||
}));
|
||||
jest.mock('@playbooks/actions/remote/runs', () => ({
|
||||
renamePlaybookRun: jest.fn(),
|
||||
}));
|
||||
jest.mock('@utils/snack_bar', () => ({
|
||||
showPlaybookErrorSnackbar: jest.fn(),
|
||||
}));
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn(() => 'some.server.url'),
|
||||
}));
|
||||
|
||||
describe('RenamePlaybookRunBottomSheet', () => {
|
||||
const componentId = 'test-component-id' as any;
|
||||
const currentTitle = 'Original Playbook Run';
|
||||
const mockOnSave = jest.fn();
|
||||
const playbookRunId = 'run-id-123';
|
||||
|
||||
const mockRightButton = {
|
||||
id: 'save-playbook-run-name',
|
||||
|
|
@ -47,6 +57,7 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(buildNavigationButton).mockReturnValue(mockRightButton as any);
|
||||
jest.mocked(renamePlaybookRun).mockResolvedValue({data: true});
|
||||
jest.mocked(useNavButtonPressed).mockImplementation((buttonId, compId, callback) => {
|
||||
// Simulate button press when buttonId matches
|
||||
if (buttonId === 'save-playbook-run-name' && compId === componentId) {
|
||||
|
|
@ -64,7 +75,7 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
return {
|
||||
componentId,
|
||||
currentTitle,
|
||||
onSave: mockOnSave,
|
||||
playbookRunId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +259,7 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should call onSave and close when save button is pressed with valid title', () => {
|
||||
it('should call renamePlaybookRun and close when save button is pressed with valid title', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
|
|
@ -262,16 +273,16 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
|
||||
// Trigger save button press
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
act(() => {
|
||||
saveCallback();
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith(newTitle);
|
||||
expect(renamePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, newTitle);
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should trim title when saving', () => {
|
||||
it('should trim title when saving', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
|
|
@ -285,12 +296,12 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
|
||||
// Trigger save button press
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
act(() => {
|
||||
saveCallback();
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('New Playbook Run Name');
|
||||
expect(mockOnSave).not.toHaveBeenCalledWith(titleWithSpaces);
|
||||
expect(renamePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, 'New Playbook Run Name');
|
||||
expect(renamePlaybookRun).not.toHaveBeenCalledWith('some.server.url', playbookRunId, titleWithSpaces);
|
||||
});
|
||||
|
||||
it('should close when Android back button is pressed', () => {
|
||||
|
|
@ -305,7 +316,7 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
expect(renamePlaybookRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update navigation button when canSave changes', () => {
|
||||
|
|
|
|||
|
|
@ -6,18 +6,21 @@ import {useIntl} from 'react-intl';
|
|||
import {Keyboard, StyleSheet, View} from 'react-native';
|
||||
|
||||
import FloatingTextInput from '@components/floating_input/floating_text_input_label';
|
||||
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 SecurityManager from '@managers/security_manager';
|
||||
import {renamePlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
currentTitle: string;
|
||||
onSave: (newTitle: string) => void;
|
||||
playbookRunId: string;
|
||||
}
|
||||
|
||||
const SAVE_BUTTON_ID = 'save-playbook-run-name';
|
||||
|
|
@ -38,11 +41,12 @@ const styles = StyleSheet.create({
|
|||
const RenamePlaybookRunBottomSheet = ({
|
||||
componentId,
|
||||
currentTitle,
|
||||
onSave,
|
||||
playbookRunId,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const [title, setTitle] = useState<string>(currentTitle);
|
||||
|
||||
|
|
@ -72,12 +76,16 @@ const RenamePlaybookRunBottomSheet = ({
|
|||
close(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const handleSave = useCallback(async () => {
|
||||
if (canSave) {
|
||||
onSave(title.trim());
|
||||
close(componentId);
|
||||
const res = await renamePlaybookRun(serverUrl, playbookRunId, title.trim());
|
||||
if (res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
} else {
|
||||
close(componentId);
|
||||
}
|
||||
}
|
||||
}, [canSave, title, componentId, onSave]);
|
||||
}, [canSave, title, componentId, serverUrl, playbookRunId]);
|
||||
|
||||
useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]);
|
||||
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||
|
|
|
|||
Loading…
Reference in a new issue