diff --git a/app/components/chips/base_chip.test.tsx b/app/components/chips/base_chip.test.tsx
index a53e196ea..6b95394e2 100644
--- a/app/components/chips/base_chip.test.tsx
+++ b/app/components/chips/base_chip.test.tsx
@@ -11,6 +11,7 @@ import BaseChip from './base_chip';
describe('BaseChip', () => {
const onPressMock = jest.fn();
+ const onActionPressMock = jest.fn();
afterEach(() => {
jest.clearAllMocks();
@@ -28,66 +29,66 @@ describe('BaseChip', () => {
expect(getByText('Test Label')).toBeTruthy();
});
- it('should render with the X button when actionIcon is remove', () => {
+ it('should render with the X button when action is remove', () => {
const {getByTestId} = renderWithIntlAndTheme(
,
);
- expect(getByTestId('base_chip.remove.button')).toBeTruthy();
+ expect(getByTestId('base_chip.remove.icon')).toBeTruthy();
});
- it('should render with the chevron down button when actionIcon is downArrow', () => {
+ it('should render with the chevron down button when action is downArrow', () => {
const {getByTestId} = renderWithIntlAndTheme(
,
);
- expect(getByTestId('base_chip.downArrow.button')).toBeTruthy();
+ expect(getByTestId('base_chip.downArrow.icon')).toBeTruthy();
});
- it('should not render the X button when actionIcon is undefined', () => {
+ it('should not render the action button when action is undefined', () => {
const {queryByTestId} = renderWithIntlAndTheme(
,
);
- expect(queryByTestId('base_chip.remove.button')).toBeNull();
+ expect(queryByTestId('base_chip.remove.icon')).toBeNull();
+ expect(queryByTestId('base_chip.downArrow.icon')).toBeNull();
});
- it('should call onPress when the X button is pressed', () => {
+ it('should call onPressMock when the action button is pressed', () => {
const {getByTestId} = renderWithIntlAndTheme(
,
);
fireEvent.press(getByTestId('base_chip.remove.button'));
- expect(onPressMock).toHaveBeenCalledTimes(1);
+ expect(onPressMock).not.toHaveBeenCalled();
+ expect(onActionPressMock).toHaveBeenCalledTimes(1);
});
- it('should call onPress when the chip is pressed without X button', () => {
+ it('should call onPress when the chip is pressed without action button', () => {
const {getByTestId} = renderWithIntlAndTheme(
,
);
@@ -95,6 +96,21 @@ describe('BaseChip', () => {
expect(onPressMock).toHaveBeenCalledTimes(1);
});
+ it('should call onPress when the chip is pressed and we have an action button', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ fireEvent.press(getByTestId('base_chip.chip_button'));
+ expect(onPressMock).toHaveBeenCalledTimes(1);
+ expect(onActionPressMock).not.toHaveBeenCalled();
+ });
+
it('should handle animations when showAnimation is true', () => {
const {getByTestId} = renderWithIntlAndTheme(
void;
testID?: string;
- actionIcon?: 'remove' | 'downArrow';
+ action?: {
+ icon: 'remove' | 'downArrow';
+ onPress?: () => void;
+ };
showAnimation?: boolean;
label: string;
prefix?: JSX.Element;
@@ -70,7 +73,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
export default function BaseChip({
testID,
onPress,
- actionIcon,
+ action,
showAnimation,
label,
prefix,
@@ -78,6 +81,9 @@ export default function BaseChip({
type = 'normal',
boldText = false,
}: SelectedChipProps) {
+ const actionIcon = action?.icon;
+ const actionOnPress = action?.onPress;
+
const theme = useTheme();
const style = getStyleFromTheme(theme);
const dimensions = useWindowDimensions();
@@ -118,30 +124,49 @@ export default function BaseChip({
let content = chipContent;
if (actionIcon) {
const iconName = actionIcon === 'remove' ? 'close-circle' : 'chevron-down';
- content = (
- <>
- {chipContent}
+ let icon = (
+
+ );
+
+ if (actionOnPress) {
+ icon = (
-
+ {icon}
+ );
+ } else {
+ icon = (
+
+ {icon}
+
+ );
+ }
+
+ content = (
+ <>
+ {content}
+ {icon}
>
);
- } else if (onPress) {
+ }
+
+ if (onPress) {
content = (
- {chipContent}
+ {content}
);
}
diff --git a/app/components/chips/selected_chip.test.tsx b/app/components/chips/selected_chip.test.tsx
index f95fadaf7..d3202819a 100644
--- a/app/components/chips/selected_chip.test.tsx
+++ b/app/components/chips/selected_chip.test.tsx
@@ -32,10 +32,10 @@ describe('SelectedChip', () => {
const baseChip = getByTestId('selected-chip');
expect(baseChip.props.label).toBe('Test Chip');
- expect(baseChip.props.actionIcon).toBe('remove');
+ expect(baseChip.props.action).toEqual({icon: 'remove', onPress: expect.any(Function)});
expect(baseChip.props.showAnimation).toBe(true);
expect(baseChip.props.prefix).toBeUndefined();
- baseChip.props.onPress();
+ baseChip.props.action.onPress();
expect(mockOnRemove).toHaveBeenCalledTimes(1);
expect(mockOnRemove).toHaveBeenCalledWith('test-id');
});
diff --git a/app/components/chips/selected_chip.tsx b/app/components/chips/selected_chip.tsx
index 2f4ddbbb6..f6a92e7f7 100644
--- a/app/components/chips/selected_chip.tsx
+++ b/app/components/chips/selected_chip.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {useCallback} from 'react';
+import React, {useMemo} from 'react';
import BaseChip from './base_chip';
@@ -18,15 +18,12 @@ export default function SelectedChip({
onRemove,
testID,
}: SelectedChipProps) {
- const onPress = useCallback(() => {
- onRemove(id);
- }, [onRemove, id]);
+ const action = useMemo(() => ({icon: 'remove' as const, onPress: () => onRemove(id)}), [id, onRemove]);
return (
diff --git a/app/components/chips/selected_user_chip.test.tsx b/app/components/chips/selected_user_chip.test.tsx
index 303156984..36cf0e8d7 100644
--- a/app/components/chips/selected_user_chip.test.tsx
+++ b/app/components/chips/selected_user_chip.test.tsx
@@ -32,9 +32,9 @@ describe('SelectedUserChip', () => {
const userChip = getByTestId('selected-user-chip');
expect(userChip.props.user).toBe(mockUser);
expect(userChip.props.teammateNameDisplay).toBe('username');
- expect(userChip.props.actionIcon).toBe('remove');
+ expect(userChip.props.action).toEqual({icon: 'remove', onPress: expect.any(Function)});
expect(userChip.props.showAnimation).toBe(true);
- userChip.props.onPress();
+ userChip.props.action.onPress();
expect(mockOnPress).toHaveBeenCalledTimes(1);
});
});
diff --git a/app/components/chips/selected_user_chip.tsx b/app/components/chips/selected_user_chip.tsx
index a05087d7f..2f5b899a4 100644
--- a/app/components/chips/selected_user_chip.tsx
+++ b/app/components/chips/selected_user_chip.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react';
+import React, {useMemo} from 'react';
import UserChip from './user_chip';
@@ -20,11 +20,11 @@ export default function SelectedUserChip({
teammateNameDisplay,
onPress,
}: SelectedChipProps) {
+ const action = useMemo(() => ({icon: 'remove' as const, onPress}), [onPress]);
return (
React.createElement('P
describe('UserChip', () => {
const mockOnPress = jest.fn();
+ const mockOnActionPress = jest.fn();
const mockUser = TestHelper.fakeUser({id: 'user-id', username: 'test-user'});
it('should render with the correct props', () => {
@@ -34,13 +35,13 @@ describe('UserChip', () => {
testID='user-chip'
teammateNameDisplay='username'
showAnimation={true}
- actionIcon='remove'
+ action={{icon: 'remove', onPress: mockOnActionPress}}
/>,
);
const baseChip = getByTestId('user-chip');
expect(baseChip.props.label).toBe('test-user');
- expect(baseChip.props.actionIcon).toBe('remove');
+ expect(baseChip.props.action).toEqual({icon: 'remove', onPress: expect.any(Function)});
expect(baseChip.props.showAnimation).toBe(true);
expect(baseChip.props.prefix).toBeDefined();
@@ -53,5 +54,42 @@ describe('UserChip', () => {
baseChip.props.onPress();
expect(mockOnPress).toHaveBeenCalledTimes(1);
expect(mockOnPress).toHaveBeenCalledWith('user-id');
+ expect(mockOnActionPress).not.toHaveBeenCalled();
+
+ mockOnPress.mockClear();
+
+ baseChip.props.action.onPress();
+ expect(mockOnActionPress).toHaveBeenCalledTimes(1);
+ expect(mockOnActionPress).toHaveBeenCalledWith('user-id');
+ expect(mockOnPress).not.toHaveBeenCalled();
+ });
+
+ it('should leave action onPress undefined when action with no onPress is provided', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ const baseChip = getByTestId('user-chip');
+ expect(baseChip.props.action.onPress).toBeUndefined();
+ });
+
+ it('should leave onPress undefined when onPress is not provided', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ const baseChip = getByTestId('user-chip');
+ expect(baseChip.props.onPress).toBeUndefined();
});
});
diff --git a/app/components/chips/user_chip.tsx b/app/components/chips/user_chip.tsx
index d57f06298..42ce98dec 100644
--- a/app/components/chips/user_chip.tsx
+++ b/app/components/chips/user_chip.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {useCallback, useMemo} from 'react';
+import React, {useMemo} from 'react';
import {useIntl} from 'react-intl';
import ProfilePicture from '@components/profile_picture';
@@ -13,10 +13,13 @@ import type UserModel from '@typings/database/models/servers/user';
type SelectedChipProps = {
user: UserModel | UserProfile;
- onPress: (id: string) => void;
+ onPress?: (id: string) => void;
testID?: string;
teammateNameDisplay: string;
- actionIcon?: 'remove' | 'downArrow';
+ action?: {
+ icon: 'remove' | 'downArrow';
+ onPress?: (id: string) => void;
+ };
showAnimation?: boolean;
}
@@ -25,15 +28,26 @@ export default function UserChip({
user,
teammateNameDisplay,
onPress: receivedOnPress,
- actionIcon,
+ action: receivedAction,
showAnimation,
}: SelectedChipProps) {
const intl = useIntl();
- const onPress = useCallback(() => {
- receivedOnPress(user.id);
+ const onPress = useMemo(() => {
+ if (!receivedOnPress) {
+ return undefined;
+ }
+ return () => receivedOnPress(user.id);
}, [receivedOnPress, user.id]);
+ const action = useMemo(() => {
+ if (!receivedAction) {
+ return undefined;
+ }
+ const onActionPress = receivedAction.onPress ? (() => receivedAction.onPress?.(user.id)) : undefined;
+ return {icon: receivedAction.icon, onPress: onActionPress};
+ }, [receivedAction, user.id]);
+
const name = displayUsername(user, intl.locale, teammateNameDisplay);
const picture = useMemo(() => (
{
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);
+ expect(ownerChip.props.action).toBe(undefined);
ownerChip.props.onPress();
expect(openUserProfileModal).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
@@ -213,7 +213,31 @@ describe('PlaybookRun', () => {
const {getByTestId} = renderWithEverything(, {database});
const ownerChip = getByTestId('user-chip');
- expect(ownerChip).toHaveProp('actionIcon', 'downArrow');
+ expect(ownerChip).toHaveProp('action', {icon: 'downArrow', onPress: expect.any(Function)});
+ ownerChip.props.action.onPress();
+
+ expect(goToSelectUser).toHaveBeenCalledWith(
+ 'Owner',
+ [...props.participants.map((p) => p.id), props.owner!.id],
+ props.owner!.id,
+ expect.any(Function),
+ );
+ expect(openUserProfileModal).not.toHaveBeenCalled();
+
+ let 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();
+
+ jest.mocked(goToSelectUser).mockClear();
+ jest.mocked(setOwner).mockClear();
+
+ // Test also pressing on the whole chip
ownerChip.props.onPress();
expect(goToSelectUser).toHaveBeenCalledWith(
@@ -224,7 +248,7 @@ describe('PlaybookRun', () => {
);
expect(openUserProfileModal).not.toHaveBeenCalled();
- const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
+ handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
expect(setOwner).toHaveBeenCalledWith(
diff --git a/app/products/playbooks/screens/playbook_run/playbook_run.tsx b/app/products/playbooks/screens/playbook_run/playbook_run.tsx
index 3849ee1a7..47f4fe027 100644
--- a/app/products/playbooks/screens/playbook_run/playbook_run.tsx
+++ b/app/products/playbooks/screens/playbook_run/playbook_run.tsx
@@ -207,6 +207,13 @@ export default function PlaybookRun({
);
}, [handleSelectOwner, intl, owner, participants]);
+ const ownerAction = useMemo(() => {
+ if (readOnly) {
+ return undefined;
+ }
+ return {icon: 'downArrow' as const, onPress: openChangeOwnerModal};
+ }, [openChangeOwnerModal, readOnly]);
+
if (!playbookRun) {
return ;
}
@@ -255,7 +262,7 @@ export default function PlaybookRun({
user={owner}
onPress={readOnly ? openOwnerProfile : openChangeOwnerModal}
teammateNameDisplay={teammateNameDisplay}
- actionIcon={readOnly ? undefined : 'downArrow'}
+ action={ownerAction}
/>
diff --git a/app/screens/component_library/chip.cl.tsx b/app/screens/component_library/chip.cl.tsx
index e67f823e0..b528b3f07 100644
--- a/app/screens/component_library/chip.cl.tsx
+++ b/app/screens/component_library/chip.cl.tsx
@@ -14,6 +14,7 @@ import {buildComponent} from './utils';
const propPossibilities = {};
const onPress = () => Alert.alert('Button pressed!');
+const onActionPress = () => Alert.alert('Action pressed!');
const ChipComponentLibrary = () => {
const theme = useTheme();
@@ -21,9 +22,21 @@ const ChipComponentLibrary = () => {
const [actionIcon, actionIconPossibilities, actionIconSelector] = useDropdownProp('actionIcon', 'remove', ['remove', 'downArrow'], true);
const [hasPrefix, hasPrefixSelector] = useBooleanProp('hasPrefix', false);
+ const actionPossibilities = useMemo(() => {
+ if (!actionIconPossibilities) {
+ return undefined;
+ }
+ return {
+ action: actionIconPossibilities.actionIcon.map((iconName) => ({
+ icon: iconName,
+ onPress: onActionPress,
+ })),
+ };
+ }, [actionIconPossibilities]);
+
const components = useMemo(
() => buildComponent(BaseChip, propPossibilities, [
- actionIconPossibilities,
+ actionPossibilities,
], [
label,
actionIcon,
@@ -41,7 +54,7 @@ const ChipComponentLibrary = () => {
theme,
},
]),
- [label, actionIconPossibilities, actionIcon, hasPrefix.hasPrefix, theme],
+ [actionPossibilities, label, actionIcon, hasPrefix.hasPrefix, theme],
);
return (
diff --git a/app/screens/component_library/utils.tsx b/app/screens/component_library/utils.tsx
index dddb6ff21..052fe732b 100644
--- a/app/screens/component_library/utils.tsx
+++ b/app/screens/component_library/utils.tsx
@@ -40,7 +40,7 @@ function buildPropString(inputProps: {[x: string]: any}) {
export function buildComponent(
Component: React.ComponentType,
propPossibilities: {[x: string]: any[]},
- dropdownPossibilities: Array<{[x: string]: string[]} | undefined>,
+ dropdownPossibilities: Array<{[x: string]: unknown[]} | undefined>,
setProps: Array<{[x: string]: any} | undefined>,
) {
const res: React.ReactNode[] = [];