[FIX] Add search and fix pagination for omnichannels departments (#3621)

* [FIX] Search and pagination for omnichannels departments

* pagination complete

* minor tweak

* renamed a param and workaround for a ux bug

* fix style of flatlist and search as header scrollable

* stick the header

* Merge branch 'fix.forward-department-list' of https://github.com/RocketChat/Rocket.Chat.ReactNative into fix.forward-department-list

* refactor pagination

* fix value type

* refactor render search

* refactor layout

* make ts happy
This commit is contained in:
Reinaldo Neto 2022-02-17 11:57:30 -03:00 committed by GitHub
parent a58b27e4f1
commit 9ec598407d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 61 deletions

View File

@ -821,7 +821,6 @@ const RocketChat = {
return sdk.methodCallWrapper(method, ...params); return sdk.methodCallWrapper(method, ...params);
}, },
getUserInfo, getUserInfo,
getUidDirectMessage(room) { getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user; const { id: userId } = reduxStore.getState().login.user;

View File

@ -421,9 +421,18 @@ export const getDepartmentInfo = (departmentId: string): any =>
// RC 2.2.0 // RC 2.2.0
sdk.get(`livechat/department/${departmentId}?includeAgents=false`); sdk.get(`livechat/department/${departmentId}?includeAgents=false`);
export const getDepartments = () => export const getDepartments = (args?: { count: number; offset: number; text: string }) => {
let params;
if (args) {
params = {
text: args.text,
count: args.count,
offset: args.offset
};
}
// RC 2.2.0 // RC 2.2.0
sdk.get('livechat/department'); return sdk.get('livechat/department', params);
};
export const usersAutoComplete = (selector: any) => export const usersAutoComplete = (selector: any) =>
// RC 2.4.0 // RC 2.4.0

View File

@ -102,8 +102,10 @@ export type ChatsStackParamList = {
PickerView: { PickerView: {
title: string; title: string;
data: IOptionsField[]; data: IOptionsField[];
value?: any; // TODO: Change value?: string;
onChangeText?: ((text: string) => IOptionsField[]) | ((term?: string) => Promise<any>); onSearch?: (text?: string) => Promise<any>;
onEndReached?: (text: string, offset?: number) => Promise<any>;
total?: number;
goBack?: boolean; goBack?: boolean;
onChangeValue: Function; onChangeValue: Function;
}; };

View File

@ -13,8 +13,9 @@ import RocketChat from '../lib/rocketchat';
import OrSeparator from '../containers/OrSeparator'; import OrSeparator from '../containers/OrSeparator';
import Input from '../containers/UIKit/MultiSelect/Input'; import Input from '../containers/UIKit/MultiSelect/Input';
import { forwardRoom as forwardRoomAction } from '../actions/room'; import { forwardRoom as forwardRoomAction } from '../actions/room';
import { ILivechatDepartment } from './definition/ILivechatDepartment'; import { IRoom } from '../definitions';
import { ChatsStackParamList } from '../stacks/types'; import { ChatsStackParamList } from '../stacks/types';
import { IOptionsField } from './NotificationPreferencesView/options';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -23,14 +24,6 @@ const styles = StyleSheet.create({
} }
}); });
// TODO: Refactor when migrate room
interface IRoom {
departmentId?: any;
servedBy?: {
_id: string;
};
}
interface ITransferData { interface ITransferData {
roomId: string; roomId: string;
userId?: string; userId?: string;
@ -41,12 +34,6 @@ interface IUser {
username: string; username: string;
_id: string; _id: string;
} }
interface IParsedData {
label: string;
value: string;
}
interface IForwardLivechatViewProps { interface IForwardLivechatViewProps {
navigation: StackNavigationProp<ChatsStackParamList, 'ForwardLivechatView'>; navigation: StackNavigationProp<ChatsStackParamList, 'ForwardLivechatView'>;
route: RouteProp<ChatsStackParamList, 'ForwardLivechatView'>; route: RouteProp<ChatsStackParamList, 'ForwardLivechatView'>;
@ -54,22 +41,31 @@ interface IForwardLivechatViewProps {
forwardRoom: (rid: string, transferData: ITransferData) => void; forwardRoom: (rid: string, transferData: ITransferData) => void;
} }
const COUNT_DEPARTMENT = 50;
const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }: IForwardLivechatViewProps) => { const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }: IForwardLivechatViewProps) => {
const [departments, setDepartments] = useState<IParsedData[]>([]); const [departments, setDepartments] = useState<IOptionsField[]>([]);
const [departmentId, setDepartment] = useState(''); const [departmentId, setDepartment] = useState('');
const [users, setUsers] = useState<IParsedData[]>([]); const [departmentTotal, setDepartmentTotal] = useState(0);
const [users, setUsers] = useState<IOptionsField[]>([]);
const [userId, setUser] = useState(); const [userId, setUser] = useState();
const [room, setRoom] = useState<IRoom>({}); const [room, setRoom] = useState<IRoom>({} as IRoom);
const rid = route.params?.rid; const rid = route.params?.rid;
const getDepartments = async () => { const getDepartments = async (text = '', offset = 0) => {
try { try {
const result: any = await RocketChat.getDepartments(); const result = await RocketChat.getDepartments({ count: COUNT_DEPARTMENT, text, offset });
if (result.success) { if (result.success) {
setDepartments( const parsedDepartments: IOptionsField[] = result.departments.map(department => ({
result.departments.map((department: ILivechatDepartment) => ({ label: department.name, value: department._id })) label: department.name,
); value: department._id
}));
if (!text && !offset) {
setDepartments(parsedDepartments);
setDepartmentTotal(result?.total);
}
return { data: parsedDepartments, total: result?.total, offset: result?.offset };
} }
} catch { } catch {
// do nothing // do nothing
@ -80,26 +76,27 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }: IForward
try { try {
const { servedBy: { _id: agentId } = {} } = room; const { servedBy: { _id: agentId } = {} } = room;
const _id = agentId && { $ne: agentId }; const _id = agentId && { $ne: agentId };
const result: any = await RocketChat.usersAutoComplete({ const result = await RocketChat.usersAutoComplete({
conditions: { _id, status: { $ne: 'offline' }, statusLivechat: 'available' }, conditions: { _id, status: { $ne: 'offline' }, statusLivechat: 'available' },
term term
}); });
if (result.success) { if (result.success) {
const parsedUsers = result.items.map((user: IUser) => ({ label: user.username, value: user._id })); const parsedUsers = result.items.map((user: IUser) => ({ label: user.username, value: user._id }));
setUsers(parsedUsers); if (!term) {
return parsedUsers; setUsers(parsedUsers);
}
return { data: parsedUsers };
} }
} catch { } catch {
// do nothing // do nothing
} }
return [];
}; };
const getRoom = async () => { const getRoom = async () => {
try { try {
const result = await RocketChat.getRoomInfo(rid); const result = await RocketChat.getRoomInfo(rid);
if (result.success) { if (result.success) {
setRoom(result.room); setRoom(result.room as IRoom);
} }
} catch { } catch {
// do nothing // do nothing
@ -148,6 +145,9 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }: IForward
value: room?.departmentId, value: room?.departmentId,
data: departments, data: departments,
onChangeValue: setDepartment, onChangeValue: setDepartment,
onSearch: getDepartments,
onEndReached: getDepartments,
total: departmentTotal,
goBack: false goBack: false
}); });
}; };
@ -157,7 +157,7 @@ const ForwardLivechatView = ({ forwardRoom, navigation, route, theme }: IForward
title: I18n.t('Forward_to_user'), title: I18n.t('Forward_to_user'),
data: users, data: users,
onChangeValue: setUser, onChangeValue: setUser,
onChangeText: getUsers, onSearch: getUsers,
goBack: false goBack: false
}); });
}; };

View File

@ -24,6 +24,9 @@ const styles = StyleSheet.create({
paddingVertical: 56, paddingVertical: 56,
...sharedStyles.textSemibold, ...sharedStyles.textSemibold,
...sharedStyles.textAlignCenter ...sharedStyles.textAlignCenter
},
listNoHeader: {
marginTop: 32
} }
}); });
@ -34,9 +37,16 @@ interface IItem {
theme: string; theme: string;
} }
interface IRenderSearch {
hasSearch: boolean;
onChangeText: (text: string) => void;
}
interface IPickerViewState { interface IPickerViewState {
data: IOptionsField[]; data: IOptionsField[];
value: string; value: string;
total: number;
searchText: string;
} }
interface IPickerViewProps { interface IPickerViewProps {
@ -54,8 +64,22 @@ const Item = React.memo(({ item, selected, onItemPress, theme }: IItem) => (
/> />
)); ));
const RenderSearch = ({ hasSearch, onChangeText }: IRenderSearch) => {
if (!hasSearch) {
return <List.Separator style={styles.listNoHeader} />;
}
return (
<>
<View style={styles.search}>
<SearchBox onChangeText={onChangeText} />
</View>
<List.Separator />
</>
);
};
class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState> { class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState> {
private onSearch?: ((text: string) => IOptionsField[]) | ((term?: string | undefined) => Promise<any>); private onSearch?: (text?: string) => Promise<any>;
static navigationOptions = ({ route }: IPickerViewProps) => ({ static navigationOptions = ({ route }: IPickerViewProps) => ({
title: route.params?.title ?? I18n.t('Select_an_option') title: route.params?.title ?? I18n.t('Select_an_option')
@ -64,10 +88,10 @@ class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState>
constructor(props: IPickerViewProps) { constructor(props: IPickerViewProps) {
super(props); super(props);
const data = props.route.params?.data ?? []; const data = props.route.params?.data ?? [];
const value = props.route.params?.value; const value = props.route.params?.value ?? '';
this.state = { data, value }; const total = props.route.params?.total ?? 0;
this.state = { data, value, total, searchText: '' };
this.onSearch = props.route.params?.onChangeText; this.onSearch = props.route.params?.onSearch;
} }
onChangeValue = (value: string) => { onChangeValue = (value: string) => {
@ -80,28 +104,26 @@ class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState>
} }
}; };
onChangeText = debounce( onChangeText = debounce(async (searchText: string) => {
async (text: string) => { if (this.onSearch) {
if (this.onSearch) { const data = await this.onSearch(searchText);
const data = await this.onSearch(text); if (data?.data) {
this.setState({ data }); this.setState({ ...data, searchText });
} }
},
300,
true
);
renderSearch() {
if (!this.onSearch) {
return null;
} }
}, 500);
return ( onEndReached = async () => {
<View style={styles.search}> const { route } = this.props;
<SearchBox onChangeText={this.onChangeText} /> const { data, total, searchText } = this.state;
</View> const onEndReached = route.params?.onEndReached;
); if (onEndReached && data.length < total) {
} const val = await onEndReached(searchText, data.length);
if (val?.data) {
this.setState({ ...val, data: [...data, ...val.data] });
}
}
};
render() { render() {
const { data, value } = this.state; const { data, value } = this.state;
@ -109,7 +131,6 @@ class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState>
return ( return (
<SafeAreaView> <SafeAreaView>
{this.renderSearch()}
<FlatList <FlatList
data={data} data={data}
keyExtractor={item => item.value as string} keyExtractor={item => item.value as string}
@ -121,13 +142,14 @@ class PickerView extends React.PureComponent<IPickerViewProps, IPickerViewState>
onItemPress={() => this.onChangeValue(item.value as string)} onItemPress={() => this.onChangeValue(item.value as string)}
/> />
)} )}
onEndReached={() => this.onEndReached()}
onEndReachedThreshold={0.5}
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator} ListHeaderComponent={<RenderSearch hasSearch={!!this.onSearch} onChangeText={this.onChangeText} />}
ListFooterComponent={List.Separator} ListFooterComponent={List.Separator}
ListEmptyComponent={() => ( ListEmptyComponent={() => (
<Text style={[styles.noResult, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text> <Text style={[styles.noResult, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text>
)} )}
contentContainerStyle={[List.styles.contentContainerStyleFlatList]}
/> />
</SafeAreaView> </SafeAreaView>
); );