[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:
parent
a58b27e4f1
commit
9ec598407d
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue