Chore: Migrate CannedResponsesListView to Typescript (#3553)

* Chore: Migrate CannedResponsesListView to TS

* Moved IcannedResponse to definitions and fixed the index

* Chore: Migrate CannedResponseDetail to TS

* minor tweaks

* refactor: update new types and interfaces for use ISubscription

* fix lint error and canned responses's dropdown

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Reinaldo Neto 2022-02-04 19:15:01 -03:00 committed by GitHub
parent b95cad71eb
commit 6933278fd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 211 additions and 163 deletions

View File

@ -22,7 +22,7 @@ const styles = StyleSheet.create({
interface ISearchHeaderProps {
onSearchChangeText?: (text: string) => void;
testID: string;
testID?: string;
}
const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.Element => {

View File

@ -0,0 +1,31 @@
export interface IDepartment {
_id: string;
enabled: boolean;
name: string;
description: string;
showOnRegistration: boolean;
showOnOfflineForm: boolean;
requestTagBeforeClosingChat: boolean;
email: string;
chatClosingTags: string[];
offlineMessageChannelName: string;
maxNumberSimultaneousChat: number;
abandonedRoomsCloseCustomMessage: string;
waitingQueueMessage: string;
departmentsAllowedToForward: string;
_updatedAt: Date;
numAgents: number;
ancestors: string[];
}
export interface ICannedResponse {
_id: string;
shortcut: string;
text: string;
scope: string;
tags: string[];
createdBy: { _id: string; username: string };
userId: string;
scopeName: string;
departmentId?: string;
}

View File

@ -8,6 +8,7 @@ import { IServer } from '../definitions/IServer';
import { IAttachment } from '../definitions/IAttachment';
import { IMessage } from '../definitions/IMessage';
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription';
import { ICannedResponse } from '../definitions/ICannedResponse';
export type ChatsStackParamList = {
RoomsListView: undefined;
@ -137,12 +138,7 @@ export type ChatsStackParamList = {
rid: string;
};
CannedResponseDetail: {
cannedResponse: {
shortcut: string;
text: string;
scopeName: string;
tags: string[];
};
cannedResponse: ICannedResponse;
room: ISubscription;
};
};

View File

@ -44,6 +44,7 @@ export const goRoom = async ({
isMasterDetail: boolean;
navigationMethod?: any;
jumpToMessageId?: string;
usedCannedResponse?: string;
}): Promise<void> => {
if (item.t === SubscriptionType.DIRECT && item?.search) {
// if user is using the search we need first to join/create room
@ -54,7 +55,7 @@ export const goRoom = async ({
return navigate({
item: {
rid: result.room._id,
name: username,
name: username || '',
t: SubscriptionType.DIRECT
},
isMasterDetail,

View File

@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { StyleSheet, Text, View, ScrollView } from 'react-native';
import { useSelector } from 'react-redux';
@ -13,6 +14,8 @@ import Navigation from '../lib/Navigation';
import { goRoom } from '../utils/goRoom';
import { themes } from '../constants/colors';
import Markdown from '../containers/markdown';
import { ICannedResponse } from '../definitions/ICannedResponse';
import { ChatsStackParamList } from '../stacks/types';
import sharedStyles from './Styles';
const styles = StyleSheet.create({
@ -68,27 +71,34 @@ const styles = StyleSheet.create({
}
});
const Item = ({ label, content, theme, testID }) =>
interface IItem {
label: string;
content?: string;
theme: string;
testID?: string;
}
const Item = ({ label, content, theme, testID }: IItem) =>
content ? (
<View style={styles.item} testID={testID}>
<Text accessibilityLabel={label} style={[styles.itemLabel, { color: themes[theme].titleText }]}>
{label}
</Text>
{/* @ts-ignore */}
<Markdown style={[styles.itemContent, { color: themes[theme].auxiliaryText }]} msg={content} theme={theme} />
</View>
) : null;
Item.propTypes = {
label: PropTypes.string,
content: PropTypes.string,
theme: PropTypes.string,
testID: PropTypes.string
};
const CannedResponseDetail = ({ navigation, route }) => {
interface ICannedResponseDetailProps {
navigation: StackNavigationProp<ChatsStackParamList, 'CannedResponseDetail'>;
route: RouteProp<ChatsStackParamList, 'CannedResponseDetail'>;
}
const CannedResponseDetail = ({ navigation, route }: ICannedResponseDetailProps): JSX.Element => {
const { cannedResponse } = route?.params;
const { theme } = useTheme();
const { isMasterDetail } = useSelector(state => state.app);
const { rooms } = useSelector(state => state.room);
const { isMasterDetail } = useSelector((state: any) => state.app);
const { rooms } = useSelector((state: any) => state.room);
useEffect(() => {
navigation.setOptions({
@ -96,15 +106,14 @@ const CannedResponseDetail = ({ navigation, route }) => {
});
}, []);
const navigateToRoom = item => {
const navigateToRoom = (item: ICannedResponse) => {
const { room } = route.params;
const { name, username } = room;
const { name } = room;
const params = {
rid: room.rid,
name: RocketChat.getRoomTitle({
t: room.t,
fname: name,
name: username
fname: name
}),
t: room.t,
roomUserId: RocketChat.getUidDirectMessage(room),
@ -115,7 +124,7 @@ const CannedResponseDetail = ({ navigation, route }) => {
// if it's on master detail layout, we close the modal and replace RoomView
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
goRoom({ item: params, isMasterDetail, usedCannedResponse: item.text });
goRoom({ item: params, isMasterDetail });
} else {
let navigate = navigation.push;
// if this is a room focused
@ -163,9 +172,4 @@ const CannedResponseDetail = ({ navigation, route }) => {
);
};
CannedResponseDetail.propTypes = {
navigation: PropTypes.object,
route: PropTypes.object
};
export default CannedResponseDetail;

View File

@ -1,14 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { themes } from '../../constants/colors';
import Button from '../../containers/Button';
import I18n from '../../i18n';
import styles from './styles';
const CannedResponseItem = ({ theme, onPressDetail, shortcut, scope, onPressUse, text, tags }) => (
interface ICannedResponseItem {
theme: string;
onPressDetail: () => void;
shortcut: string;
scope: string;
onPressUse: () => void;
text: string;
tags: string[];
}
const CannedResponseItem = ({
theme,
onPressDetail = () => {},
shortcut,
scope,
onPressUse = () => {},
text,
tags
}: ICannedResponseItem): JSX.Element => (
<Touchable onPress={onPressDetail} style={[styles.wrapCannedItem, { backgroundColor: themes[theme].messageboxBackground }]}>
<>
<View style={styles.cannedRow}>
@ -43,19 +60,4 @@ const CannedResponseItem = ({ theme, onPressDetail, shortcut, scope, onPressUse,
</Touchable>
);
CannedResponseItem.propTypes = {
theme: PropTypes.string,
onPressDetail: PropTypes.func,
shortcut: PropTypes.string,
scope: PropTypes.string,
onPressUse: PropTypes.func,
text: PropTypes.string,
tags: PropTypes.array
};
CannedResponseItem.defaultProps = {
onPressDetail: () => {},
onPressUse: () => {}
};
export default CannedResponseItem;

View File

@ -1,44 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons';
import sharedStyles from '../../Styles';
export const ROW_HEIGHT = 44;
const styles = StyleSheet.create({
container: {
paddingVertical: 11,
height: ROW_HEIGHT,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center'
},
text: {
flex: 1,
fontSize: 16,
...sharedStyles.textRegular
}
});
const DropdownItem = React.memo(({ theme, onPress, iconName, text }) => (
<Touch theme={theme} onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }}>
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{text}</Text>
{iconName ? <CustomIcon name={iconName} size={22} color={themes[theme].auxiliaryText} /> : null}
</View>
</Touch>
));
DropdownItem.propTypes = {
text: PropTypes.string,
iconName: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func
};
export default withTheme(DropdownItem);

View File

@ -0,0 +1,46 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons';
import sharedStyles from '../../Styles';
export const ROW_HEIGHT = 44;
const styles = StyleSheet.create({
container: {
paddingVertical: 11,
height: ROW_HEIGHT,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center'
},
text: {
flex: 1,
fontSize: 16,
...sharedStyles.textRegular
}
});
interface IDropdownItem {
text: string;
iconName: string | null;
onPress: () => void;
}
const DropdownItem = React.memo(({ onPress, iconName, text }: IDropdownItem) => {
const { theme } = useTheme();
return (
<Touch theme={theme} onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }}>
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{text}</Text>
{iconName ? <CustomIcon name={iconName} size={22} color={themes[theme].auxiliaryText} /> : null}
</View>
</Touch>
);
});
export default DropdownItem;

View File

@ -1,9 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IDepartment } from '../../../definitions/ICannedResponse';
import DropdownItem from './DropdownItem';
const DropdownItemFilter = ({ currentDepartment, value, onPress }) => (
interface IDropdownItemFilter {
currentDepartment: IDepartment;
value: IDepartment;
onPress: (value: IDepartment) => void;
}
const DropdownItemFilter = ({ currentDepartment, value, onPress }: IDropdownItemFilter): JSX.Element => (
<DropdownItem
text={value?.name}
iconName={currentDepartment?._id === value?._id ? 'check' : null}
@ -11,10 +17,4 @@ const DropdownItemFilter = ({ currentDepartment, value, onPress }) => (
/>
);
DropdownItemFilter.propTypes = {
currentDepartment: PropTypes.object,
value: PropTypes.string,
onPress: PropTypes.func
};
export default DropdownItemFilter;

View File

@ -1,15 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import DropdownItem from './DropdownItem';
const DropdownItemHeader = ({ department, onPress }) => (
<DropdownItem text={department?.name} iconName='filter' onPress={onPress} />
);
DropdownItemHeader.propTypes = {
department: PropTypes.object,
onPress: PropTypes.func
};
export default DropdownItemHeader;

View File

@ -0,0 +1,15 @@
import React from 'react';
import { IDepartment } from '../../../definitions/ICannedResponse';
import DropdownItem from './DropdownItem';
interface IDropdownItemHeader {
department: IDepartment;
onPress: () => void;
}
const DropdownItemHeader = ({ department, onPress }: IDropdownItemHeader): JSX.Element => (
<DropdownItem text={department?.name} iconName='filter' onPress={onPress} />
);
export default DropdownItemHeader;

View File

@ -1,31 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Animated, Easing, FlatList, TouchableWithoutFeedback } from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
import { headerHeight } from '../../../containers/Header';
import * as List from '../../../containers/List';
import { IDepartment } from '../../../definitions/ICannedResponse';
import DropdownItemFilter from './DropdownItemFilter';
import DropdownItemHeader from './DropdownItemHeader';
import { ROW_HEIGHT } from './DropdownItem';
const ANIMATION_DURATION = 200;
class Dropdown extends React.Component {
static propTypes = {
isMasterDetail: PropTypes.bool,
theme: PropTypes.string,
insets: PropTypes.object,
currentDepartment: PropTypes.object,
onClose: PropTypes.func,
onDepartmentSelected: PropTypes.func,
departments: PropTypes.array
};
interface IDropdownProps {
theme?: string;
currentDepartment: IDepartment;
onClose: () => void;
onDepartmentSelected: (value: IDepartment) => void;
departments: IDepartment[];
}
constructor(props) {
class Dropdown extends React.Component<IDropdownProps> {
private animatedValue: Animated.Value;
constructor(props: IDropdownProps) {
super(props);
this.animatedValue = new Animated.Value(0);
}
@ -50,16 +49,15 @@ class Dropdown extends React.Component {
};
render() {
const { isMasterDetail, insets, theme, currentDepartment, onDepartmentSelected, departments } = this.props;
const statusBarHeight = insets?.top ?? 0;
const heightDestination = isMasterDetail ? headerHeight + statusBarHeight : 0;
const { theme, currentDepartment, onDepartmentSelected, departments } = this.props;
const heightDestination = 0;
const translateY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-300, heightDestination] // approximated height of the component when closed/open
});
const backdropOpacity = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity]
outputRange: [0, themes[theme!].backdropOpacity]
});
const maxRows = 5;
@ -70,7 +68,7 @@ class Dropdown extends React.Component {
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
backgroundColor: themes[theme!].backdropColor,
opacity: backdropOpacity,
top: heightDestination
}
@ -82,8 +80,8 @@ class Dropdown extends React.Component {
styles.dropdownContainer,
{
transform: [{ translateY }],
backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor
backgroundColor: themes[theme!].backgroundColor,
borderColor: themes[theme!].separatorColor
}
]}>
<DropdownItemHeader department={currentDepartment} onPress={this.close} />

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBackButton } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import database from '../../lib/database';
import I18n from '../../i18n';
@ -26,6 +26,9 @@ import CannedResponseItem from './CannedResponseItem';
import Dropdown from './Dropdown';
import DropdownItemHeader from './Dropdown/DropdownItemHeader';
import styles from './styles';
import { ICannedResponse, IDepartment } from '../../definitions/ICannedResponse';
import { ChatsStackParamList } from '../../stacks/types';
import { ISubscription } from '../../definitions/ISubscription';
const COUNT = 25;
@ -42,14 +45,19 @@ const fixedScopes = [
_id: 'user',
name: I18n.t('Private')
}
];
] as IDepartment[];
const CannedResponsesListView = ({ navigation, route }) => {
const [room, setRoom] = useState(null);
interface ICannedResponsesListViewProps {
navigation: StackNavigationProp<ChatsStackParamList, 'CannedResponsesListView'>;
route: RouteProp<ChatsStackParamList, 'CannedResponsesListView'>;
}
const [cannedResponses, setCannedResponses] = useState([]);
const [cannedResponsesScopeName, setCannedResponsesScopeName] = useState([]);
const [departments, setDepartments] = useState([]);
const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListViewProps): JSX.Element => {
const [room, setRoom] = useState<ISubscription | null>(null);
const [cannedResponses, setCannedResponses] = useState<ICannedResponse[]>([]);
const [cannedResponsesScopeName, setCannedResponsesScopeName] = useState<ICannedResponse[]>([]);
const [departments, setDepartments] = useState<IDepartment[]>([]);
// states used by the filter in Header and Dropdown
const [isSearching, setIsSearching] = useState(false);
@ -65,8 +73,8 @@ const CannedResponsesListView = ({ navigation, route }) => {
const insets = useSafeAreaInsets();
const { theme } = useTheme();
const { isMasterDetail } = useSelector(state => state.app);
const { rooms } = useSelector(state => state.room);
const { isMasterDetail } = useSelector((state: any) => state.app);
const { rooms } = useSelector((state: any) => state.room);
const getRoomFromDb = async () => {
const { rid } = route.params;
@ -93,21 +101,22 @@ const CannedResponsesListView = ({ navigation, route }) => {
}
}, 300);
const goToDetail = item => {
navigation.navigate('CannedResponseDetail', { cannedResponse: item, room });
const goToDetail = (item: ICannedResponse) => {
if (room) {
navigation.navigate('CannedResponseDetail', { cannedResponse: item, room });
}
};
const navigateToRoom = item => {
const navigateToRoom = (item: ICannedResponse) => {
if (!room) {
return;
}
const { name, username } = room;
const { name } = room;
const params = {
rid: room.rid,
name: RocketChat.getRoomTitle({
t: room.t,
fname: name,
name: username
fname: name
}),
t: room.t,
roomUserId: RocketChat.getUidDirectMessage(room),
@ -118,7 +127,7 @@ const CannedResponsesListView = ({ navigation, route }) => {
// if it's on master detail layout, we close the modal and replace RoomView
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
goRoom({ item: params, isMasterDetail, usedCannedResponse: item.text });
goRoom({ item: params, isMasterDetail });
} else {
let navigate = navigation.push;
// if this is a room focused
@ -130,7 +139,17 @@ const CannedResponsesListView = ({ navigation, route }) => {
}
};
const getListCannedResponse = async ({ text, department, depId, debounced }) => {
const getListCannedResponse = async ({
text,
department,
depId,
debounced
}: {
text: string;
department: string;
depId: string;
debounced: boolean;
}) => {
try {
const res = await RocketChat.getListCannedResponse({
text,
@ -188,13 +207,13 @@ const CannedResponsesListView = ({ navigation, route }) => {
setOffset(0);
};
const onChangeText = text => {
const onChangeText = (text: string) => {
newSearch();
setSearchText(text);
searchCallback(text, scope, departmentId);
};
const onDepartmentSelect = value => {
const onDepartmentSelect = (value: IDepartment) => {
let department = '';
let depId = '';
@ -225,7 +244,7 @@ const CannedResponsesListView = ({ navigation, route }) => {
await getListCannedResponse({ text: searchText, department: scope, depId: departmentId, debounced: false });
};
const getHeader = () => {
const getHeader = (): StackNavigationOptions => {
if (isSearching) {
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 });
return {
@ -235,7 +254,7 @@ const CannedResponsesListView = ({ navigation, route }) => {
<HeaderButton.Item
iconName='close'
onPress={() => {
onChangeText();
onChangeText('');
setIsSearching(false);
}}
/>
@ -250,7 +269,7 @@ const CannedResponsesListView = ({ navigation, route }) => {
};
}
const options = {
const options: StackNavigationOptions = {
headerLeft: () => (
<HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />
),
@ -355,9 +374,4 @@ const CannedResponsesListView = ({ navigation, route }) => {
);
};
CannedResponsesListView.propTypes = {
navigation: PropTypes.object,
route: PropTypes.object
};
export default CannedResponsesListView;

View File

@ -13,7 +13,7 @@ export default StyleSheet.create({
borderBottomWidth: StyleSheet.hairlineWidth
},
backdrop: {
...StyleSheet.absoluteFill
...StyleSheet.absoluteFillObject
},
wrapCannedItem: {
minHeight: 117,