Add select user screen to select owner and task assignee (#9089)
* Add select user screen to select owner and task assignee * Fix i18n * Add tests * Address feedback * Fix test * Address UX feedback * Fix test * Put the no assignee button inline with the search
This commit is contained in:
parent
530cbb4ba5
commit
a5d7bf7db4
56 changed files with 2138 additions and 141 deletions
|
|
@ -28,26 +28,39 @@ describe('BaseChip', () => {
|
|||
expect(getByText('Test Label')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with the X button when showRemoveOption is true', () => {
|
||||
it('should render with the X button when actionIcon is remove', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
showRemoveOption={true}
|
||||
actionIcon='remove'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('base_chip.remove.button')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render the X button when showRemoveOption is false', () => {
|
||||
it('should render with the chevron down button when actionIcon is downArrow', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
actionIcon='downArrow'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('base_chip.downArrow.button')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render the X button when actionIcon is undefined', () => {
|
||||
const {queryByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
showRemoveOption={false}
|
||||
actionIcon={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
|
@ -60,7 +73,7 @@ describe('BaseChip', () => {
|
|||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
showRemoveOption={true}
|
||||
actionIcon='remove'
|
||||
/>,
|
||||
);
|
||||
|
||||
|
|
@ -74,7 +87,7 @@ describe('BaseChip', () => {
|
|||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
showRemoveOption={false}
|
||||
actionIcon={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {CHIP_HEIGHT} from './constants';
|
|||
type SelectedChipProps = {
|
||||
onPress?: () => void;
|
||||
testID?: string;
|
||||
showRemoveOption?: boolean;
|
||||
actionIcon?: 'remove' | 'downArrow';
|
||||
showAnimation?: boolean;
|
||||
label: string;
|
||||
prefix?: JSX.Element;
|
||||
|
|
@ -70,7 +70,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
export default function BaseChip({
|
||||
testID,
|
||||
onPress,
|
||||
showRemoveOption,
|
||||
actionIcon,
|
||||
showAnimation,
|
||||
label,
|
||||
prefix,
|
||||
|
|
@ -85,7 +85,7 @@ export default function BaseChip({
|
|||
// We set the max width to 70% of the screen width to make sure
|
||||
// text like names get ellipsized correctly.
|
||||
const textMaxWidth = maxWidth || dimensions.width * 0.70;
|
||||
const marginRight = showRemoveOption ? undefined : 7;
|
||||
const marginRight = actionIcon ? undefined : 7;
|
||||
const marginLeft = prefix ? 5 : 7;
|
||||
return [
|
||||
style.text,
|
||||
|
|
@ -94,7 +94,7 @@ export default function BaseChip({
|
|||
type === 'danger' && style.dangerText,
|
||||
boldText && style.boldText,
|
||||
];
|
||||
}, [maxWidth, dimensions.width, showRemoveOption, prefix, style.text, style.linkText, style.dangerText, style.boldText, type, boldText]);
|
||||
}, [maxWidth, dimensions.width, actionIcon, prefix, style.text, style.linkText, style.dangerText, style.boldText, type, boldText]);
|
||||
const containerStyle = useMemo(() => {
|
||||
return [
|
||||
style.container,
|
||||
|
|
@ -116,19 +116,20 @@ export default function BaseChip({
|
|||
);
|
||||
|
||||
let content = chipContent;
|
||||
if (showRemoveOption) {
|
||||
if (actionIcon) {
|
||||
const iconName = actionIcon === 'remove' ? 'close-circle' : 'chevron-down';
|
||||
content = (
|
||||
<>
|
||||
{chipContent}
|
||||
<TouchableOpacity
|
||||
style={style.remove}
|
||||
onPress={onPress}
|
||||
testID={`${testID}.remove.button`}
|
||||
testID={`${testID}.${actionIcon}.button`}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close-circle'
|
||||
name={iconName}
|
||||
size={16}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe('SelectedChip', () => {
|
|||
|
||||
const baseChip = getByTestId('selected-chip');
|
||||
expect(baseChip.props.label).toBe('Test Chip');
|
||||
expect(baseChip.props.showRemoveOption).toBe(true);
|
||||
expect(baseChip.props.actionIcon).toBe('remove');
|
||||
expect(baseChip.props.showAnimation).toBe(true);
|
||||
expect(baseChip.props.prefix).toBeUndefined();
|
||||
baseChip.props.onPress();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default function SelectedChip({
|
|||
<BaseChip
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
showRemoveOption={true}
|
||||
actionIcon='remove'
|
||||
showAnimation={true}
|
||||
label={text}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe('SelectedUserChip', () => {
|
|||
const userChip = getByTestId('selected-user-chip');
|
||||
expect(userChip.props.user).toBe(mockUser);
|
||||
expect(userChip.props.teammateNameDisplay).toBe('username');
|
||||
expect(userChip.props.showRemoveOption).toBe(true);
|
||||
expect(userChip.props.actionIcon).toBe('remove');
|
||||
expect(userChip.props.showAnimation).toBe(true);
|
||||
userChip.props.onPress();
|
||||
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default function SelectedUserChip({
|
|||
<UserChip
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
showRemoveOption={true}
|
||||
actionIcon='remove'
|
||||
showAnimation={true}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
user={user}
|
||||
|
|
|
|||
86
app/components/chips/selected_user_chip_by_id/index.test.tsx
Normal file
86
app/components/chips/selected_user_chip_by_id/index.test.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SelectedUserChipByIdComponent from './selected_user_chip_by_id';
|
||||
|
||||
import SelectedUserChipById from './index';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
const serverUrl = 'server-url';
|
||||
|
||||
jest.mock('./selected_user_chip_by_id', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(SelectedUserChipByIdComponent).mockImplementation((props) => React.createElement('SelectedUserChipById', {...props}));
|
||||
|
||||
describe('SelectedUserChipById Enhanced Component', () => {
|
||||
const userId = 'user-id';
|
||||
|
||||
let database: Database;
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof SelectedUserChipById> {
|
||||
return {
|
||||
onPress: jest.fn(),
|
||||
teammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME,
|
||||
userId,
|
||||
testID: 'test-chip',
|
||||
};
|
||||
}
|
||||
|
||||
async function addUserToDatabase(user: UserProfile) {
|
||||
await operator.handleUsers({
|
||||
users: [user],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = serverDatabaseAndOperator.database;
|
||||
operator = serverDatabaseAndOperator.operator;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
describe('enhanced component behavior', () => {
|
||||
it('should render correctly with no user data initially', () => {
|
||||
const baseProps = getBaseProps();
|
||||
baseProps.userId = 'non-existent-user-id';
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<SelectedUserChipById {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
const chip = getByTestId('test-chip');
|
||||
expect(chip.props.user).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should render correctly with user data after database population', async () => {
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: userId}));
|
||||
const baseProps = getBaseProps();
|
||||
baseProps.userId = userId;
|
||||
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<SelectedUserChipById {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
const chip = getByTestId('test-chip');
|
||||
expect(chip).toHaveProp('user', expect.objectContaining({id: userId}));
|
||||
});
|
||||
});
|
||||
});
|
||||
20
app/components/chips/selected_user_chip_by_id/index.ts
Normal file
20
app/components/chips/selected_user_chip_by_id/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
|
||||
import SelectedUserChipById from './selected_user_chip_by_id';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = WithDatabaseArgs & {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['userId'], ({database, userId}: OwnProps) => ({
|
||||
user: observeUser(database, userId),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(SelectedUserChipById));
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import SelectedUserChip from '@components/chips/selected_user_chip';
|
||||
import {General} from '@constants';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SelectedUserChipById from './selected_user_chip_by_id';
|
||||
|
||||
// Mock the SelectedUserChip component
|
||||
jest.mock('@components/chips/selected_user_chip', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(SelectedUserChip).mockImplementation((props) => React.createElement('SelectedUserChip', {...props}));
|
||||
|
||||
describe('SelectedUserChipById', () => {
|
||||
function getBaseProps(): ComponentProps<typeof SelectedUserChipById> {
|
||||
return {
|
||||
onPress: jest.fn(),
|
||||
teammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME,
|
||||
testID: 'test-selected-user-chip',
|
||||
};
|
||||
}
|
||||
|
||||
it('should render SelectedUserChip with correct props', () => {
|
||||
const user = TestHelper.fakeUserModel({id: 'user-123'});
|
||||
const props = getBaseProps();
|
||||
props.user = user;
|
||||
|
||||
const {getByTestId} = renderWithIntl(
|
||||
<SelectedUserChipById {...props}/>,
|
||||
);
|
||||
|
||||
const chip = getByTestId('test-selected-user-chip');
|
||||
expect(chip).toHaveProp('user', user);
|
||||
expect(chip).toHaveProp('onPress', props.onPress);
|
||||
expect(chip).toHaveProp('teammateNameDisplay', props.teammateNameDisplay);
|
||||
});
|
||||
|
||||
it('should handle missing user', () => {
|
||||
const props = getBaseProps();
|
||||
|
||||
const {queryByTestId} = renderWithIntl(
|
||||
<SelectedUserChipById {...props}/>,
|
||||
);
|
||||
|
||||
expect(queryByTestId('test-selected-user-chip')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import SelectedUserChip from '@components/chips/selected_user_chip';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type SelectedChipProps = {
|
||||
user?: UserModel;
|
||||
onPress: (id: string) => void;
|
||||
testID?: string;
|
||||
teammateNameDisplay: string;
|
||||
}
|
||||
|
||||
export default function SelectedUserChipById({
|
||||
testID,
|
||||
user,
|
||||
teammateNameDisplay,
|
||||
onPress,
|
||||
}: SelectedChipProps) {
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectedUserChip
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -33,14 +33,14 @@ describe('UserChip', () => {
|
|||
onPress={mockOnPress}
|
||||
testID='user-chip'
|
||||
teammateNameDisplay='username'
|
||||
showRemoveOption={true}
|
||||
showAnimation={true}
|
||||
actionIcon='remove'
|
||||
/>,
|
||||
);
|
||||
|
||||
const baseChip = getByTestId('user-chip');
|
||||
expect(baseChip.props.label).toBe('test-user');
|
||||
expect(baseChip.props.showRemoveOption).toBe(true);
|
||||
expect(baseChip.props.actionIcon).toBe('remove');
|
||||
expect(baseChip.props.showAnimation).toBe(true);
|
||||
|
||||
expect(baseChip.props.prefix).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ type SelectedChipProps = {
|
|||
onPress: (id: string) => void;
|
||||
testID?: string;
|
||||
teammateNameDisplay: string;
|
||||
showRemoveOption?: boolean;
|
||||
actionIcon?: 'remove' | 'downArrow';
|
||||
showAnimation?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ export default function UserChip({
|
|||
user,
|
||||
teammateNameDisplay,
|
||||
onPress: receivedOnPress,
|
||||
showRemoveOption,
|
||||
actionIcon,
|
||||
showAnimation,
|
||||
}: SelectedChipProps) {
|
||||
const intl = useIntl();
|
||||
|
|
@ -49,7 +49,7 @@ export default function UserChip({
|
|||
<BaseChip
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
showRemoveOption={showRemoveOption}
|
||||
actionIcon={actionIcon}
|
||||
showAnimation={showAnimation}
|
||||
label={name}
|
||||
prefix={picture}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
|||
|
||||
import Button from '@components/button';
|
||||
import {CHIP_HEIGHT} from '@components/chips/constants';
|
||||
import SelectedUserChip from '@components/chips/selected_user_chip';
|
||||
import SelectedUserChipById from '@components/chips/selected_user_chip_by_id';
|
||||
import Toast from '@components/toast';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet, useKeyboardHeightWithDuration} from '@hooks/device';
|
||||
|
|
@ -44,7 +44,7 @@ type Props = {
|
|||
/**
|
||||
* An object mapping user ids to a falsey value indicating whether or not they have been selected.
|
||||
*/
|
||||
selectedIds: {[id: string]: UserProfile};
|
||||
selectedIds: Set<string>;
|
||||
|
||||
/**
|
||||
* callback to set the value of showToast
|
||||
|
|
@ -165,17 +165,13 @@ export default function SelectedUsers({
|
|||
|
||||
const users = useMemo(() => {
|
||||
const u = [];
|
||||
for (const user of Object.values(selectedIds)) {
|
||||
if (!user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const userItemTestID = `${testID}.${user.id}`;
|
||||
for (const userId of selectedIds) {
|
||||
const userItemTestID = `${testID}.${userId}`;
|
||||
|
||||
u.push(
|
||||
<SelectedUserChip
|
||||
key={user.id}
|
||||
user={user}
|
||||
<SelectedUserChipById
|
||||
key={userId}
|
||||
userId={userId}
|
||||
onPress={onRemove}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
testID={userItemTestID}
|
||||
|
|
|
|||
|
|
@ -10,18 +10,20 @@ import {debounce} from '@helpers/api/general';
|
|||
import {filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
import type {SectionListData} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
currentUserId: string;
|
||||
tutorialWatched: boolean;
|
||||
handleSelectProfile: (user: UserProfile) => void;
|
||||
term: string;
|
||||
selectedIds: {[id: string]: UserProfile};
|
||||
selectedIds: Set<string>;
|
||||
fetchFunction: (page: number) => Promise<UserProfile[]>;
|
||||
searchFunction: (term: string) => Promise<UserProfile[]>;
|
||||
createFilter: (exactMatches: UserProfile[], term: string) => ((p: UserProfile) => boolean);
|
||||
testID: string;
|
||||
location: AvailableScreens;
|
||||
customSection?: (profiles: UserProfile[]) => Array<SectionListData<UserProfile>>;
|
||||
}
|
||||
|
||||
export default function ServerUserList({
|
||||
|
|
@ -35,6 +37,7 @@ export default function ServerUserList({
|
|||
createFilter,
|
||||
testID,
|
||||
location,
|
||||
customSection,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
|
|
@ -129,6 +132,7 @@ export default function ServerUserList({
|
|||
tutorialWatched={tutorialWatched}
|
||||
includeUserMargin={true}
|
||||
location={location}
|
||||
customSection={customSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ describe('components/channel_list_row', () => {
|
|||
handleSelectProfile: jest.fn(),
|
||||
fetchMore: jest.fn(),
|
||||
loading: true,
|
||||
selectedIds: {},
|
||||
selectedIds: new Set(),
|
||||
showNoResults: true,
|
||||
tutorialWatched: true,
|
||||
term: 'some term',
|
||||
|
|
|
|||
|
|
@ -179,12 +179,13 @@ type Props = {
|
|||
manageMode?: boolean;
|
||||
showManageMode?: boolean;
|
||||
showNoResults: boolean;
|
||||
selectedIds: {[id: string]: UserProfile};
|
||||
selectedIds: Set<string>;
|
||||
testID?: string;
|
||||
term?: string;
|
||||
tutorialWatched: boolean;
|
||||
includeUserMargin?: boolean;
|
||||
location: AvailableScreens;
|
||||
customSection?: (profiles: UserProfile[]) => Array<SectionListData<UserProfile>>;
|
||||
}
|
||||
|
||||
export default function UserList({
|
||||
|
|
@ -203,6 +204,7 @@ export default function UserList({
|
|||
tutorialWatched,
|
||||
includeUserMargin,
|
||||
location,
|
||||
customSection,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
|
@ -223,8 +225,11 @@ export default function UserList({
|
|||
return createProfiles(profiles, channelMembers);
|
||||
}
|
||||
|
||||
if (customSection) {
|
||||
return customSection(profiles);
|
||||
}
|
||||
return createProfilesSections(intl, profiles, channelMembers);
|
||||
}, [channelMembers, intl, loading, profiles, term]);
|
||||
}, [channelMembers, customSection, intl, loading, profiles, term]);
|
||||
|
||||
const openUserProfile = useCallback(async (profile: UserProfile | UserModel) => {
|
||||
let user: UserModel;
|
||||
|
|
@ -246,8 +251,8 @@ export default function UserList({
|
|||
|
||||
const renderItem = useCallback(({item, index, section}: RenderItemType) => {
|
||||
// The list will re-render when the selection changes because it's passed into the list as extraData
|
||||
const selected = Boolean(selectedIds[item.id]);
|
||||
const canAdd = Object.keys(selectedIds).length < General.MAX_USERS_IN_GM;
|
||||
const selected = selectedIds.has(item.id);
|
||||
const canAdd = selectedIds.size < General.MAX_USERS_IN_GM;
|
||||
|
||||
const isChAdmin = item.scheme_admin || false;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {getPlaybookChecklistItemById} from '@playbooks/database/queries/item';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {updateChecklistItem, setChecklistItemCommand} from './checklist';
|
||||
import {updateChecklistItem, setChecklistItemCommand, setAssignee} from './checklist';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
|
|
@ -124,3 +124,59 @@ describe('setChecklistItemCommand', () => {
|
|||
expect(updated!.command).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAssignee', () => {
|
||||
it('should handle not found database', async () => {
|
||||
const {error} = await setAssignee('foo', 'itemid', 'user123');
|
||||
expect(error).toBeTruthy();
|
||||
expect((error as Error).message).toContain('foo database not found');
|
||||
});
|
||||
|
||||
it('should handle item not found', async () => {
|
||||
const {error} = await setAssignee(serverUrl, 'nonexistent', 'user123');
|
||||
expect(error).toBe('Item not found: nonexistent');
|
||||
});
|
||||
|
||||
it('should handle database write errors', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const originalWrite = operator.database.write;
|
||||
operator.database.write = jest.fn().mockRejectedValue(new Error('Database write failed'));
|
||||
|
||||
const {error} = await setAssignee(serverUrl, item.id, 'user123');
|
||||
expect(error).toBeTruthy();
|
||||
|
||||
operator.database.write = originalWrite;
|
||||
});
|
||||
|
||||
it('should successfully set assignee for existing item', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const assigneeId = 'user123';
|
||||
const {data, error} = await setAssignee(serverUrl, item.id, assigneeId);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.assigneeId).toBe(assigneeId);
|
||||
});
|
||||
|
||||
it('should handle empty assignee ID', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const {data, error} = await setAssignee(serverUrl, item.id, '');
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.assigneeId).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,3 +46,24 @@ export async function setChecklistItemCommand(serverUrl: string, itemId: string,
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function setAssignee(serverUrl: string, itemId: string, assigneeId: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const item = await getPlaybookChecklistItemById(database, itemId);
|
||||
if (!item) {
|
||||
return {error: `Item not found: ${itemId}`};
|
||||
}
|
||||
|
||||
await database.write(async () => {
|
||||
item.update((i) => {
|
||||
i.assigneeId = assigneeId;
|
||||
});
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logError('failed to update checklist item assignee', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {handlePlaybookRuns} from './run';
|
||||
import {handlePlaybookRuns, setOwner} from './run';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
||||
|
|
@ -45,3 +49,107 @@ describe('handlePlaybookRuns', () => {
|
|||
expect(data![2]._preparedState).toBe('create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOwner', () => {
|
||||
let database: Database;
|
||||
beforeEach(() => {
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
});
|
||||
|
||||
it('should set owner successfully when run exists', async () => {
|
||||
// Create a playbook run
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const newOwnerId = 'new_owner_user_id';
|
||||
|
||||
const {data, error} = await setOwner(serverUrl, playbookRunId, newOwnerId);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify the owner was actually updated in the database
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.ownerUserId).toBe(newOwnerId);
|
||||
});
|
||||
|
||||
it('should return error when run is not found', async () => {
|
||||
const nonExistentRunId = 'non_existent_run_id';
|
||||
const newOwnerId = 'new_owner_user_id';
|
||||
|
||||
const {data, error} = await setOwner(serverUrl, nonExistentRunId, newOwnerId);
|
||||
|
||||
expect(error).toBe('Run not found');
|
||||
expect(data).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return error when database is not found', async () => {
|
||||
const playbookRunId = 'playbook_run_1';
|
||||
const newOwnerId = 'new_owner_user_id';
|
||||
|
||||
const {data, error} = await setOwner('non_existent_server', playbookRunId, newOwnerId);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect((error as Error).message).toContain('non_existent_server database not found');
|
||||
expect(data).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle database write errors gracefully', async () => {
|
||||
// Create a playbook run
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const newOwnerId = 'new_owner_user_id';
|
||||
|
||||
// Mock the database.write to throw an error
|
||||
const originalWrite = database.write;
|
||||
database.write = jest.fn().mockRejectedValue(new Error('Database write failed'));
|
||||
|
||||
const {data, error} = await setOwner(serverUrl, playbookRunId, newOwnerId);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect((error as Error).message).toBe('Database write failed');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
// Restore the original method
|
||||
database.write = originalWrite;
|
||||
});
|
||||
|
||||
it('should update owner to the same user id without issues', async () => {
|
||||
// Create a playbook run
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const sameOwnerId = runs[0].owner_user_id; // Use the same owner ID
|
||||
|
||||
const {data, error} = await setOwner(serverUrl, playbookRunId, sameOwnerId);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify the owner remains the same
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.ownerUserId).toBe(sameOwnerId);
|
||||
});
|
||||
|
||||
it('should handle empty string owner id', async () => {
|
||||
// Create a playbook run
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const emptyOwnerId = '';
|
||||
|
||||
const {data, error} = await setOwner(serverUrl, playbookRunId, emptyOwnerId);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify the owner was updated to empty string
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.ownerUserId).toBe(emptyOwnerId);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPlaybookRunById} from '@playbooks/database/queries/run';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export async function handlePlaybookRuns(serverUrl: string, runs: PlaybookRun[], prepareRecordsOnly = false, processChildren = false) {
|
||||
|
|
@ -18,3 +19,24 @@ export async function handlePlaybookRuns(serverUrl: string, runs: PlaybookRun[],
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function setOwner(serverUrl: string, playbookRunId: string, ownerId: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const run = await getPlaybookRunById(database, playbookRunId);
|
||||
if (!run) {
|
||||
return {error: 'Run not found'};
|
||||
}
|
||||
|
||||
await database.write(async () => {
|
||||
run.update((r) => {
|
||||
r.ownerUserId = ownerId;
|
||||
});
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logError('failed to set owner', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,20 @@
|
|||
import DatabaseManager from '@database/manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setChecklistItemCommand as localSetChecklistItemCommand, updateChecklistItem as localUpdateChecklistItem} from '@playbooks/actions/local/checklist';
|
||||
import {
|
||||
setChecklistItemCommand as localSetChecklistItemCommand,
|
||||
updateChecklistItem as localUpdateChecklistItem,
|
||||
setAssignee as localSetAssignee,
|
||||
} from '@playbooks/actions/local/checklist';
|
||||
|
||||
import {updateChecklistItem, runChecklistItem, skipChecklistItem, restoreChecklistItem, setChecklistItemCommand} from './checklist';
|
||||
import {
|
||||
updateChecklistItem,
|
||||
runChecklistItem,
|
||||
skipChecklistItem,
|
||||
restoreChecklistItem,
|
||||
setChecklistItemCommand,
|
||||
setAssignee,
|
||||
} from './checklist';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
||||
|
|
@ -21,6 +32,7 @@ const mockClient = {
|
|||
runChecklistItemSlashCommand: jest.fn(),
|
||||
skipChecklistItem: jest.fn(),
|
||||
restoreChecklistItem: jest.fn(),
|
||||
setAssignee: jest.fn(),
|
||||
setChecklistItemCommand: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -182,6 +194,46 @@ describe('checklist', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setAssignee', () => {
|
||||
it('should handle client error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await setAssignee(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 'user-id-1');
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.setAssignee).not.toHaveBeenCalled();
|
||||
expect(localSetAssignee).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle API exception', async () => {
|
||||
mockClient.setAssignee.mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await setAssignee(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 'user-id-1');
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.setAssignee).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, 'user-id-1');
|
||||
expect(localSetAssignee).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set assignee successfully', async () => {
|
||||
mockClient.setAssignee.mockResolvedValueOnce({});
|
||||
|
||||
const result = await setAssignee(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 'user-id-1');
|
||||
expect(result.data).toBe(true);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(mockClient.setAssignee).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, 'user-id-1');
|
||||
expect(localSetAssignee).toHaveBeenCalledWith(serverUrl, itemId, 'user-id-1');
|
||||
});
|
||||
|
||||
it('should handle empty assignee ID', async () => {
|
||||
mockClient.setAssignee.mockResolvedValueOnce({});
|
||||
|
||||
const result = await setAssignee(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, '');
|
||||
expect(result.data).toBe(true);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(mockClient.setAssignee).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, '');
|
||||
expect(localSetAssignee).toHaveBeenCalledWith(serverUrl, itemId, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setChecklistItemCommand', () => {
|
||||
it('should handle client error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@
|
|||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setChecklistItemCommand as localSetChecklistItemCommand, updateChecklistItem as localUpdateChecklistItem} from '@playbooks/actions/local/checklist';
|
||||
import {
|
||||
setChecklistItemCommand as localSetChecklistItemCommand,
|
||||
updateChecklistItem as localUpdateChecklistItem,
|
||||
setAssignee as localSetAssignee,
|
||||
} from '@playbooks/actions/local/checklist';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
|
|
@ -109,3 +113,24 @@ export const setChecklistItemCommand = async (
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const setAssignee = async (
|
||||
serverUrl: string,
|
||||
playbookRunId: string,
|
||||
itemId: string,
|
||||
checklistNumber: number,
|
||||
itemNumber: number,
|
||||
assigneeId: string,
|
||||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.setAssignee(playbookRunId, checklistNumber, itemNumber, assigneeId);
|
||||
|
||||
await localSetAssignee(serverUrl, itemId, assigneeId);
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logDebug('error on setAssignee', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {updateLastPlaybookRunsFetchAt} from '@playbooks/actions/local/channel';
|
||||
import {handlePlaybookRuns} from '@playbooks/actions/local/run';
|
||||
import {handlePlaybookRuns, setOwner as localSetOwner} from '@playbooks/actions/local/run';
|
||||
import {getLastPlaybookRunsFetchAt} from '@playbooks/database/queries/run';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel} from './runs';
|
||||
import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, setOwner} from './runs';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
const channelId = 'channel-id-1';
|
||||
|
|
@ -20,6 +20,7 @@ const mockPlaybookRun2 = TestHelper.fakePlaybookRun({channel_id: channelId});
|
|||
|
||||
const mockClient = {
|
||||
fetchPlaybookRuns: jest.fn(),
|
||||
setOwner: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@playbooks/database/queries/run');
|
||||
|
|
@ -212,3 +213,61 @@ describe('fetchFinishedRunsForChannel', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOwner', () => {
|
||||
const playbookRunId = 'playbook-run-id-1';
|
||||
const ownerId = 'owner-user-id-1';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(localSetOwner).mockResolvedValue({data: true});
|
||||
});
|
||||
|
||||
it('should set owner successfully', async () => {
|
||||
mockClient.setOwner.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await setOwner(serverUrl, playbookRunId, ownerId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.setOwner).toHaveBeenCalledWith(playbookRunId, ownerId);
|
||||
expect(localSetOwner).toHaveBeenCalledWith(serverUrl, playbookRunId, ownerId);
|
||||
});
|
||||
|
||||
it('should handle client error', async () => {
|
||||
const clientError = new Error('Client error');
|
||||
mockClient.setOwner.mockRejectedValueOnce(clientError);
|
||||
|
||||
const result = await setOwner(serverUrl, playbookRunId, ownerId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(mockClient.setOwner).toHaveBeenCalledWith(playbookRunId, ownerId);
|
||||
expect(localSetOwner).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle network manager error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await setOwner(serverUrl, playbookRunId, ownerId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(localSetOwner).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty string parameters', async () => {
|
||||
mockClient.setOwner.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await setOwner(serverUrl, '', '');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.setOwner).toHaveBeenCalledWith('', '');
|
||||
expect(localSetOwner).toHaveBeenCalledWith(serverUrl, '', '');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {updateLastPlaybookRunsFetchAt} from '@playbooks/actions/local/channel';
|
||||
import {handlePlaybookRuns} from '@playbooks/actions/local/run';
|
||||
import {handlePlaybookRuns, setOwner as localSetOwner} from '@playbooks/actions/local/run';
|
||||
import {getLastPlaybookRunsFetchAt} from '@playbooks/database/queries/run';
|
||||
import {getMaxRunUpdateAt} from '@playbooks/utils/run';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
|
@ -109,3 +109,17 @@ export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOn
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.setOwner(playbookRunId, ownerId);
|
||||
|
||||
await localSetOwner(serverUrl, playbookRunId, ownerId);
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logDebug('error on setOwner', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,10 +78,9 @@ describe('setChecklistItemState', () => {
|
|||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState);
|
||||
await client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should handle error when setting checklist item state', async () => {
|
||||
|
|
@ -89,13 +88,10 @@ describe('setChecklistItemState', () => {
|
|||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const newState = 'in_progress' as ChecklistItemState;
|
||||
const expectedError = {error: new Error('Network error')};
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState);
|
||||
|
||||
expect(result).toEqual(expectedError);
|
||||
await expect(client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState)).rejects.toThrow('Network error');
|
||||
});
|
||||
|
||||
test('should set checklist item state with empty state', async () => {
|
||||
|
|
@ -109,10 +105,9 @@ describe('setChecklistItemState', () => {
|
|||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState);
|
||||
await client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should set checklist item state with skipped state', async () => {
|
||||
|
|
@ -126,10 +121,9 @@ describe('setChecklistItemState', () => {
|
|||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState);
|
||||
await client.setChecklistItemState(playbookRunID, checklistNum, itemNum, newState);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -285,3 +279,115 @@ describe('setChecklistItemCommand', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setOwner', () => {
|
||||
test('should set owner successfully', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const ownerId = 'user456';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/owner`;
|
||||
const expectedOptions = {method: 'post', body: {owner_id: ownerId}};
|
||||
const mockResponse = {success: true};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setOwner(playbookRunId, ownerId);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should set owner with empty owner id', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const ownerId = '';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/owner`;
|
||||
const expectedOptions = {method: 'post', body: {owner_id: ownerId}};
|
||||
const mockResponse = {success: true};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setOwner(playbookRunId, ownerId);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should handle error when setting owner', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const ownerId = 'user456';
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.setOwner(playbookRunId, ownerId)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAssignee', () => {
|
||||
test('should set assignee successfully', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const assigneeId = 'user789';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}/assignee`;
|
||||
const expectedOptions = {method: 'put', body: {assignee_id: assigneeId}};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.setAssignee(playbookRunId, checklistNum, itemNum, assigneeId);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should set assignee with zero indices', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 0;
|
||||
const itemNum = 0;
|
||||
const assigneeId = 'user789';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}/assignee`;
|
||||
const expectedOptions = {method: 'put', body: {assignee_id: assigneeId}};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.setAssignee(playbookRunId, checklistNum, itemNum, assigneeId);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should set assignee with undefined assignee id', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}/assignee`;
|
||||
const expectedOptions = {method: 'put', body: {assignee_id: undefined}};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.setAssignee(playbookRunId, checklistNum, itemNum);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should set assignee with empty assignee id', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const assigneeId = '';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}/assignee`;
|
||||
const expectedOptions = {method: 'put', body: {assignee_id: assigneeId}};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.setAssignee(playbookRunId, checklistNum, itemNum, assigneeId);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should handle error when setting assignee', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const assigneeId = 'user789';
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.setAssignee(playbookRunId, checklistNum, itemNum, assigneeId)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export interface ClientPlaybooksMix {
|
|||
// Playbook Runs
|
||||
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
|
||||
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
||||
setOwner: (playbookRunId: string, ownerId: string) => Promise<void>;
|
||||
|
||||
// Run Management
|
||||
// finishRun: (playbookRunId: string) => Promise<any>;
|
||||
|
|
@ -18,6 +19,7 @@ export interface ClientPlaybooksMix {
|
|||
setChecklistItemState: (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => Promise<void>;
|
||||
skipChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise<void>;
|
||||
restoreChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise<void>;
|
||||
setAssignee: (playbookRunId: string, checklistNum: number, itemNum: number, assigneeId?: string) => Promise<void>;
|
||||
|
||||
// skipChecklist: (playbookRunID: string, checklistNum: number) => Promise<void>;
|
||||
|
||||
|
|
@ -62,6 +64,14 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
);
|
||||
};
|
||||
|
||||
setOwner = async (playbookRunId: string, ownerId: string) => {
|
||||
const data = await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunId)}/owner`,
|
||||
{method: 'post', body: {owner_id: ownerId}},
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
// Run Management
|
||||
// finishRun = async (playbookRunId: string) => {
|
||||
// try {
|
||||
|
|
@ -76,14 +86,10 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
|
||||
// Checklist Management
|
||||
setChecklistItemState = async (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => {
|
||||
try {
|
||||
return await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunID)}/checklists/${checklistNum}/item/${itemNum}/state`,
|
||||
{method: 'put', body: {new_state: newState}},
|
||||
);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunID)}/checklists/${checklistNum}/item/${itemNum}/state`,
|
||||
{method: 'put', body: {new_state: newState}},
|
||||
);
|
||||
};
|
||||
|
||||
skipChecklistItem = async (playbookRunID: string, checklistNum: number, itemNum: number) => {
|
||||
|
|
@ -107,6 +113,13 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
// );
|
||||
// };
|
||||
|
||||
setAssignee = async (playbookRunId: string, checklistNum: number, itemNum: number, assigneeId?: string) => {
|
||||
await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunId)}/checklists/${checklistNum}/item/${itemNum}/assignee`,
|
||||
{method: 'put', body: {assignee_id: assigneeId}},
|
||||
);
|
||||
};
|
||||
|
||||
// Slash Commands
|
||||
runChecklistItemSlashCommand = async (playbookRunId: string, checklistNumber: number, itemNumber: number) => {
|
||||
const data = await this.doFetch(
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
export const PLAYBOOKS_RUNS = 'PlaybookRuns';
|
||||
export const PLAYBOOK_RUN = 'PlaybookRun';
|
||||
export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand';
|
||||
export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser';
|
||||
|
||||
export default {
|
||||
PLAYBOOKS_RUNS,
|
||||
PLAYBOOK_RUN,
|
||||
PLAYBOOK_EDIT_COMMAND,
|
||||
PLAYBOOK_SELECT_USER,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q, type Query, type Relation} from '@nozbe/watermelondb';
|
||||
import {children, field, immutableRelation, json} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {type Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
|
|
@ -9,6 +8,7 @@ import {MM_TABLES} from '@constants/database';
|
|||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
import {safeParseJSONStringArray} from '@utils/helpers';
|
||||
|
||||
import type {Query, Relation} from '@nozbe/watermelondb';
|
||||
import type PlaybookRunModelInterface from '@playbooks//types/database/models/playbook_run';
|
||||
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
|
||||
import type {SyncStatus} from '@typings/database/database';
|
||||
|
|
@ -137,11 +137,6 @@ export default class PlaybookRunModel extends Model implements PlaybookRunModelI
|
|||
|
||||
@children(PLAYBOOK_CHECKLIST) checklists!: Query<PlaybookChecklistModel>;
|
||||
|
||||
participants = (): Query<UserModel> => {
|
||||
const filteredParticipantIds = this.participantIds.filter((id) => id !== this.ownerUserId);
|
||||
return this.database.get<UserModel>(USER).query(Q.where('id', Q.oneOf(filteredParticipantIds)));
|
||||
};
|
||||
|
||||
prepareDestroyWithRelations = async (): Promise<Model[]> => {
|
||||
const preparedModels: Model[] = [this.prepareDestroyPermanently()];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {of as of$} from 'rxjs';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
|
@ -15,6 +14,7 @@ import {
|
|||
observePlaybookRunProgress,
|
||||
getLastPlaybookRunsFetchAt,
|
||||
queryParticipantsFromAPIRun,
|
||||
observeParticipantsIdsFromPlaybookModel,
|
||||
} from './run';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
|
@ -421,4 +421,208 @@ describe('Playbook Run Queries', () => {
|
|||
expect(participantIds).not.toContain(ownerId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeParticipantsIdsFromPlaybookModel', () => {
|
||||
it('should return empty array when runModel is undefined', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const result = observeParticipantsIdsFromPlaybookModel(undefined);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should return all participant IDs excluding owner when includeOwner is false (default)', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
const participant1Id = 'participant-1-id';
|
||||
const participant2Id = 'participant-2-id';
|
||||
const participant3Id = 'participant-3-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [ownerId, participant1Id, participant2Id, participant3Id],
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [ownerId, participant1Id, participant2Id, participant3Id],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([participant1Id, participant2Id, participant3Id]);
|
||||
});
|
||||
|
||||
it('should return all participant IDs including owner when includeOwner is true', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
const participant1Id = 'participant-1-id';
|
||||
const participant2Id = 'participant-2-id';
|
||||
const participant3Id = 'participant-3-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [ownerId, participant1Id, participant2Id, participant3Id],
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [ownerId, participant1Id, participant2Id, participant3Id],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel, true);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([ownerId, participant1Id, participant2Id, participant3Id]);
|
||||
});
|
||||
|
||||
it('should return empty array when no participants exist and includeOwner is false', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [ownerId], // Only owner
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [ownerId],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should return only owner when no other participants exist and includeOwner is true', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [ownerId], // Only owner
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [ownerId],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel, true);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([ownerId]);
|
||||
});
|
||||
|
||||
it('should handle case where owner is not in participantIds and includeOwner is false', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
const participant1Id = 'participant-1-id';
|
||||
const participant2Id = 'participant-2-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [participant1Id, participant2Id], // Owner not included
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [participant1Id, participant2Id],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([participant1Id, participant2Id]);
|
||||
});
|
||||
|
||||
it('should handle case where owner is not in participantIds and includeOwner is true', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
const participant1Id = 'participant-1-id';
|
||||
const participant2Id = 'participant-2-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [participant1Id, participant2Id], // Owner not included
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [participant1Id, participant2Id],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel, true);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([participant1Id, participant2Id]);
|
||||
});
|
||||
|
||||
it('should return empty array when participantIds is empty and includeOwner is false', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [], // Empty participants
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should return empty array when participantIds is empty and includeOwner is true', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const ownerId = 'owner-user-id';
|
||||
|
||||
const mockRunModel = TestHelper.fakePlaybookRunModel({
|
||||
ownerUserId: ownerId,
|
||||
participantIds: [], // Empty participants
|
||||
});
|
||||
|
||||
// Mock the observe method to return the run model
|
||||
mockRunModel.observe = jest.fn().mockReturnValue(
|
||||
of$({
|
||||
participantIds: [],
|
||||
ownerUserId: ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = observeParticipantsIdsFromPlaybookModel(mockRunModel, true);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,3 +72,19 @@ export const queryParticipantsFromAPIRun = (database: Database, run: PlaybookRun
|
|||
const participantsIds = run.participant_ids.filter((id) => id !== run.owner_user_id);
|
||||
return queryUsersById(database, participantsIds);
|
||||
};
|
||||
|
||||
const emptyParticipantsList: string[] = [];
|
||||
export const observeParticipantsIdsFromPlaybookModel = (runModel: PlaybookRunModel | undefined, includeOwner = false) => {
|
||||
if (!runModel) {
|
||||
return of$(emptyParticipantsList);
|
||||
}
|
||||
return runModel.observe().pipe(
|
||||
switchMap((run) => {
|
||||
let participantsIds = run.participantIds;
|
||||
if (!includeOwner) {
|
||||
participantsIds = participantsIds.filter((id) => id !== run.ownerUserId);
|
||||
}
|
||||
return of$(participantsIds);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 SelectUser from './select_user';
|
||||
|
||||
import {loadPlaybooksScreen} from '.';
|
||||
|
||||
|
|
@ -38,6 +39,12 @@ jest.mock('@playbooks/screens/edit_command', () => ({
|
|||
}));
|
||||
jest.mocked(EditCommand).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_EDIT_COMMAND}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/select_user', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(SelectUser).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_SELECT_USER}</Text>);
|
||||
|
||||
describe('Screen Registration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
|||
|
|
@ -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_SELECT_USER:
|
||||
return withServerDatabase(require('@playbooks/screens/select_user').default);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {goToScreen} from '@screens/navigation';
|
|||
import TestHelper from '@test/test_helper';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import {goToPlaybookRuns, goToPlaybookRun} from './navigation';
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToSelectUser} from './navigation';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
goToScreen: jest.fn(),
|
||||
|
|
@ -64,4 +64,28 @@ describe('Playbooks Navigation', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goToSelectUser', () => {
|
||||
it('should navigate to select user screen with correct parameters when handleRemove is provided', async () => {
|
||||
const title = 'Select User';
|
||||
const participantIds = ['user1', 'user2', 'user3'];
|
||||
const selected = 'user2';
|
||||
const handleSelect = jest.fn();
|
||||
const handleRemove = jest.fn();
|
||||
|
||||
await goToSelectUser(title, participantIds, selected, handleSelect, handleRemove);
|
||||
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOK_SELECT_USER,
|
||||
title,
|
||||
{
|
||||
participantIds,
|
||||
selected,
|
||||
handleSelect,
|
||||
handleRemove,
|
||||
},
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,3 +24,18 @@ export async function goToPlaybookRun(intl: IntlShape, playbookRunId: string, pl
|
|||
const title = intl.formatMessage({id: 'playbooks.playbook_run.title', defaultMessage: 'Playbook run'});
|
||||
goToScreen(Screens.PLAYBOOK_RUN, title, {playbookRunId, playbookRun}, {});
|
||||
}
|
||||
|
||||
export async function goToSelectUser(
|
||||
title: string,
|
||||
participantIds: string[],
|
||||
selected: string | undefined,
|
||||
handleSelect: (user: UserProfile) => void,
|
||||
handleRemove?: () => void,
|
||||
) {
|
||||
goToScreen(Screens.PLAYBOOK_SELECT_USER, title, {
|
||||
participantIds,
|
||||
selected,
|
||||
handleSelect,
|
||||
handleRemove,
|
||||
}, {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetScrollView} from '@gorhom/bottom-sheet';
|
||||
import {act, fireEvent} from '@testing-library/react-native';
|
||||
import {act, fireEvent, waitFor} from '@testing-library/react-native';
|
||||
import React, {type ComponentProps} from 'react';
|
||||
import {ScrollView} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {setChecklistItemCommand} from '@playbooks/actions/remote/checklist';
|
||||
import {setAssignee, setChecklistItemCommand} from '@playbooks/actions/remote/checklist';
|
||||
import {goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {dismissBottomSheet, goToScreen, 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';
|
||||
|
||||
|
|
@ -33,12 +35,15 @@ jest.mocked(OptionBox).mockImplementation((props) => React.createElement('Option
|
|||
jest.mock('@components/option_item');
|
||||
jest.mocked(OptionItem).mockImplementation((props) => React.createElement('OptionItem', props));
|
||||
|
||||
jest.mock('@playbooks/screens/navigation');
|
||||
jest.mock('@playbooks/actions/remote/checklist');
|
||||
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn().mockReturnValue('server-url'),
|
||||
}));
|
||||
|
||||
jest.mock('@utils/snack_bar');
|
||||
|
||||
describe('ChecklistItemBottomSheet', () => {
|
||||
const mockOnCheck = jest.fn();
|
||||
const mockOnSkip = jest.fn();
|
||||
|
|
@ -85,6 +90,7 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
onRunCommand: mockOnRunCommand,
|
||||
teammateNameDisplay: mockTeammateNameDisplay,
|
||||
isDisabled: false,
|
||||
participantIds: ['user-1', 'user-2'],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -345,6 +351,27 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
expect(commandItem.props.info).toBe('None');
|
||||
});
|
||||
|
||||
it('user profile option item is disabled when isDisabled is true', () => {
|
||||
const props = getBaseProps();
|
||||
props.isDisabled = true;
|
||||
props.item = TestHelper.fakePlaybookChecklistItemModel({
|
||||
...props.item,
|
||||
assigneeId: 'user-1',
|
||||
});
|
||||
const {getByTestId, rerender} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
let assigneeItem = getByTestId('checklist_item.assignee');
|
||||
expect(assigneeItem).toHaveProp('type', 'none');
|
||||
expect(assigneeItem).toHaveProp('action', undefined);
|
||||
|
||||
props.isDisabled = false;
|
||||
rerender(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
assigneeItem = getByTestId('checklist_item.assignee');
|
||||
expect(assigneeItem).toHaveProp('type', 'arrow');
|
||||
expect(assigneeItem).toHaveProp('action', expect.any(Function));
|
||||
});
|
||||
|
||||
it('opens the command modal when the command is clicked', async () => {
|
||||
const props = getBaseProps();
|
||||
props.checklistNumber = 2;
|
||||
|
|
@ -402,13 +429,100 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('opens the select assigneed screen when the assignee option item is pressed', async () => {
|
||||
const props = getBaseProps();
|
||||
props.checklistNumber = 2;
|
||||
props.itemNumber = 4;
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const assigneeItem = getByTestId('checklist_item.assignee');
|
||||
const onPress = assigneeItem.props.action;
|
||||
|
||||
await act(async () => {
|
||||
onPress('user-1');
|
||||
});
|
||||
|
||||
expect(goToSelectUser).toHaveBeenCalledWith(
|
||||
'Assignee',
|
||||
['user-1', 'user-2'],
|
||||
'user-1',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
|
||||
const handleRemove = jest.mocked(goToSelectUser).mock.calls[0][4];
|
||||
|
||||
await act(async () => {
|
||||
handleSelect(TestHelper.fakeUser({id: 'user-1'}));
|
||||
});
|
||||
|
||||
expect(setAssignee).toHaveBeenCalledWith(
|
||||
'server-url',
|
||||
'run-1',
|
||||
'item-1',
|
||||
2,
|
||||
4,
|
||||
'user-1',
|
||||
);
|
||||
|
||||
jest.mocked(setAssignee).mockClear();
|
||||
|
||||
await act(async () => {
|
||||
handleRemove?.();
|
||||
});
|
||||
|
||||
expect(setAssignee).toHaveBeenCalledWith(
|
||||
'server-url',
|
||||
'run-1',
|
||||
'item-1',
|
||||
2,
|
||||
4,
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles set assignee error', async () => {
|
||||
const props = getBaseProps();
|
||||
jest.mocked(setAssignee).mockResolvedValue({error: 'error'});
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const assigneeItem = getByTestId('checklist_item.assignee');
|
||||
const onPress = assigneeItem.props.action;
|
||||
|
||||
await act(async () => {
|
||||
onPress('user-1');
|
||||
});
|
||||
|
||||
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
|
||||
const handleRemove = jest.mocked(goToSelectUser).mock.calls[0][4];
|
||||
|
||||
await act(async () => {
|
||||
handleSelect(TestHelper.fakeUser({id: 'user-1'}));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showPlaybookErrorSnackbar).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
jest.mocked(showPlaybookErrorSnackbar).mockClear();
|
||||
|
||||
await act(async () => {
|
||||
handleRemove?.();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showPlaybookErrorSnackbar).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays correct assignee label and icon', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const assigneeItem = getByTestId('checklist_item.assignee');
|
||||
expect(assigneeItem.props.label).toBe('Assignee');
|
||||
expect(assigneeItem.props.icon).toBe('account-multiple-plus-outline');
|
||||
expect(assigneeItem.props.icon).toBe('account-plus-outline');
|
||||
});
|
||||
|
||||
it('displays correct due date label and icon', () => {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ import OptionBox from '@components/option_box';
|
|||
import OptionItem, {ITEM_HEIGHT} from '@components/option_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {setChecklistItemCommand} from '@playbooks/actions/remote/checklist';
|
||||
import {setAssignee, setChecklistItemCommand} from '@playbooks/actions/remote/checklist';
|
||||
import {goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {dismissBottomSheet, goToScreen, openUserProfileModal} from '@screens/navigation';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
|
|
@ -125,6 +127,7 @@ type Props = {
|
|||
onRunCommand: () => void;
|
||||
teammateNameDisplay: string;
|
||||
isDisabled: boolean;
|
||||
participantIds: string[];
|
||||
};
|
||||
|
||||
function getDueDateInfo(intl: IntlShape, dueDate: number | undefined) {
|
||||
|
|
@ -152,6 +155,7 @@ const ChecklistItemBottomSheet = ({
|
|||
onRunCommand,
|
||||
teammateNameDisplay,
|
||||
isDisabled,
|
||||
participantIds,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -244,14 +248,39 @@ const ChecklistItemBottomSheet = ({
|
|||
};
|
||||
}, [assignee, intl, onUserChipPress, teammateNameDisplay]);
|
||||
|
||||
const handleSelect = useCallback(async (selected: UserProfile) => {
|
||||
const res = await setAssignee(serverUrl, runId, item.id, checklistNumber, itemNumber, selected.id);
|
||||
if (res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
}
|
||||
}, [checklistNumber, item.id, itemNumber, runId, serverUrl]);
|
||||
|
||||
const handleRemove = useCallback(async () => {
|
||||
const res = await setAssignee(serverUrl, runId, item.id, checklistNumber, itemNumber, '');
|
||||
if (res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
}
|
||||
}, [checklistNumber, item.id, itemNumber, runId, serverUrl]);
|
||||
|
||||
const openUserSelector = useCallback(() => {
|
||||
goToSelectUser(
|
||||
intl.formatMessage(messages.assignee),
|
||||
participantIds,
|
||||
assignee?.id,
|
||||
handleSelect,
|
||||
handleRemove,
|
||||
);
|
||||
}, [assignee?.id, handleRemove, handleSelect, intl, participantIds]);
|
||||
|
||||
const renderTaskDetails = () => (
|
||||
<View style={styles.taskDetailsContainer}>
|
||||
<OptionItem
|
||||
type='none'
|
||||
icon='account-multiple-plus-outline'
|
||||
type={isDisabled ? 'none' : 'arrow'}
|
||||
icon='account-plus-outline'
|
||||
label={intl.formatMessage(messages.assignee)}
|
||||
info={assigneeInfo}
|
||||
testID='checklist_item.assignee'
|
||||
action={isDisabled ? undefined : openUserSelector}
|
||||
/>
|
||||
<OptionItem
|
||||
type='none'
|
||||
|
|
|
|||
|
|
@ -52,6 +52,13 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
|
|||
});
|
||||
}
|
||||
|
||||
async function addRunToDatabase(run: PartialPlaybookRun) {
|
||||
await operator.handlePlaybookRun({
|
||||
prepareRecordsOnly: false,
|
||||
runs: [run],
|
||||
});
|
||||
}
|
||||
|
||||
async function addUserToDatabase(user: UserProfile) {
|
||||
await operator.handleUsers({
|
||||
prepareRecordsOnly: false,
|
||||
|
|
@ -74,12 +81,15 @@ 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']});
|
||||
await addRunToDatabase(rawRun);
|
||||
await addItemToDatabase(rawItem);
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'user-1'}));
|
||||
|
||||
const item = await getPlaybookChecklistItemById(database, rawItem.id);
|
||||
|
||||
const props = getBaseProps();
|
||||
props.runId = rawRun.id;
|
||||
props.item = item!;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
|
@ -88,6 +98,24 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
|
|||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', item);
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
expect(bottomSheet).toHaveProp('participantIds', ['user-1', 'user-2']);
|
||||
});
|
||||
|
||||
it('should handle missing run correctly', async () => {
|
||||
const rawItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-1', assignee_id: 'user-1'});
|
||||
await addItemToDatabase(rawItem);
|
||||
|
||||
const item = await getPlaybookChecklistItemById(database, rawItem.id);
|
||||
|
||||
const props = getBaseProps();
|
||||
props.runId = 'missing-run-id';
|
||||
props.item = item!;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('participantIds', []);
|
||||
});
|
||||
|
||||
it('should handle assigneeId changes through observable', async () => {
|
||||
|
|
@ -159,6 +187,7 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
|
|||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', apiItem);
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
expect(bottomSheet).toHaveProp('participantIds', []);
|
||||
});
|
||||
|
||||
it('should handle assigneeId changes through observable', async () => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {of as of$, switchMap} from 'rxjs';
|
||||
|
||||
import {observeParticipantsIdsFromPlaybookModel, observePlaybookRunById} from '@playbooks/database/queries/run';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
|
||||
import ChecklistItemBottomSheet, {BOTTOM_SHEET_HEIGHT} from './checklist_item_bottom_sheet';
|
||||
|
|
@ -13,9 +14,10 @@ import type {WithDatabaseArgs} from '@typings/database/database';
|
|||
|
||||
type OwnProps = {
|
||||
item: PlaybookChecklistItemModel | PlaybookChecklistItem;
|
||||
runId: string;
|
||||
} & WithDatabaseArgs;
|
||||
|
||||
const enhanced = withObservables(['item'], ({item, database}: OwnProps) => {
|
||||
const enhanced = withObservables(['item', 'runId'], ({item, runId, database}: OwnProps) => {
|
||||
if ('observe' in item) {
|
||||
const observedItem = item.observe();
|
||||
|
||||
|
|
@ -33,6 +35,9 @@ const enhanced = withObservables(['item'], ({item, database}: OwnProps) => {
|
|||
return {
|
||||
item: observedItem,
|
||||
assignee,
|
||||
participantIds: observePlaybookRunById(database, runId).pipe(
|
||||
switchMap((run) => observeParticipantsIdsFromPlaybookModel(run, true)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +46,7 @@ const enhanced = withObservables(['item'], ({item, database}: OwnProps) => {
|
|||
return {
|
||||
item: of$(item),
|
||||
assignee,
|
||||
participantIds: of$([]),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,14 @@ import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
|||
|
||||
import {queryPlaybookChecklistByRun} from '@playbooks/database/queries/checklist';
|
||||
import {queryPlaybookChecklistItemsByChecklists} from '@playbooks/database/queries/item';
|
||||
import {observePlaybookRunById, queryParticipantsFromAPIRun} from '@playbooks/database/queries/run';
|
||||
import {observeParticipantsIdsFromPlaybookModel, observePlaybookRunById, queryParticipantsFromAPIRun} from '@playbooks/database/queries/run';
|
||||
import {areItemsOrdersEqual} from '@playbooks/utils/items_order';
|
||||
import {isOverdue} from '@playbooks/utils/run';
|
||||
import {observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay, observeUser} from '@queries/servers/user';
|
||||
import {observeTeammateNameDisplay, observeUser, queryUsersById} from '@queries/servers/user';
|
||||
|
||||
import PlaybookRun from './playbook_run';
|
||||
|
||||
import type {UserModel} from '@database/models/server';
|
||||
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
|
||||
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
|
@ -38,7 +37,6 @@ const getIds = (checklists: PlaybookChecklistModel[]) => {
|
|||
return checklists.map((c) => c.id);
|
||||
};
|
||||
|
||||
const emptyParticipantsList: UserModel[] = [];
|
||||
const enhanced = withObservables(['playbookRunId', 'playbookRun'], ({playbookRunId, playbookRun: providedRun, database}: OwnProps) => {
|
||||
// We receive a API run instead of a model from the database
|
||||
if (providedRun) {
|
||||
|
|
@ -61,10 +59,11 @@ const enhanced = withObservables(['playbookRunId', 'playbookRun'], ({playbookRun
|
|||
// We only receive the id, so it should be a model from the database
|
||||
const playbookRun = observePlaybookRunById(database, playbookRunId);
|
||||
const owner = playbookRun.pipe(
|
||||
switchMap((r) => (r ? r.owner.observe() : of$(undefined))),
|
||||
switchMap((r) => (r ? observeUser(database, r.ownerUserId) : of$(undefined))),
|
||||
);
|
||||
const participants = playbookRun.pipe(
|
||||
switchMap((r) => (r ? r.participants().observe() : of$(emptyParticipantsList))),
|
||||
switchMap((r) => observeParticipantsIdsFromPlaybookModel(r, false)),
|
||||
switchMap((ids) => queryUsersById(database, ids).observe()),
|
||||
);
|
||||
|
||||
const checklists = queryPlaybookChecklistByRun(database, playbookRunId).observe();
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@ import UserAvatarsStack from '@components/user_avatars_stack';
|
|||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {setOwner} from '@playbooks/actions/remote/runs';
|
||||
import {openUserProfileModal} from '@screens/navigation';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import {renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import {goToSelectUser} from '../navigation';
|
||||
|
||||
import ChecklistList from './checklist_list';
|
||||
import ErrorState from './error_state';
|
||||
|
|
@ -22,6 +26,9 @@ import type {Database} from '@nozbe/watermelondb';
|
|||
import type {PlaybookRunModel} from '@playbooks/database/models';
|
||||
|
||||
const serverUrl = 'some.server.url';
|
||||
|
||||
jest.mock('@utils/snack_bar');
|
||||
|
||||
jest.mock('@context/server');
|
||||
jest.mocked(useServerUrl).mockReturnValue(serverUrl);
|
||||
|
||||
|
|
@ -55,6 +62,14 @@ jest.mocked(StatusUpdateIndicator).mockImplementation(
|
|||
(props) => React.createElement('StatusUpdateIndicator', {testID: 'status-update-indicator', ...props}),
|
||||
);
|
||||
|
||||
jest.mock('../navigation', () => ({
|
||||
goToSelectUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@playbooks/actions/remote/runs', () => ({
|
||||
setOwner: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('PlaybookRun', () => {
|
||||
let database: Database;
|
||||
|
||||
|
|
@ -159,6 +174,7 @@ describe('PlaybookRun', () => {
|
|||
expect(ownerChip.props.user).toBe(props.owner);
|
||||
expect(ownerChip.props.onPress).toBeDefined();
|
||||
expect(ownerChip.props.teammateNameDisplay).toBe(General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME);
|
||||
expect(ownerChip.props.actionIcon).toBe(undefined);
|
||||
|
||||
ownerChip.props.onPress();
|
||||
expect(openUserProfileModal).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
|
||||
|
|
@ -191,6 +207,51 @@ describe('PlaybookRun', () => {
|
|||
expect(queryByText('Participants')).toBeNull();
|
||||
});
|
||||
|
||||
it('handles owner chip action when not read only', () => {
|
||||
const props = getBaseProps();
|
||||
props.participants.push(TestHelper.fakeUserModel({id: props.currentUserId}));
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const ownerChip = getByTestId('user-chip');
|
||||
expect(ownerChip).toHaveProp('actionIcon', 'downArrow');
|
||||
ownerChip.props.onPress();
|
||||
|
||||
expect(goToSelectUser).toHaveBeenCalledWith(
|
||||
'Owner',
|
||||
[...props.participants.map((p) => p.id), props.owner!.id],
|
||||
props.owner!.id,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(openUserProfileModal).not.toHaveBeenCalled();
|
||||
|
||||
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
|
||||
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
|
||||
|
||||
expect(setOwner).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
props.playbookRun!.id,
|
||||
'user-2',
|
||||
);
|
||||
expect(showPlaybookErrorSnackbar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles set owner error', async () => {
|
||||
const props = getBaseProps();
|
||||
props.participants.push(TestHelper.fakeUserModel({id: props.currentUserId}));
|
||||
jest.mocked(setOwner).mockResolvedValue({error: 'error'});
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const ownerChip = getByTestId('user-chip');
|
||||
ownerChip.props.onPress();
|
||||
|
||||
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
|
||||
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showPlaybookErrorSnackbar).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders checklist list correctly', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, getByText, queryByText, rerender} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
|
|
|||
|
|
@ -13,12 +13,16 @@ import UserAvatarsStack from '@components/user_avatars_stack';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {setOwner} from '@playbooks/actions/remote/runs';
|
||||
import {getRunScheduledTimestamp, isRunFinished} from '@playbooks/utils/run';
|
||||
import {openUserProfileModal, popTopScreen} from '@screens/navigation';
|
||||
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import {goToSelectUser} from '../navigation';
|
||||
|
||||
import ChecklistList from './checklist_list';
|
||||
import ErrorState from './error_state';
|
||||
import OutOfDateHeader from './out_of_date_header';
|
||||
|
|
@ -157,6 +161,9 @@ export default function PlaybookRun({
|
|||
|
||||
const isParticipant = participants.some((p) => p.id === currentUserId) || owner?.id === currentUserId;
|
||||
|
||||
const isFinished = isRunFinished(playbookRun);
|
||||
const readOnly = isFinished || !isParticipant;
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [
|
||||
styles.container,
|
||||
|
|
@ -176,12 +183,34 @@ export default function PlaybookRun({
|
|||
});
|
||||
}, [owner, intl, theme, channelId, componentId]);
|
||||
|
||||
const handleSelectOwner = useCallback(async (selected: UserProfile) => {
|
||||
if (!playbookRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await setOwner(serverUrl, playbookRun.id, selected.id);
|
||||
if (res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
}
|
||||
}, [playbookRun, serverUrl]);
|
||||
|
||||
const openChangeOwnerModal = useCallback(() => {
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
|
||||
goToSelectUser(
|
||||
intl.formatMessage(messages.owner),
|
||||
[...participants.map((p) => p.id), owner?.id || ''],
|
||||
owner?.id,
|
||||
handleSelectOwner,
|
||||
);
|
||||
}, [handleSelectOwner, intl, owner, participants]);
|
||||
|
||||
if (!playbookRun) {
|
||||
return <ErrorState/>;
|
||||
}
|
||||
|
||||
const isFinished = isRunFinished(playbookRun);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OutOfDateHeader
|
||||
|
|
@ -224,8 +253,9 @@ export default function PlaybookRun({
|
|||
<View style={styles.ownerRow}>
|
||||
<UserChip
|
||||
user={owner}
|
||||
onPress={openOwnerProfile}
|
||||
onPress={readOnly ? openOwnerProfile : openChangeOwnerModal}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
actionIcon={readOnly ? undefined : 'downArrow'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {of as of$, switchMap} from 'rxjs';
|
||||
|
||||
import {observePlaybookRunProgress, queryParticipantsFromAPIRun} from '@playbooks/database/queries/run';
|
||||
import {observeParticipantsIdsFromPlaybookModel, observePlaybookRunProgress, queryParticipantsFromAPIRun} from '@playbooks/database/queries/run';
|
||||
import {getProgressFromRun} from '@playbooks/utils/progress';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {observeUser, queryUsersById} from '@queries/servers/user';
|
||||
|
||||
import PlaybookCard, {ITEM_HEIGHT} from './playbook_card';
|
||||
|
||||
|
|
@ -18,10 +18,12 @@ type OwnProps = {
|
|||
} & WithDatabaseArgs;
|
||||
|
||||
const enhanced = withObservables(['run'], ({run, database}: OwnProps) => {
|
||||
if ('participants' in run) {
|
||||
if ('observe' in run) {
|
||||
return {
|
||||
run: run.observe(),
|
||||
participants: run.participants().observe(),
|
||||
participants: observeParticipantsIdsFromPlaybookModel(run, false).pipe(
|
||||
switchMap((ids) => queryUsersById(database, ids).observe()),
|
||||
),
|
||||
progress: observePlaybookRunProgress(database, run.id),
|
||||
owner: observeUser(database, run.ownerUserId),
|
||||
};
|
||||
|
|
|
|||
172
app/products/playbooks/screens/select_user/index.test.tsx
Normal file
172
app/products/playbooks/screens/select_user/index.test.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {act, renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
|
||||
import SelectUserComponent from './select_user';
|
||||
|
||||
import SelectUser from './';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
jest.mock('./select_user');
|
||||
jest.mocked(SelectUserComponent).mockImplementation(
|
||||
(props) => React.createElement('SelectUser', {testID: 'select-user', ...props}),
|
||||
);
|
||||
|
||||
const serverUrl = 'server-url';
|
||||
|
||||
describe('SelectUser', () => {
|
||||
function getBaseProps(): ComponentProps<typeof SelectUser> {
|
||||
return {
|
||||
handleSelect: jest.fn(),
|
||||
handleRemove: jest.fn(),
|
||||
selected: 'selected-user-id',
|
||||
componentId: 'PlaybookSelectUser',
|
||||
participantIds: ['participant-id'],
|
||||
};
|
||||
}
|
||||
|
||||
let database: Database;
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = serverDatabaseAndOperator.database;
|
||||
operator = serverDatabaseAndOperator.operator;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render correctly with no data', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const selectUser = getByTestId('select-user');
|
||||
|
||||
// Default values from observables when no data exists
|
||||
expect(selectUser.props.currentUserId).toBe('');
|
||||
expect(selectUser.props.currentTeamId).toBe('');
|
||||
});
|
||||
|
||||
it('should render correctly with current user data', async () => {
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: 'current-user-id',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const selectUser = getByTestId('select-user');
|
||||
expect(selectUser.props.currentUserId).toBe('current-user-id');
|
||||
expect(selectUser.props.currentTeamId).toBe('');
|
||||
});
|
||||
|
||||
it('should render correctly with current team data', async () => {
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID,
|
||||
value: 'current-team-id',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const selectUser = getByTestId('select-user');
|
||||
expect(selectUser.props.currentTeamId).toBe('current-team-id');
|
||||
expect(selectUser.props.currentUserId).toBe('');
|
||||
});
|
||||
|
||||
it('should render correctly with both current user and team data', async () => {
|
||||
await operator.handleSystem({
|
||||
systems: [
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: 'current-user-id',
|
||||
},
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID,
|
||||
value: 'current-team-id',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const selectUser = getByTestId('select-user');
|
||||
expect(selectUser.props.currentUserId).toBe('current-user-id');
|
||||
expect(selectUser.props.currentTeamId).toBe('current-team-id');
|
||||
});
|
||||
|
||||
it('should update observables when data changes', async () => {
|
||||
await operator.handleSystem({
|
||||
systems: [
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: 'current-user-id',
|
||||
},
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID,
|
||||
value: 'current-team-id',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const selectUser = getByTestId('select-user');
|
||||
expect(selectUser.props.currentUserId).toBe('current-user-id');
|
||||
expect(selectUser.props.currentTeamId).toBe('current-team-id');
|
||||
|
||||
await act(async () => {
|
||||
// Update current user ID
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: 'new-user-id',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(selectUser.props.currentUserId).toBe('new-user-id');
|
||||
expect(selectUser.props.currentTeamId).toBe('current-team-id');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// Update current team ID
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID,
|
||||
value: 'new-team-id',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(selectUser.props.currentUserId).toBe('new-user-id');
|
||||
expect(selectUser.props.currentTeamId).toBe('new-team-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
16
app/products/playbooks/screens/select_user/index.ts
Normal file
16
app/products/playbooks/screens/select_user/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
|
||||
import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import SelectUser from './select_user';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const withTeamId = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
}));
|
||||
|
||||
export default withDatabase(withTeamId(SelectUser));
|
||||
308
app/products/playbooks/screens/select_user/select_user.test.tsx
Normal file
308
app/products/playbooks/screens/select_user/select_user.test.tsx
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {fetchProfilesInTeam, searchProfiles} from '@actions/remote/user';
|
||||
import Button from '@components/button';
|
||||
import SearchBar from '@components/search';
|
||||
import ServerUserList from '@components/server_user_list';
|
||||
import {General, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {act, renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SelectUser from './select_user';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
const serverUrl = 'some.server.url';
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn().mockReturnValue(serverUrl),
|
||||
}));
|
||||
jest.mock('@screens/navigation');
|
||||
jest.mock('@actions/remote/user');
|
||||
|
||||
jest.mock('@components/button', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(Button).mockImplementation(
|
||||
(props) => React.createElement('Button', {testID: 'button', ...props}),
|
||||
);
|
||||
|
||||
jest.mock('@components/search', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(SearchBar).mockImplementation(
|
||||
(props) => React.createElement('SearchBar', {...props}),
|
||||
);
|
||||
|
||||
jest.mock('@components/server_user_list', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(ServerUserList).mockImplementation(
|
||||
(props) => React.createElement('ServerUserList', {...props}),
|
||||
);
|
||||
|
||||
describe('SelectUser', () => {
|
||||
let database: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof SelectUser> {
|
||||
return {
|
||||
currentTeamId: 'team-1',
|
||||
currentUserId: 'current-user',
|
||||
handleSelect: jest.fn(),
|
||||
componentId: 'PlaybookSelectUser',
|
||||
participantIds: ['participant-1', 'participant-2'],
|
||||
};
|
||||
}
|
||||
|
||||
it('renders correctly with basic props', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, queryByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const searchBar = getByTestId('selector.search_bar');
|
||||
expect(searchBar).toHaveProp('placeholder', 'Search');
|
||||
expect(searchBar).toHaveProp('onChangeText', expect.any(Function));
|
||||
expect(searchBar).toHaveProp('value', '');
|
||||
expect(searchBar).toHaveProp('autoCapitalize', 'none');
|
||||
|
||||
expect(queryByTestId('button')).toBeNull();
|
||||
|
||||
const userList = getByTestId('integration_selector.user_list');
|
||||
expect(userList).toHaveProp('currentUserId', props.currentUserId);
|
||||
expect(userList).toHaveProp('term', '');
|
||||
expect(userList).toHaveProp('tutorialWatched', true);
|
||||
expect(userList).toHaveProp('handleSelectProfile', expect.any(Function));
|
||||
expect(userList).toHaveProp('selectedIds', new Set(props.participantIds));
|
||||
expect(userList).toHaveProp('fetchFunction', expect.any(Function));
|
||||
expect(userList).toHaveProp('searchFunction', expect.any(Function));
|
||||
expect(userList).toHaveProp('createFilter', expect.any(Function));
|
||||
expect(userList).toHaveProp('location', Screens.PLAYBOOK_SELECT_USER);
|
||||
expect(userList).toHaveProp('customSection', expect.any(Function));
|
||||
});
|
||||
|
||||
it('renders no assignee button when handleRemove is provided and no search term', () => {
|
||||
const props = getBaseProps();
|
||||
props.handleRemove = jest.fn();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const button = getByTestId('button');
|
||||
expect(button).toHaveProp('text', 'No Assignee');
|
||||
expect(button).toHaveProp('onPress', expect.any(Function));
|
||||
expect(button).toHaveProp('emphasis', 'link');
|
||||
});
|
||||
|
||||
it('does not render no assignee button when search term is present', () => {
|
||||
const props = getBaseProps();
|
||||
props.handleRemove = jest.fn();
|
||||
const {queryByTestId, getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
expect(getByTestId('button')).toBeTruthy();
|
||||
|
||||
const searchBar = getByTestId('selector.search_bar');
|
||||
act(() => {
|
||||
searchBar.props.onChangeText('test');
|
||||
});
|
||||
|
||||
expect(queryByTestId('button')).toBeNull();
|
||||
});
|
||||
|
||||
it('handles search term changes correctly', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const searchBar = getByTestId('selector.search_bar');
|
||||
act(() => {
|
||||
searchBar.props.onChangeText('test');
|
||||
});
|
||||
|
||||
expect(searchBar).toHaveProp('value', 'test');
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
expect(serverUserList).toHaveProp('term', 'test');
|
||||
});
|
||||
|
||||
it('clears search term when empty string is passed', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const searchBar = getByTestId('selector.search_bar');
|
||||
act(() => {
|
||||
searchBar.props.onChangeText('test');
|
||||
});
|
||||
|
||||
expect(searchBar).toHaveProp('value', 'test');
|
||||
|
||||
act(() => {
|
||||
searchBar.props.onChangeText('');
|
||||
});
|
||||
|
||||
expect(searchBar).toHaveProp('value', '');
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
expect(serverUserList).toHaveProp('term', '');
|
||||
});
|
||||
|
||||
it('handles profile selection correctly', () => {
|
||||
const props = getBaseProps();
|
||||
const mockUser = TestHelper.fakeUser({id: 'user-1'});
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
serverUserList.props.handleSelectProfile(mockUser);
|
||||
|
||||
expect(props.handleSelect).toHaveBeenCalledWith(mockUser);
|
||||
expect(popTopScreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles remove action correctly', () => {
|
||||
const props = getBaseProps();
|
||||
props.handleRemove = jest.fn();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const button = getByTestId('button');
|
||||
button.props.onPress();
|
||||
|
||||
expect(props.handleRemove).toHaveBeenCalled();
|
||||
expect(props.handleSelect).not.toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates empty selectedIds set when selected prop is not provided', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
expect(serverUserList).toHaveProp('selectedIds', new Set());
|
||||
});
|
||||
|
||||
it('provides correct fetch function for profiles in team', async () => {
|
||||
const props = getBaseProps();
|
||||
const mockUsers = [TestHelper.fakeUser({id: 'user-1'})];
|
||||
jest.mocked(fetchProfilesInTeam).mockResolvedValue({users: mockUsers});
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
const result = await serverUserList.props.fetchFunction(1);
|
||||
|
||||
expect(fetchProfilesInTeam).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
props.currentTeamId,
|
||||
1,
|
||||
General.PROFILE_CHUNK_SIZE,
|
||||
undefined,
|
||||
{active: true},
|
||||
);
|
||||
expect(result).toEqual(mockUsers);
|
||||
});
|
||||
|
||||
it('provides correct search function for profiles', async () => {
|
||||
const props = getBaseProps();
|
||||
const mockUsers = [TestHelper.fakeUser({id: 'user-1'})];
|
||||
jest.mocked(searchProfiles).mockResolvedValue({data: mockUsers});
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
const result = await serverUserList.props.searchFunction('test');
|
||||
|
||||
expect(searchProfiles).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
'test',
|
||||
{team_id: props.currentTeamId, allow_inactive: false},
|
||||
);
|
||||
expect(result).toEqual(mockUsers);
|
||||
});
|
||||
|
||||
it('returns empty array when fetchProfilesInTeam returns no users', async () => {
|
||||
const props = getBaseProps();
|
||||
jest.mocked(fetchProfilesInTeam).mockResolvedValue({users: []});
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
const result = await serverUserList.props.fetchFunction(1);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when searchProfiles returns no data', async () => {
|
||||
const props = getBaseProps();
|
||||
jest.mocked(searchProfiles).mockResolvedValue({data: []});
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
const result = await serverUserList.props.searchFunction('test');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('creates user filter that prioritizes exact username matches', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
const createFilter = serverUserList.props.createFilter;
|
||||
const exactMatches: UserProfile[] = [];
|
||||
const filter = createFilter(exactMatches, 'test');
|
||||
|
||||
const user1 = TestHelper.fakeUser({username: 'test'});
|
||||
const user2 = TestHelper.fakeUser({username: 'testuser'});
|
||||
const user3 = TestHelper.fakeUser({username: 'other'});
|
||||
|
||||
expect(filter(user1)).toBe(false);
|
||||
expect(filter(user2)).toBe(false);
|
||||
expect(filter(user3)).toBe(true);
|
||||
|
||||
expect(exactMatches).toContain(user1);
|
||||
expect(exactMatches).toContain(user2);
|
||||
expect(exactMatches).not.toContain(user3);
|
||||
});
|
||||
|
||||
it('creates custom sections for participants and non-participants', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
const customSection = serverUserList.props.customSection;
|
||||
|
||||
const participantUser = TestHelper.fakeUser({id: 'participant-1'});
|
||||
const nonParticipantUser = TestHelper.fakeUser({id: 'non-participant'});
|
||||
const profiles = [nonParticipantUser, participantUser];
|
||||
|
||||
const sections = customSection(profiles);
|
||||
|
||||
expect(sections).toHaveLength(2);
|
||||
expect(sections[0].id).toBe('RUN PARTICIPANTS');
|
||||
expect(sections[0].data).toContain(participantUser);
|
||||
expect(sections[1].id).toBe('NOT PARTICIPATING');
|
||||
expect(sections[1].data).toContain(nonParticipantUser);
|
||||
});
|
||||
|
||||
it('returns empty array when no profiles provided to custom section', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<SelectUser {...props}/>, {database});
|
||||
|
||||
const serverUserList = getByTestId('integration_selector.user_list');
|
||||
const customSection = serverUserList.props.customSection;
|
||||
|
||||
const sections = customSection([]);
|
||||
|
||||
expect(sections).toEqual([]);
|
||||
});
|
||||
});
|
||||
270
app/products/playbooks/screens/select_user/select_user.tsx
Normal file
270
app/products/playbooks/screens/select_user/select_user.tsx
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchProfilesInTeam, searchProfiles} from '@actions/remote/user';
|
||||
import Button from '@components/button';
|
||||
import SearchBar from '@components/search';
|
||||
import ServerUserList from '@components/server_user_list';
|
||||
import {General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {
|
||||
popTopScreen,
|
||||
} from '@screens/navigation';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const close = () => {
|
||||
popTopScreen();
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
currentTeamId: string;
|
||||
currentUserId: string;
|
||||
handleSelect: (opt: UserProfile) => void;
|
||||
handleRemove?: () => void;
|
||||
selected?: string;
|
||||
componentId: AvailableScreens;
|
||||
participantIds: string[];
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
participants: {
|
||||
id: 'playbooks.select_user.participants',
|
||||
defaultMessage: 'RUN PARTICIPANTS',
|
||||
},
|
||||
notParticipants: {
|
||||
id: 'playbooks.select_user.not_participants',
|
||||
defaultMessage: 'NOT PARTICIPATING',
|
||||
},
|
||||
});
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
searchBar: {
|
||||
marginVertical: 12,
|
||||
height: 38,
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
searchbarComponentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
noResultContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
...typography('Body', 600, 'Regular'),
|
||||
},
|
||||
searchBarInput: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function SelectUser({
|
||||
selected,
|
||||
handleSelect,
|
||||
handleRemove,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
componentId,
|
||||
participantIds,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
// HOOKS
|
||||
const [term, setTerm] = useState<string>('');
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
if (selected) {
|
||||
return new Set([selected]);
|
||||
}
|
||||
return new Set<string>();
|
||||
}, [selected]);
|
||||
|
||||
// Callbacks
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
}, []);
|
||||
|
||||
const handleSelectProfile = useCallback((user: UserProfile): void => {
|
||||
handleSelect(user);
|
||||
close();
|
||||
}, [handleSelect]);
|
||||
|
||||
const handleSelectRemove = useCallback(() => {
|
||||
handleRemove?.();
|
||||
close();
|
||||
}, [handleRemove]);
|
||||
|
||||
// Effects
|
||||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
searchTimeoutId.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
if (!text) {
|
||||
clearSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
setTerm(text);
|
||||
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
}, [clearSearch]);
|
||||
|
||||
const userFetchFunction = useCallback(async (userFetchPage: number) => {
|
||||
const result = await fetchProfilesInTeam(serverUrl, currentTeamId, userFetchPage, General.PROFILE_CHUNK_SIZE, undefined, {active: true});
|
||||
if (result.users?.length) {
|
||||
return result.users;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [serverUrl, currentTeamId]);
|
||||
|
||||
const userSearchFunction = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
const results = await searchProfiles(serverUrl, lowerCasedTerm, {team_id: currentTeamId, allow_inactive: false});
|
||||
|
||||
if (results.data) {
|
||||
return results.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [currentTeamId, serverUrl]);
|
||||
|
||||
const createUserFilter = useCallback((exactMatches: UserProfile[], searchTerm: string) => {
|
||||
return (p: UserProfile) => {
|
||||
if (p.username === searchTerm || p.username.startsWith(searchTerm)) {
|
||||
exactMatches.push(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const customSection = useCallback((profiles: UserProfile[]) => {
|
||||
if (!profiles.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sections = new Map<string, UserProfile[]>();
|
||||
const participantsKey = intl.formatMessage(messages.participants);
|
||||
const notParticipantsKey = intl.formatMessage(messages.notParticipants);
|
||||
|
||||
const participantsMap = new Set<string>();
|
||||
participantIds.forEach((id) => {
|
||||
participantsMap.add(id);
|
||||
});
|
||||
|
||||
profiles.forEach((p) => {
|
||||
const sectionKey = participantsMap.has(p.id) ? participantsKey : notParticipantsKey;
|
||||
const sectionValue = sections.get(sectionKey) || [];
|
||||
const section = [...sectionValue, p];
|
||||
sections.set(sectionKey, section);
|
||||
});
|
||||
|
||||
const results = [];
|
||||
let index = 0;
|
||||
for (const k of [participantsKey, notParticipantsKey]) { // This is to ensure order of sections
|
||||
const v = sections.get(k) || [];
|
||||
if (v.length) {
|
||||
results.push({
|
||||
first: index === 0,
|
||||
id: k,
|
||||
data: v,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}, [intl, participantIds]);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={style.container}
|
||||
>
|
||||
<View
|
||||
testID='integration_selector.screen'
|
||||
style={style.searchBar}
|
||||
>
|
||||
<SearchBar
|
||||
testID='selector.search_bar'
|
||||
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
inputStyle={style.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
containerStyle={style.searchbarComponentContainer}
|
||||
/>
|
||||
{Boolean(handleRemove) && !term && (
|
||||
<Button
|
||||
text={intl.formatMessage({id: 'playbooks.select_user.no_assignee', defaultMessage: 'No Assignee'})}
|
||||
theme={theme}
|
||||
onPress={handleSelectRemove}
|
||||
emphasis='link'
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<ServerUserList
|
||||
currentUserId={currentUserId}
|
||||
term={term}
|
||||
tutorialWatched={true}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
selectedIds={selectedIds}
|
||||
fetchFunction={userFetchFunction}
|
||||
searchFunction={userSearchFunction}
|
||||
createFilter={createUserFilter}
|
||||
testID={'integration_selector.user_list'}
|
||||
location={Screens.PLAYBOOK_SELECT_USER}
|
||||
customSection={customSection}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectUser;
|
||||
|
|
@ -107,9 +107,6 @@ declare class PlaybookRunModel extends Model {
|
|||
/** checklists : The CHECKLISTS associated with this Playbook Run */
|
||||
checklists: Query<PlaybookChecklistModel>;
|
||||
|
||||
/** participants : The USERS that participate in this Playbook Run */
|
||||
participants: () => Query<UserModel>;
|
||||
|
||||
/** prepareDestroyWithRelations : Prepare the model for deletion with its relations */
|
||||
prepareDestroyWithRelations: () => Promise<Model[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,10 @@ describe('run utils', () => {
|
|||
|
||||
expect(isRunFinished(run)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for undefined run', () => {
|
||||
expect(isRunFinished(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaxRunUpdateAt', () => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ export function getRunScheduledTimestamp(run: PlaybookRunModel | PlaybookRun): n
|
|||
return timestamp;
|
||||
}
|
||||
|
||||
export function isRunFinished(run: PlaybookRunModel | PlaybookRun): boolean {
|
||||
export function isRunFinished(run?: PlaybookRunModel | PlaybookRun): boolean {
|
||||
if (!run) {
|
||||
return true;
|
||||
}
|
||||
const currentStatus = 'currentStatus' in run ? run.currentStatus : run.current_status;
|
||||
return currentStatus === 'Finished';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,10 +110,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
|
||||
const newSelectedIds = Object.assign({}, list);
|
||||
|
||||
Reflect.deleteProperty(newSelectedIds, id);
|
||||
function removeProfileFromList(list: Set<string>, id: string) {
|
||||
const newSelectedIds = new Set(list);
|
||||
newSelectedIds.delete(id);
|
||||
return newSelectedIds;
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +136,7 @@ export default function ChannelAddMembers({
|
|||
|
||||
const [term, setTerm] = useState('');
|
||||
const [addingMembers, setAddingMembers] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set<string>());
|
||||
const [showBanner, setShowBanner] = useState(Boolean(channel?.abacPolicyEnforced));
|
||||
|
||||
// Use the hook to fetch access control attributes
|
||||
|
|
@ -184,12 +183,12 @@ export default function ChannelAddMembers({
|
|||
const handleSelectProfile = useCallback((user: UserProfile) => {
|
||||
clearSearch();
|
||||
setSelectedIds((current) => {
|
||||
if (current[user.id]) {
|
||||
if (current.has(user.id)) {
|
||||
return removeProfileFromList(current, user.id);
|
||||
}
|
||||
|
||||
const newSelectedIds = Object.assign({}, current);
|
||||
newSelectedIds[user.id] = user;
|
||||
const newSelectedIds = new Set(current);
|
||||
newSelectedIds.add(user.id);
|
||||
|
||||
return newSelectedIds;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import BaseChip from '@components/chips/base_chip';
|
|||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
import {useBooleanProp, useStringProp} from './hooks';
|
||||
import {useBooleanProp, useDropdownProp, useStringProp} from './hooks';
|
||||
import {buildComponent} from './utils';
|
||||
|
||||
const propPossibilities = {};
|
||||
|
|
@ -18,13 +18,15 @@ const onPress = () => Alert.alert('Button pressed!');
|
|||
const ChipComponentLibrary = () => {
|
||||
const theme = useTheme();
|
||||
const [label, labelSelector] = useStringProp('label', 'Chip text', false);
|
||||
const [showRemoveOption, showRemoveOptionSelector] = useBooleanProp('showRemoveOption', false);
|
||||
const [actionIcon, actionIconPossibilities, actionIconSelector] = useDropdownProp('actionIcon', 'remove', ['remove', 'downArrow'], true);
|
||||
const [hasPrefix, hasPrefixSelector] = useBooleanProp('hasPrefix', false);
|
||||
|
||||
const components = useMemo(
|
||||
() => buildComponent(BaseChip, propPossibilities, [], [
|
||||
() => buildComponent(BaseChip, propPossibilities, [
|
||||
actionIconPossibilities,
|
||||
], [
|
||||
label,
|
||||
showRemoveOption,
|
||||
actionIcon,
|
||||
{
|
||||
onPress,
|
||||
prefix: hasPrefix.hasPrefix ? (
|
||||
|
|
@ -39,13 +41,13 @@ const ChipComponentLibrary = () => {
|
|||
theme,
|
||||
},
|
||||
]),
|
||||
[label, showRemoveOption, hasPrefix.hasPrefix, theme],
|
||||
[label, actionIconPossibilities, actionIcon, hasPrefix.hasPrefix, theme],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{labelSelector}
|
||||
{showRemoveOptionSelector}
|
||||
{actionIconSelector}
|
||||
{hasPrefixSelector}
|
||||
<View style={{flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 16}}>{components}</View>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {dismissModal, setButtons} from '@screens/navigation';
|
|||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
|
|
@ -95,10 +94,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
|
||||
const newSelectedIds = Object.assign({}, list);
|
||||
|
||||
Reflect.deleteProperty(newSelectedIds, id);
|
||||
function removeProfileFromList(list: Set<string>, id: string) {
|
||||
const newSelectedIds = new Set(list);
|
||||
newSelectedIds.delete(id);
|
||||
return newSelectedIds;
|
||||
}
|
||||
|
||||
|
|
@ -122,9 +120,9 @@ export default function CreateDirectMessage({
|
|||
|
||||
const [term, setTerm] = useState('');
|
||||
const [startingConversation, setStartingConversation] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set<string>());
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
const selectedCount = selectedIds.size;
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
|
|
@ -134,17 +132,15 @@ export default function CreateDirectMessage({
|
|||
setSelectedIds((current) => removeProfileFromList(current, id));
|
||||
}, []);
|
||||
|
||||
const createDirectChannel = useCallback(async (id: string, selectedUser?: UserProfile): Promise<boolean> => {
|
||||
const user = selectedUser || selectedIds[id];
|
||||
const displayName = displayUsername(user, intl.locale, teammateNameDisplay);
|
||||
const result = await makeDirectChannel(serverUrl, id, displayName);
|
||||
const createDirectChannel = useCallback(async (id: string): Promise<boolean> => {
|
||||
const result = await makeDirectChannel(serverUrl, id);
|
||||
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(intl, result.error, messages.dm);
|
||||
}
|
||||
|
||||
return !result.error;
|
||||
}, [selectedIds, intl, teammateNameDisplay, serverUrl]);
|
||||
}, [intl, serverUrl]);
|
||||
|
||||
const createGroupChannel = useCallback(async (ids: string[]): Promise<boolean> => {
|
||||
const result = await makeGroupChannel(serverUrl, ids);
|
||||
|
|
@ -156,7 +152,7 @@ export default function CreateDirectMessage({
|
|||
return !result.error;
|
||||
}, [intl, serverUrl]);
|
||||
|
||||
const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}, selectedUser?: UserProfile) => {
|
||||
const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}) => {
|
||||
if (startingConversation) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -170,7 +166,7 @@ export default function CreateDirectMessage({
|
|||
} else if (idsToUse.length > 1) {
|
||||
success = await createGroupChannel(idsToUse);
|
||||
} else {
|
||||
success = await createDirectChannel(idsToUse[0], selectedUser);
|
||||
success = await createDirectChannel(idsToUse[0]);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
|
|
@ -186,25 +182,21 @@ export default function CreateDirectMessage({
|
|||
[currentUserId]: true,
|
||||
};
|
||||
|
||||
startConversation(selectedId, user);
|
||||
startConversation(selectedId);
|
||||
} else {
|
||||
clearSearch();
|
||||
setSelectedIds((current) => {
|
||||
if (current[user.id]) {
|
||||
if (current.has(user.id)) {
|
||||
return removeProfileFromList(current, user.id);
|
||||
}
|
||||
|
||||
const wasSelected = current[user.id];
|
||||
|
||||
if (!wasSelected && selectedCount >= General.MAX_USERS_IN_GM) {
|
||||
if (selectedCount >= General.MAX_USERS_IN_GM) {
|
||||
setShowToast(true);
|
||||
return current;
|
||||
}
|
||||
|
||||
const newSelectedIds = Object.assign({}, current);
|
||||
if (!wasSelected) {
|
||||
newSelectedIds[user.id] = user;
|
||||
}
|
||||
const newSelectedIds = new Set(current);
|
||||
newSelectedIds.add(user.id);
|
||||
|
||||
return newSelectedIds;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -560,6 +560,14 @@ function IntegrationSelector(
|
|||
};
|
||||
}, []);
|
||||
|
||||
const selectedUserIds = useMemo<Set<string>>(() => {
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
|
||||
return new Set(Object.keys(selectedIds));
|
||||
}
|
||||
|
||||
return new Set();
|
||||
}, [dataSource, selectedIds]);
|
||||
|
||||
const renderDataTypeList = () => {
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS:
|
||||
|
|
@ -569,7 +577,7 @@ function IntegrationSelector(
|
|||
term={term}
|
||||
tutorialWatched={true}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
selectedIds={selectedIds as {[id: string]: UserProfile}}
|
||||
selectedIds={selectedUserIds}
|
||||
fetchFunction={userFetchFunction}
|
||||
searchFunction={userSearchFunction}
|
||||
createFilter={createUserFilter}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const sortUsers = (a: UserProfile, b: UserProfile, locale: string, teammateDispl
|
|||
const MANAGE_BUTTON = 'manage-button';
|
||||
const EMPTY: UserProfile[] = [];
|
||||
const EMPTY_MEMBERS: ChannelMembership[] = [];
|
||||
const EMPTY_IDS = {};
|
||||
const EMPTY_IDS = new Set<string>();
|
||||
const {USER_PROFILE} = Screens;
|
||||
const CLOSE_BUTTON_ID = 'close-user-profile';
|
||||
const TEST_ID = 'manage_members';
|
||||
|
|
|
|||
|
|
@ -1035,6 +1035,9 @@
|
|||
"playbooks.runs.in_progress.description": "When a run starts in this channel, you’ll see it here.",
|
||||
"playbooks.runs.in_progress.title": "No in progress runs",
|
||||
"playbooks.runs.show_more": "Show More",
|
||||
"playbooks.select_user.no_assignee": "No Assignee",
|
||||
"playbooks.select_user.not_participants": "NOT PARTICIPATING",
|
||||
"playbooks.select_user.participants": "RUN PARTICIPANTS",
|
||||
"playbooks.status_update_post.num_tasks": "**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked",
|
||||
"playbooks.status_update_post.participants": "{numParticipants, number} {numParticipants, plural, =1 {participant} other {participants}}",
|
||||
"playbooks.status_update_post.update": "@{authorUsername} posted an update for [{runName}]({link})",
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,6 @@ class TestHelperSingleton {
|
|||
channel: this.fakeRelation(),
|
||||
owner: this.fakeRelation(),
|
||||
checklists: this.fakeQuery([]),
|
||||
participants: () => this.fakeQuery([]),
|
||||
prepareDestroyWithRelations: jest.fn().mockResolvedValue([]),
|
||||
previousReminder: 0,
|
||||
itemsOrder: [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue