[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);
},
getUserInfo,
getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user;

View File

@ -421,9 +421,18 @@ export const getDepartmentInfo = (departmentId: string): any =>
// RC 2.2.0
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
sdk.get('livechat/department');
return sdk.get('livechat/department', params);
};
export const usersAutoComplete = (selector: any) =>
// RC 2.4.0

View File

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

View File

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

View File

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