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:
parent
a203f67a4a
commit
77a81d577e
|
@ -31,6 +31,7 @@ const Chip = ({ item, onSelect, style }: IChip) => {
|
|||
onPress={() => onSelect(item)}
|
||||
style={[styles.chip, { backgroundColor: colors.auxiliaryBackground }, style]}
|
||||
background={Touchable.Ripple(colors.bannerBackground)}
|
||||
testID={`multi-select-chip-${item.value}`}
|
||||
>
|
||||
<>
|
||||
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
|
||||
|
|
|
@ -13,13 +13,13 @@ import { CustomIcon } from '../../CustomIcon';
|
|||
|
||||
interface IItem {
|
||||
item: IItemData;
|
||||
selected?: string;
|
||||
selected: boolean;
|
||||
onSelect: Function;
|
||||
}
|
||||
|
||||
interface IItems {
|
||||
items: IItemData[];
|
||||
selected: string[];
|
||||
selected: IItemData[];
|
||||
onSelect: Function;
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ const Items = ({ items, selected, onSelect }: IItems) => (
|
|||
keyboardShouldPersistTaps='always'
|
||||
ItemSeparatorComponent={List.Separator}
|
||||
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)} />}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -17,16 +17,16 @@ interface IMultiSelectContentProps {
|
|||
options?: IItemData[];
|
||||
multiselect: boolean;
|
||||
select: React.Dispatch<any>;
|
||||
onChange: Function;
|
||||
onChange: ({ value }: { value: string[] }) => void;
|
||||
setCurrentValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
onHide: Function;
|
||||
selectedItems: string[];
|
||||
selectedItems: IItemData[];
|
||||
}
|
||||
|
||||
export const MultiSelectContent = React.memo(
|
||||
({ onSearch, options, multiselect, select, onChange, setCurrentValue, onHide, selectedItems }: IMultiSelectContentProps) => {
|
||||
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 { hideActionSheet } = useActionSheet();
|
||||
|
||||
|
@ -37,14 +37,14 @@ export const MultiSelectContent = React.memo(
|
|||
} = item;
|
||||
if (multiselect) {
|
||||
let newSelect = [];
|
||||
if (!selected.includes(value)) {
|
||||
newSelect = [...selected, value];
|
||||
if (!selected.find(s => s.value === value)) {
|
||||
newSelect = [...selected, item];
|
||||
} else {
|
||||
newSelect = selected.filter((s: any) => s !== value);
|
||||
newSelect = selected.filter((s: any) => s.value !== value);
|
||||
}
|
||||
setSelected(newSelect);
|
||||
select(newSelect);
|
||||
onChange({ value: newSelect });
|
||||
onChange({ value: newSelect.map(s => s.value) });
|
||||
} else {
|
||||
onChange({ value });
|
||||
setCurrentValue(text);
|
||||
|
|
|
@ -17,13 +17,21 @@ export interface IItemData {
|
|||
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 {
|
||||
options?: IItemData[];
|
||||
onChange: Function;
|
||||
placeholder?: IText;
|
||||
context?: BlockContext;
|
||||
loading?: boolean;
|
||||
multiselect?: boolean;
|
||||
onSearch?: (keyword: string) => IItemData[] | Promise<IItemData[] | undefined>;
|
||||
onClose?: () => void;
|
||||
inputStyle?: TextStyle;
|
||||
|
@ -46,9 +54,9 @@ export const MultiSelect = React.memo(
|
|||
disabled,
|
||||
inputStyle,
|
||||
innerInputStyle
|
||||
}: IMultiSelect) => {
|
||||
}: IMultiSelectWithMultiSelect | IMultiSelectWithoutMultiSelect) => {
|
||||
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 { showActionSheet, hideActionSheet } = useActionSheet();
|
||||
|
@ -57,7 +65,7 @@ export const MultiSelect = React.memo(
|
|||
if (Array.isArray(values)) {
|
||||
select(values);
|
||||
}
|
||||
}, [values]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (values && values.length && !multiselect) {
|
||||
|
@ -95,13 +103,13 @@ export const MultiSelect = React.memo(
|
|||
} = item;
|
||||
if (multiselect) {
|
||||
let newSelect = [];
|
||||
if (!selected.includes(value)) {
|
||||
newSelect = [...selected, value];
|
||||
if (!selected.find(s => s.value === value)) {
|
||||
newSelect = [...selected, item];
|
||||
} else {
|
||||
newSelect = selected.filter((s: any) => s !== value);
|
||||
newSelect = selected.filter((s: any) => s.value !== value);
|
||||
}
|
||||
select(newSelect);
|
||||
onChange({ value: newSelect });
|
||||
onChange({ value: newSelect.map(s => s.value) });
|
||||
} else {
|
||||
onChange({ value });
|
||||
setCurrentValue(text);
|
||||
|
@ -119,12 +127,10 @@ export const MultiSelect = React.memo(
|
|||
);
|
||||
|
||||
if (context === BlockContext.FORM) {
|
||||
const items: any = options.filter((option: any) => selected.includes(option.value));
|
||||
|
||||
button = (
|
||||
<Input onPress={onShow} loading={loading} disabled={disabled} inputStyle={inputStyle} innerInputStyle={innerInputStyle}>
|
||||
{items.length ? (
|
||||
<Chips items={items} onSelect={(item: any) => (disabled ? {} : onSelect(item))} />
|
||||
{selected.length ? (
|
||||
<Chips items={selected} onSelect={(item: any) => (disabled ? {} : onSelect(item))} />
|
||||
) : (
|
||||
<Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder.text}</Text>
|
||||
)}
|
||||
|
|
|
@ -220,36 +220,37 @@ export const SectionMultiSelect = () =>
|
|||
},
|
||||
accessory: {
|
||||
type: 'multi_static_select',
|
||||
appId: 'app-id',
|
||||
blockId: 'block-id',
|
||||
actionId: 'action-id',
|
||||
initialValue: ['option_1', 'option_2'],
|
||||
options: [
|
||||
{
|
||||
value: 'option_1',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'button'
|
||||
},
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'opt 1'
|
||||
},
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'opt 2'
|
||||
},
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'opt 3'
|
||||
},
|
||||
value: 4
|
||||
text: 'lorem ipsum 🚀',
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 'option_2',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'lorem ipsum 🚀',
|
||||
emoji: true
|
||||
}
|
||||
}
|
||||
],
|
||||
placeholder: {
|
||||
type: 'plain_text',
|
||||
text: 'Select an item'
|
||||
},
|
||||
label: {
|
||||
type: 'plain_text',
|
||||
text: 'Label',
|
||||
emoji: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -138,7 +138,8 @@ class MessageParser extends UiKitParserMessage<React.ReactElement> {
|
|||
|
||||
multiStaticSelect(element: IElement, context: BlockContext) {
|
||||
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) {
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface ICreateDiscussionViewSelectChannel {
|
|||
token: string;
|
||||
userId: string;
|
||||
initial: object;
|
||||
onChannelSelect: Function;
|
||||
onChannelSelect: ({ value }: { value: any }) => void;
|
||||
blockUnauthenticatedAccess: boolean;
|
||||
serverVersion: string;
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export interface ICreateDiscussionViewSelectUsers {
|
|||
token: string;
|
||||
userId: string;
|
||||
selected: any[];
|
||||
onUserSelect: Function;
|
||||
onUserSelect: ({ value }: { value: string[] }) => void;
|
||||
blockUnauthenticatedAccess: boolean;
|
||||
serverVersion: string;
|
||||
}
|
||||
|
|
|
@ -90,6 +90,11 @@ const LivechatEditView = ({ user, navigation, route, theme }: ILivechatEditViewP
|
|||
const [tagParam, setTags] = 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(() => {
|
||||
const arr = [...tagParam, ...availableUserTags];
|
||||
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>
|
||||
<MultiSelect
|
||||
options={tagParam.map((tag: string) => ({ text: { text: tag }, value: tag }))}
|
||||
options={tagOptions}
|
||||
onChange={({ value }: { value: string[] }) => {
|
||||
setTagParamSelected([...value]);
|
||||
}}
|
||||
placeholder={{ text: I18n.t('Tags') }}
|
||||
value={tagParamSelected}
|
||||
value={tagValues}
|
||||
context={BlockContext.FORM}
|
||||
multiselect
|
||||
disabled={!editLivechatRoomCustomFieldsPermission}
|
||||
|
|
|
@ -78,6 +78,11 @@ interface IRoomInfoEditViewProps extends IBaseScreen<ChatsStackParamList | Modal
|
|||
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> {
|
||||
randomValue = random(15);
|
||||
private querySubscription: Subscription | undefined;
|
||||
|
@ -447,15 +452,16 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
return null;
|
||||
}
|
||||
|
||||
const values = Array.isArray(systemMessages)
|
||||
? MESSAGE_TYPE_VALUES.filter((option: any) => systemMessages.includes(option.value))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
options={MessageTypeValues.map(m => ({
|
||||
value: m.value,
|
||||
text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) }
|
||||
}))}
|
||||
onChange={({ value }: { value: boolean }) => this.setState({ systemMessages: value })}
|
||||
options={MESSAGE_TYPE_VALUES}
|
||||
onChange={({ value }) => this.setState({ systemMessages: value })}
|
||||
placeholder={{ text: I18n.t('Hide_System_Messages') }}
|
||||
value={systemMessages as string[]}
|
||||
value={values}
|
||||
context={BlockContext.FORM}
|
||||
multiselect
|
||||
/>
|
||||
|
|
|
@ -31,6 +31,7 @@ describe('Discussion', () => {
|
|||
});
|
||||
|
||||
it('should create discussion from NewMessageView', async () => {
|
||||
const selectUser = 'rocket.cat';
|
||||
await waitFor(element(by.id('rooms-list-view-create-channel')))
|
||||
.toExist()
|
||||
.withTimeout(2000);
|
||||
|
@ -53,6 +54,30 @@ describe('Discussion', () => {
|
|||
.withTimeout(10000);
|
||||
await element(by.id(`multi-select-item-${room}`)).tap();
|
||||
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')))
|
||||
.toExist()
|
||||
.withTimeout(10000);
|
||||
|
|
Loading…
Reference in New Issue