fix: selected items on a multiselect change when do a new search (#5145)

* fix: selected items on a multiselect change when do a new search

* fix livechateditview

* minor tweak at const

* update uikit storyshot and uikit handle the multiStaticSelect

* add e2e test

* minor tweak
This commit is contained in:
Reinaldo Neto 2023-08-03 16:37:14 -03:00 committed by GitHub
parent a203f67a4a
commit 77a81d577e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 100 additions and 55 deletions

View File

@ -31,6 +31,7 @@ const Chip = ({ item, onSelect, style }: IChip) => {
onPress={() => onSelect(item)} onPress={() => onSelect(item)}
style={[styles.chip, { backgroundColor: colors.auxiliaryBackground }, style]} style={[styles.chip, { backgroundColor: colors.auxiliaryBackground }, style]}
background={Touchable.Ripple(colors.bannerBackground)} background={Touchable.Ripple(colors.bannerBackground)}
testID={`multi-select-chip-${item.value}`}
> >
<> <>
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null} {item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}

View File

@ -13,13 +13,13 @@ import { CustomIcon } from '../../CustomIcon';
interface IItem { interface IItem {
item: IItemData; item: IItemData;
selected?: string; selected: boolean;
onSelect: Function; onSelect: Function;
} }
interface IItems { interface IItems {
items: IItemData[]; items: IItemData[];
selected: string[]; selected: IItemData[];
onSelect: Function; onSelect: Function;
} }
@ -54,7 +54,7 @@ const Items = ({ items, selected, onSelect }: IItems) => (
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
renderItem={({ item }) => <Item item={item} onSelect={onSelect} selected={selected.find(s => s === item.value)} />} renderItem={({ item }) => <Item item={item} onSelect={onSelect} selected={!!selected.find(s => s.value === item.value)} />}
/> />
); );

View File

@ -17,16 +17,16 @@ interface IMultiSelectContentProps {
options?: IItemData[]; options?: IItemData[];
multiselect: boolean; multiselect: boolean;
select: React.Dispatch<any>; select: React.Dispatch<any>;
onChange: Function; onChange: ({ value }: { value: string[] }) => void;
setCurrentValue: React.Dispatch<React.SetStateAction<string>>; setCurrentValue: React.Dispatch<React.SetStateAction<string>>;
onHide: Function; onHide: Function;
selectedItems: string[]; selectedItems: IItemData[];
} }
export const MultiSelectContent = React.memo( export const MultiSelectContent = React.memo(
({ onSearch, options, multiselect, select, onChange, setCurrentValue, onHide, selectedItems }: IMultiSelectContentProps) => { ({ onSearch, options, multiselect, select, onChange, setCurrentValue, onHide, selectedItems }: IMultiSelectContentProps) => {
const { colors } = useTheme(); const { colors } = useTheme();
const [selected, setSelected] = useState<string[]>(Array.isArray(selectedItems) ? selectedItems : []); const [selected, setSelected] = useState<IItemData[]>(Array.isArray(selectedItems) ? selectedItems : []);
const [items, setItems] = useState<IItemData[] | undefined>(options); const [items, setItems] = useState<IItemData[] | undefined>(options);
const { hideActionSheet } = useActionSheet(); const { hideActionSheet } = useActionSheet();
@ -37,14 +37,14 @@ export const MultiSelectContent = React.memo(
} = item; } = item;
if (multiselect) { if (multiselect) {
let newSelect = []; let newSelect = [];
if (!selected.includes(value)) { if (!selected.find(s => s.value === value)) {
newSelect = [...selected, value]; newSelect = [...selected, item];
} else { } else {
newSelect = selected.filter((s: any) => s !== value); newSelect = selected.filter((s: any) => s.value !== value);
} }
setSelected(newSelect); setSelected(newSelect);
select(newSelect); select(newSelect);
onChange({ value: newSelect }); onChange({ value: newSelect.map(s => s.value) });
} else { } else {
onChange({ value }); onChange({ value });
setCurrentValue(text); setCurrentValue(text);

View File

@ -17,13 +17,21 @@ export interface IItemData {
imageUrl?: string; imageUrl?: string;
} }
interface IMultiSelectWithMultiSelect extends IMultiSelect {
multiselect: true;
onChange: ({ value }: { value: string[] }) => void;
}
interface IMultiSelectWithoutMultiSelect extends IMultiSelect {
multiselect?: false;
onChange: ({ value }: { value: any }) => void;
}
interface IMultiSelect { interface IMultiSelect {
options?: IItemData[]; options?: IItemData[];
onChange: Function;
placeholder?: IText; placeholder?: IText;
context?: BlockContext; context?: BlockContext;
loading?: boolean; loading?: boolean;
multiselect?: boolean;
onSearch?: (keyword: string) => IItemData[] | Promise<IItemData[] | undefined>; onSearch?: (keyword: string) => IItemData[] | Promise<IItemData[] | undefined>;
onClose?: () => void; onClose?: () => void;
inputStyle?: TextStyle; inputStyle?: TextStyle;
@ -46,9 +54,9 @@ export const MultiSelect = React.memo(
disabled, disabled,
inputStyle, inputStyle,
innerInputStyle innerInputStyle
}: IMultiSelect) => { }: IMultiSelectWithMultiSelect | IMultiSelectWithoutMultiSelect) => {
const { colors } = useTheme(); const { colors } = useTheme();
const [selected, select] = useState<string[]>(Array.isArray(values) ? values : []); const [selected, select] = useState<IItemData[]>(Array.isArray(values) ? values : []);
const [currentValue, setCurrentValue] = useState(''); const [currentValue, setCurrentValue] = useState('');
const { showActionSheet, hideActionSheet } = useActionSheet(); const { showActionSheet, hideActionSheet } = useActionSheet();
@ -57,7 +65,7 @@ export const MultiSelect = React.memo(
if (Array.isArray(values)) { if (Array.isArray(values)) {
select(values); select(values);
} }
}, [values]); }, []);
useEffect(() => { useEffect(() => {
if (values && values.length && !multiselect) { if (values && values.length && !multiselect) {
@ -95,13 +103,13 @@ export const MultiSelect = React.memo(
} = item; } = item;
if (multiselect) { if (multiselect) {
let newSelect = []; let newSelect = [];
if (!selected.includes(value)) { if (!selected.find(s => s.value === value)) {
newSelect = [...selected, value]; newSelect = [...selected, item];
} else { } else {
newSelect = selected.filter((s: any) => s !== value); newSelect = selected.filter((s: any) => s.value !== value);
} }
select(newSelect); select(newSelect);
onChange({ value: newSelect }); onChange({ value: newSelect.map(s => s.value) });
} else { } else {
onChange({ value }); onChange({ value });
setCurrentValue(text); setCurrentValue(text);
@ -119,12 +127,10 @@ export const MultiSelect = React.memo(
); );
if (context === BlockContext.FORM) { if (context === BlockContext.FORM) {
const items: any = options.filter((option: any) => selected.includes(option.value));
button = ( button = (
<Input onPress={onShow} loading={loading} disabled={disabled} inputStyle={inputStyle} innerInputStyle={innerInputStyle}> <Input onPress={onShow} loading={loading} disabled={disabled} inputStyle={inputStyle} innerInputStyle={innerInputStyle}>
{items.length ? ( {selected.length ? (
<Chips items={items} onSelect={(item: any) => (disabled ? {} : onSelect(item))} /> <Chips items={selected} onSelect={(item: any) => (disabled ? {} : onSelect(item))} />
) : ( ) : (
<Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder.text}</Text> <Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder.text}</Text>
)} )}

View File

@ -220,36 +220,37 @@ export const SectionMultiSelect = () =>
}, },
accessory: { accessory: {
type: 'multi_static_select', type: 'multi_static_select',
appId: 'app-id',
blockId: 'block-id',
actionId: 'action-id',
initialValue: ['option_1', 'option_2'],
options: [ options: [
{ {
value: 'option_1',
text: { text: {
type: 'plain_text', type: 'plain_text',
text: 'button' text: 'lorem ipsum 🚀',
}, emoji: true
value: 1 }
}, },
{ {
value: 'option_2',
text: { text: {
type: 'plain_text', type: 'plain_text',
text: 'opt 1' text: 'lorem ipsum 🚀',
}, emoji: true
value: 2 }
},
{
text: {
type: 'plain_text',
text: 'opt 2'
},
value: 3
},
{
text: {
type: 'plain_text',
text: 'opt 3'
},
value: 4
} }
] ],
placeholder: {
type: 'plain_text',
text: 'Select an item'
},
label: {
type: 'plain_text',
text: 'Label',
emoji: true
}
} }
} }
]); ]);

View File

@ -138,7 +138,8 @@ class MessageParser extends UiKitParserMessage<React.ReactElement> {
multiStaticSelect(element: IElement, context: BlockContext) { multiStaticSelect(element: IElement, context: BlockContext) {
const [{ loading, value }, action] = useBlockContext(element, context); const [{ loading, value }, action] = useBlockContext(element, context);
return <MultiSelect {...element} value={value} onChange={action} context={context} loading={loading} multiselect />; const valueFiltered = element.options?.filter(option => value.includes(option.value));
return <MultiSelect {...element} value={valueFiltered} onChange={action} context={context} loading={loading} multiselect />;
} }
staticSelect(element: IElement, context: BlockContext) { staticSelect(element: IElement, context: BlockContext) {

View File

@ -39,7 +39,7 @@ export interface ICreateDiscussionViewSelectChannel {
token: string; token: string;
userId: string; userId: string;
initial: object; initial: object;
onChannelSelect: Function; onChannelSelect: ({ value }: { value: any }) => void;
blockUnauthenticatedAccess: boolean; blockUnauthenticatedAccess: boolean;
serverVersion: string; serverVersion: string;
} }
@ -49,7 +49,7 @@ export interface ICreateDiscussionViewSelectUsers {
token: string; token: string;
userId: string; userId: string;
selected: any[]; selected: any[];
onUserSelect: Function; onUserSelect: ({ value }: { value: string[] }) => void;
blockUnauthenticatedAccess: boolean; blockUnauthenticatedAccess: boolean;
serverVersion: string; serverVersion: string;
} }

View File

@ -90,6 +90,11 @@ const LivechatEditView = ({ user, navigation, route, theme }: ILivechatEditViewP
const [tagParam, setTags] = useState(livechat?.tags || []); const [tagParam, setTags] = useState(livechat?.tags || []);
const [tagParamSelected, setTagParamSelected] = useState(livechat?.tags || []); const [tagParamSelected, setTagParamSelected] = useState(livechat?.tags || []);
const tagOptions = tagParam.map((tag: string) => ({ text: { text: tag }, value: tag }));
const tagValues = Array.isArray(tagParamSelected)
? tagOptions.filter((option: any) => tagParamSelected.includes(option.value))
: [];
useEffect(() => { useEffect(() => {
const arr = [...tagParam, ...availableUserTags]; const arr = [...tagParam, ...availableUserTags];
const uniqueArray = arr.filter((val, i) => arr.indexOf(val) === i); const uniqueArray = arr.filter((val, i) => arr.indexOf(val) === i);
@ -254,12 +259,12 @@ const LivechatEditView = ({ user, navigation, route, theme }: ILivechatEditViewP
<Text style={[styles.label, { color: themes[theme!].titleText }]}>{I18n.t('Tags')}</Text> <Text style={[styles.label, { color: themes[theme!].titleText }]}>{I18n.t('Tags')}</Text>
<MultiSelect <MultiSelect
options={tagParam.map((tag: string) => ({ text: { text: tag }, value: tag }))} options={tagOptions}
onChange={({ value }: { value: string[] }) => { onChange={({ value }: { value: string[] }) => {
setTagParamSelected([...value]); setTagParamSelected([...value]);
}} }}
placeholder={{ text: I18n.t('Tags') }} placeholder={{ text: I18n.t('Tags') }}
value={tagParamSelected} value={tagValues}
context={BlockContext.FORM} context={BlockContext.FORM}
multiselect multiselect
disabled={!editLivechatRoomCustomFieldsPermission} disabled={!editLivechatRoomCustomFieldsPermission}

View File

@ -78,6 +78,11 @@ interface IRoomInfoEditViewProps extends IBaseScreen<ChatsStackParamList | Modal
deleteTeamPermission: string[]; deleteTeamPermission: string[];
} }
const MESSAGE_TYPE_VALUES = MessageTypeValues.map(m => ({
value: m.value,
text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) }
}));
class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfoEditViewState> { class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfoEditViewState> {
randomValue = random(15); randomValue = random(15);
private querySubscription: Subscription | undefined; private querySubscription: Subscription | undefined;
@ -447,15 +452,16 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
return null; return null;
} }
const values = Array.isArray(systemMessages)
? MESSAGE_TYPE_VALUES.filter((option: any) => systemMessages.includes(option.value))
: [];
return ( return (
<MultiSelect <MultiSelect
options={MessageTypeValues.map(m => ({ options={MESSAGE_TYPE_VALUES}
value: m.value, onChange={({ value }) => this.setState({ systemMessages: value })}
text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) }
}))}
onChange={({ value }: { value: boolean }) => this.setState({ systemMessages: value })}
placeholder={{ text: I18n.t('Hide_System_Messages') }} placeholder={{ text: I18n.t('Hide_System_Messages') }}
value={systemMessages as string[]} value={values}
context={BlockContext.FORM} context={BlockContext.FORM}
multiselect multiselect
/> />

View File

@ -31,6 +31,7 @@ describe('Discussion', () => {
}); });
it('should create discussion from NewMessageView', async () => { it('should create discussion from NewMessageView', async () => {
const selectUser = 'rocket.cat';
await waitFor(element(by.id('rooms-list-view-create-channel'))) await waitFor(element(by.id('rooms-list-view-create-channel')))
.toExist() .toExist()
.withTimeout(2000); .withTimeout(2000);
@ -53,6 +54,30 @@ describe('Discussion', () => {
.withTimeout(10000); .withTimeout(10000);
await element(by.id(`multi-select-item-${room}`)).tap(); await element(by.id(`multi-select-item-${room}`)).tap();
await element(by.id('multi-select-discussion-name')).replaceText(discussionFromNewMessage); await element(by.id('multi-select-discussion-name')).replaceText(discussionFromNewMessage);
await element(by[textMatcher]('Select users...')).tap();
await element(by.id('multi-select-search')).replaceText(`${selectUser}`);
await waitFor(element(by.id(`multi-select-item-${selectUser}`)))
.toExist()
.withTimeout(10000);
await element(by.id(`multi-select-item-${selectUser}`)).tap();
await sleep(300);
// checking if the chip was placed properly
await waitFor(element(by.id(`multi-select-chip-${selectUser}`)))
.toExist()
.withTimeout(10000);
// should keep the same chip even when the user does a new research
await element(by.id('multi-select-search')).replaceText(`user`);
await waitFor(element(by.id(`multi-select-item-${selectUser}`)))
.not.toExist()
.withTimeout(10000);
await waitFor(element(by.id(`multi-select-chip-${selectUser}`)))
.toExist()
.withTimeout(10000);
await sleep(500);
await element(by.id('multi-select-search')).tapReturnKey();
await sleep(500);
// removing the rocket.cat from the users
await element(by.id(`multi-select-chip-${selectUser}`)).tap();
await waitFor(element(by.id('create-discussion-submit'))) await waitFor(element(by.id('create-discussion-submit')))
.toExist() .toExist()
.withTimeout(10000); .withTimeout(10000);