[NEW] Show server history (#2421)
* Add dropdown Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com> * Adding new table to serverSchema Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com> * Saving if not exists Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com> * list of visited servers finished Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com> * Fix lint Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com> * Rename ServerLinks to ServersHistory * Refactor * Save username * Sort servers desc * ServerInput * Item * Refactor * Layout tweaks * Layout * query by text * Small refactor * Redirecting to login * Save username for oauth * Fix keyboard persist * Add tests * Unnecessary yield * Stop rendering FlatList logic when there's no servers on history * Dismiss keyboard and autocomplete when tapped outside server TextInput Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
334140df0d
commit
d37678b354
|
@ -23,11 +23,13 @@ export function selectServerFailure() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serverRequest(server, certificate = null) {
|
export function serverRequest(server, certificate = null, username = null, fromServerHistory = false) {
|
||||||
return {
|
return {
|
||||||
type: SERVER.REQUEST,
|
type: SERVER.REQUEST,
|
||||||
server,
|
server,
|
||||||
certificate
|
certificate,
|
||||||
|
username,
|
||||||
|
fromServerHistory
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,21 @@ export const FormContainerInner = ({ children }) => (
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
const FormContainer = ({ children, theme, testID }) => (
|
const FormContainer = ({
|
||||||
|
children, theme, testID, ...props
|
||||||
|
}) => (
|
||||||
<KeyboardView
|
<KeyboardView
|
||||||
style={{ backgroundColor: themes[theme].backgroundColor }}
|
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
contentContainerStyle={sharedStyles.container}
|
contentContainerStyle={sharedStyles.container}
|
||||||
keyboardVerticalOffset={128}
|
keyboardVerticalOffset={128}
|
||||||
>
|
>
|
||||||
<StatusBar theme={theme} />
|
<StatusBar theme={theme} />
|
||||||
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
|
<ScrollView
|
||||||
|
style={sharedStyles.container}
|
||||||
|
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
|
||||||
|
{...scrollPersistTaps}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<SafeAreaView testID={testID} theme={theme} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
<SafeAreaView testID={testID} theme={theme} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
{children}
|
{children}
|
||||||
<AppVersion theme={theme} />
|
<AppVersion theme={theme} />
|
||||||
|
|
|
@ -16,6 +16,7 @@ import Permission from './model/Permission';
|
||||||
import SlashCommand from './model/SlashCommand';
|
import SlashCommand from './model/SlashCommand';
|
||||||
import User from './model/User';
|
import User from './model/User';
|
||||||
import Server from './model/Server';
|
import Server from './model/Server';
|
||||||
|
import ServersHistory from './model/ServersHistory';
|
||||||
|
|
||||||
import serversSchema from './schema/servers';
|
import serversSchema from './schema/servers';
|
||||||
import appSchema from './schema/app';
|
import appSchema from './schema/app';
|
||||||
|
@ -71,7 +72,7 @@ class DB {
|
||||||
schema: serversSchema,
|
schema: serversSchema,
|
||||||
migrations: serversMigrations
|
migrations: serversMigrations
|
||||||
}),
|
}),
|
||||||
modelClasses: [Server, User],
|
modelClasses: [Server, User, ServersHistory],
|
||||||
actionsEnabled: true
|
actionsEnabled: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Model } from '@nozbe/watermelondb';
|
||||||
|
import { field, date, readonly } from '@nozbe/watermelondb/decorators';
|
||||||
|
|
||||||
|
export default class ServersHistory extends Model {
|
||||||
|
static table = 'servers_history';
|
||||||
|
|
||||||
|
@field('url') url;
|
||||||
|
|
||||||
|
@field('username') username;
|
||||||
|
|
||||||
|
@readonly @date('updated_at') updatedAt
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { schemaMigrations, addColumns } from '@nozbe/watermelondb/Schema/migrations';
|
import { schemaMigrations, addColumns, createTable } from '@nozbe/watermelondb/Schema/migrations';
|
||||||
|
|
||||||
export default schemaMigrations({
|
export default schemaMigrations({
|
||||||
migrations: [
|
migrations: [
|
||||||
|
@ -70,6 +70,19 @@ export default schemaMigrations({
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toVersion: 9,
|
||||||
|
steps: [
|
||||||
|
createTable({
|
||||||
|
name: 'servers_history',
|
||||||
|
columns: [
|
||||||
|
{ name: 'url', type: 'string', isIndexed: true },
|
||||||
|
{ name: 'username', type: 'string', isOptional: true },
|
||||||
|
{ name: 'updated_at', type: 'number' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
export default appSchema({
|
export default appSchema({
|
||||||
version: 8,
|
version: 9,
|
||||||
tables: [
|
tables: [
|
||||||
tableSchema({
|
tableSchema({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
@ -34,6 +34,14 @@ export default appSchema({
|
||||||
{ name: 'enterprise_modules', type: 'string', isOptional: true },
|
{ name: 'enterprise_modules', type: 'string', isOptional: true },
|
||||||
{ name: 'e2e_enable', type: 'boolean', isOptional: true }
|
{ name: 'e2e_enable', type: 'boolean', isOptional: true }
|
||||||
]
|
]
|
||||||
|
}),
|
||||||
|
tableSchema({
|
||||||
|
name: 'servers_history',
|
||||||
|
columns: [
|
||||||
|
{ name: 'url', type: 'string', isIndexed: true },
|
||||||
|
{ name: 'username', type: 'string', isOptional: true },
|
||||||
|
{ name: 'updated_at', type: 'number' }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import 'moment/min/locales';
|
import 'moment/min/locales';
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import * as types from '../actions/actionsTypes';
|
import * as types from '../actions/actionsTypes';
|
||||||
import {
|
import {
|
||||||
|
@ -52,6 +53,26 @@ const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnE
|
||||||
} else {
|
} else {
|
||||||
const server = yield select(getServer);
|
const server = yield select(getServer);
|
||||||
yield localAuthenticate(server);
|
yield localAuthenticate(server);
|
||||||
|
|
||||||
|
// Saves username on server history
|
||||||
|
const serversDB = database.servers;
|
||||||
|
const serversHistoryCollection = serversDB.collections.get('servers_history');
|
||||||
|
yield serversDB.action(async() => {
|
||||||
|
try {
|
||||||
|
const serversHistory = await serversHistoryCollection.query(Q.where('url', server)).fetch();
|
||||||
|
if (serversHistory?.length) {
|
||||||
|
const serverHistoryRecord = serversHistory[0];
|
||||||
|
// this is updating on every login just to save `updated_at`
|
||||||
|
// keeping this server as the most recent on autocomplete order
|
||||||
|
await serverHistoryRecord.update((s) => {
|
||||||
|
s.username = result.username;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
yield put(loginSuccess(result));
|
yield put(loginSuccess(result));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { put, takeLatest } from 'redux-saga/effects';
|
import { put, takeLatest } from 'redux-saga/effects';
|
||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
import Navigation from '../lib/Navigation';
|
import Navigation from '../lib/Navigation';
|
||||||
|
@ -131,18 +132,39 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleServerRequest = function* handleServerRequest({ server, certificate }) {
|
const handleServerRequest = function* handleServerRequest({
|
||||||
|
server, certificate, username, fromServerHistory
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
if (certificate) {
|
if (certificate) {
|
||||||
yield UserPreferences.setMapAsync(extractHostname(server), certificate);
|
yield UserPreferences.setMapAsync(extractHostname(server), certificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverInfo = yield getServerInfo({ server });
|
const serverInfo = yield getServerInfo({ server });
|
||||||
|
const serversDB = database.servers;
|
||||||
|
const serversHistoryCollection = serversDB.collections.get('servers_history');
|
||||||
|
|
||||||
if (serverInfo) {
|
if (serverInfo) {
|
||||||
yield RocketChat.getLoginServices(server);
|
yield RocketChat.getLoginServices(server);
|
||||||
yield RocketChat.getLoginSettings({ server });
|
yield RocketChat.getLoginSettings({ server });
|
||||||
Navigation.navigate('WorkspaceView');
|
Navigation.navigate('WorkspaceView');
|
||||||
|
|
||||||
|
if (fromServerHistory) {
|
||||||
|
Navigation.navigate('LoginView', { username });
|
||||||
|
}
|
||||||
|
|
||||||
|
yield serversDB.action(async() => {
|
||||||
|
try {
|
||||||
|
const serversHistory = await serversHistoryCollection.query(Q.where('url', server)).fetch();
|
||||||
|
if (!serversHistory?.length) {
|
||||||
|
await serversHistoryCollection.create((s) => {
|
||||||
|
s.url = server;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
yield put(selectServerRequest(server, serverInfo.version, false));
|
yield put(selectServerRequest(server, serverInfo.version, false));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -56,6 +56,7 @@ class LoginView extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
navigation: PropTypes.object,
|
navigation: PropTypes.object,
|
||||||
|
route: PropTypes.object,
|
||||||
Site_Name: PropTypes.string,
|
Site_Name: PropTypes.string,
|
||||||
Accounts_RegistrationForm: PropTypes.string,
|
Accounts_RegistrationForm: PropTypes.string,
|
||||||
Accounts_RegistrationForm_LinkReplacementText: PropTypes.string,
|
Accounts_RegistrationForm_LinkReplacementText: PropTypes.string,
|
||||||
|
@ -74,7 +75,7 @@ class LoginView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
user: '',
|
user: props.route.params?.username ?? '',
|
||||||
password: ''
|
password: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -123,6 +124,7 @@ class LoginView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUserForm = () => {
|
renderUserForm = () => {
|
||||||
|
const { user } = this.state;
|
||||||
const {
|
const {
|
||||||
Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_PasswordReset, Accounts_RegistrationForm_LinkReplacementText, isFetching, theme, Accounts_ShowFormLogin
|
Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_PasswordReset, Accounts_RegistrationForm_LinkReplacementText, isFetching, theme, Accounts_ShowFormLogin
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -146,6 +148,7 @@ class LoginView extends React.Component {
|
||||||
textContentType='username'
|
textContentType='username'
|
||||||
autoCompleteType='username'
|
autoCompleteType='username'
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
value={user}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label='Password'
|
label='Password'
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
View, StyleSheet, Text
|
||||||
|
} from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { CustomIcon } from '../../../lib/Icons';
|
||||||
|
import sharedStyles from '../../Styles';
|
||||||
|
import Touch from '../../../utils/touch';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
height: 56,
|
||||||
|
paddingHorizontal: 15,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column'
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
...sharedStyles.textMedium,
|
||||||
|
fontSize: 16
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Item = ({
|
||||||
|
item, theme, onPress, onDelete
|
||||||
|
}) => (
|
||||||
|
<Touch style={styles.container} onPress={() => onPress(item.url)} theme={theme} testID={`server-history-${ item.url }`}>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<Text style={[styles.server, { color: themes[theme].bodyText }]}>{item.url}</Text>
|
||||||
|
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]}>{item.username}</Text>
|
||||||
|
</View>
|
||||||
|
<BorderlessButton onPress={() => onDelete(item)} testID={`server-history-delete-${ item.url }`}>
|
||||||
|
<CustomIcon name='delete' size={24} color={themes[theme].auxiliaryText} />
|
||||||
|
</BorderlessButton>
|
||||||
|
</Touch>
|
||||||
|
);
|
||||||
|
|
||||||
|
Item.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
onPress: PropTypes.func,
|
||||||
|
onDelete: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Item;
|
|
@ -0,0 +1,87 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, FlatList, StyleSheet } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import TextInput from '../../../containers/TextInput';
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import Item from './Item';
|
||||||
|
import Separator from '../../../containers/Separator';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
zIndex: 1,
|
||||||
|
marginTop: 24,
|
||||||
|
marginBottom: 32
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
marginTop: 0,
|
||||||
|
marginBottom: 0
|
||||||
|
},
|
||||||
|
serverHistory: {
|
||||||
|
maxHeight: 180,
|
||||||
|
width: '100%',
|
||||||
|
top: '100%',
|
||||||
|
zIndex: 1,
|
||||||
|
position: 'absolute',
|
||||||
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderRadius: 2,
|
||||||
|
borderTopWidth: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ServerInput = ({
|
||||||
|
text,
|
||||||
|
theme,
|
||||||
|
serversHistory,
|
||||||
|
onChangeText,
|
||||||
|
onSubmit,
|
||||||
|
onDelete,
|
||||||
|
onPressServerHistory
|
||||||
|
}) => {
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<TextInput
|
||||||
|
label='Enter workspace URL'
|
||||||
|
placeholder='Ex. your-company.rocket.chat'
|
||||||
|
containerStyle={styles.inputContainer}
|
||||||
|
value={text}
|
||||||
|
returnKeyType='send'
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
testID='new-server-view-input'
|
||||||
|
onSubmitEditing={onSubmit}
|
||||||
|
clearButtonMode='while-editing'
|
||||||
|
keyboardType='url'
|
||||||
|
textContentType='URL'
|
||||||
|
theme={theme}
|
||||||
|
onFocus={() => setFocused(true)}
|
||||||
|
onBlur={() => setFocused(false)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
focused && serversHistory?.length
|
||||||
|
? (
|
||||||
|
<View style={[styles.serverHistory, { backgroundColor: themes[theme].backgroundColor, borderColor: themes[theme].separatorColor }]}>
|
||||||
|
<FlatList
|
||||||
|
data={serversHistory}
|
||||||
|
renderItem={({ item }) => <Item item={item} theme={theme} onPress={() => onPressServerHistory(item)} onDelete={onDelete} />}
|
||||||
|
ItemSeparatorComponent={() => <Separator theme={theme} />}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ServerInput.propTypes = {
|
||||||
|
text: PropTypes.string,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
serversHistory: PropTypes.array,
|
||||||
|
onChangeText: PropTypes.func,
|
||||||
|
onSubmit: PropTypes.func,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
onPressServerHistory: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerInput;
|
|
@ -1,42 +1,41 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Text, Keyboard, StyleSheet, TouchableOpacity, View, Alert, BackHandler
|
Text, Keyboard, StyleSheet, View, Alert, BackHandler
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as FileSystem from 'expo-file-system';
|
import * as FileSystem from 'expo-file-system';
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import parse from 'url-parse';
|
import parse from 'url-parse';
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import UserPreferences from '../lib/userPreferences';
|
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||||
import EventEmitter from '../utils/events';
|
import UserPreferences from '../../lib/userPreferences';
|
||||||
import { selectServerRequest, serverRequest } from '../actions/server';
|
import EventEmitter from '../../utils/events';
|
||||||
import { inviteLinksClear as inviteLinksClearAction } from '../actions/inviteLinks';
|
import { selectServerRequest, serverRequest } from '../../actions/server';
|
||||||
import sharedStyles from './Styles';
|
import { inviteLinksClear as inviteLinksClearAction } from '../../actions/inviteLinks';
|
||||||
import Button from '../containers/Button';
|
import sharedStyles from '../Styles';
|
||||||
import TextInput from '../containers/TextInput';
|
import Button from '../../containers/Button';
|
||||||
import OrSeparator from '../containers/OrSeparator';
|
import OrSeparator from '../../containers/OrSeparator';
|
||||||
import FormContainer, { FormContainerInner } from '../containers/FormContainer';
|
import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
|
||||||
import I18n from '../i18n';
|
import I18n from '../../i18n';
|
||||||
import { isIOS } from '../utils/deviceInfo';
|
import { isIOS } from '../../utils/deviceInfo';
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import log, { logEvent, events } from '../utils/log';
|
import log, { logEvent, events } from '../../utils/log';
|
||||||
import { animateNextTransition } from '../utils/layoutAnimation';
|
import { animateNextTransition } from '../../utils/layoutAnimation';
|
||||||
import { withTheme } from '../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch';
|
import { setBasicAuth, BASIC_AUTH_KEY } from '../../utils/fetch';
|
||||||
import { CloseModalButton } from '../containers/HeaderButton';
|
import { CloseModalButton } from '../../containers/HeaderButton';
|
||||||
import { showConfirmationAlert } from '../utils/info';
|
import { showConfirmationAlert } from '../../utils/info';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
import ServerInput from './ServerInput';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
title: {
|
title: {
|
||||||
...sharedStyles.textBold,
|
...sharedStyles.textBold,
|
||||||
fontSize: 22
|
fontSize: 22
|
||||||
},
|
},
|
||||||
inputContainer: {
|
|
||||||
marginTop: 24,
|
|
||||||
marginBottom: 32
|
|
||||||
},
|
|
||||||
certificatePicker: {
|
certificatePicker: {
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -84,12 +83,17 @@ class NewServerView extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
text: '',
|
text: '',
|
||||||
connectingOpen: false,
|
connectingOpen: false,
|
||||||
certificate: null
|
certificate: null,
|
||||||
|
serversHistory: []
|
||||||
};
|
};
|
||||||
EventEmitter.addEventListener('NewServer', this.handleNewServerEvent);
|
EventEmitter.addEventListener('NewServer', this.handleNewServerEvent);
|
||||||
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
|
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.queryServerHistory();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { adding } = this.props;
|
const { adding } = this.props;
|
||||||
if (prevProps.adding !== adding) {
|
if (prevProps.adding !== adding) {
|
||||||
|
@ -122,6 +126,29 @@ class NewServerView extends React.Component {
|
||||||
|
|
||||||
onChangeText = (text) => {
|
onChangeText = (text) => {
|
||||||
this.setState({ text });
|
this.setState({ text });
|
||||||
|
this.queryServerHistory(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryServerHistory = async(text) => {
|
||||||
|
const db = database.servers;
|
||||||
|
try {
|
||||||
|
const serversHistoryCollection = db.collections.get('servers_history');
|
||||||
|
let whereClause = [
|
||||||
|
Q.where('username', Q.notEq(null)),
|
||||||
|
Q.experimentalSortBy('updated_at', Q.desc),
|
||||||
|
Q.experimentalTake(3)
|
||||||
|
];
|
||||||
|
if (text) {
|
||||||
|
whereClause = [
|
||||||
|
...whereClause,
|
||||||
|
Q.where('url', Q.like(`%${ Q.sanitizeLikeString(text) }%`))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const serversHistory = await serversHistoryCollection.query(...whereClause).fetch();
|
||||||
|
this.setState({ serversHistory });
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close = () => {
|
close = () => {
|
||||||
|
@ -138,7 +165,11 @@ class NewServerView extends React.Component {
|
||||||
connectServer(server);
|
connectServer(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async() => {
|
onPressServerHistory = (serverHistory) => {
|
||||||
|
this.setState({ text: serverHistory?.url }, () => this.submit({ fromServerHistory: true, username: serverHistory?.username }));
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async({ fromServerHistory = false, username }) => {
|
||||||
logEvent(events.NEWSERVER_CONNECT_TO_WORKSPACE);
|
logEvent(events.NEWSERVER_CONNECT_TO_WORKSPACE);
|
||||||
const { text, certificate } = this.state;
|
const { text, certificate } = this.state;
|
||||||
const { connectServer } = this.props;
|
const { connectServer } = this.props;
|
||||||
|
@ -164,7 +195,11 @@ class NewServerView extends React.Component {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
const server = this.completeUrl(text);
|
const server = this.completeUrl(text);
|
||||||
await this.basicAuth(server, text);
|
await this.basicAuth(server, text);
|
||||||
connectServer(server, cert);
|
if (fromServerHistory) {
|
||||||
|
connectServer(server, cert, username, true);
|
||||||
|
} else {
|
||||||
|
connectServer(server, cert);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,6 +286,19 @@ class NewServerView extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteServerHistory = async(item) => {
|
||||||
|
const { serversHistory } = this.state;
|
||||||
|
const db = database.servers;
|
||||||
|
try {
|
||||||
|
await db.action(async() => {
|
||||||
|
await item.destroyPermanently();
|
||||||
|
});
|
||||||
|
this.setState({ serversHistory: serversHistory.filter(server => server.id !== item.id) });
|
||||||
|
} catch {
|
||||||
|
// Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderCertificatePicker = () => {
|
renderCertificatePicker = () => {
|
||||||
const { certificate } = this.state;
|
const { certificate } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
|
@ -283,24 +331,25 @@ class NewServerView extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { connecting, theme } = this.props;
|
const { connecting, theme } = this.props;
|
||||||
const { text, connectingOpen } = this.state;
|
const {
|
||||||
|
text, connectingOpen, serversHistory
|
||||||
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<FormContainer theme={theme} testID='new-server-view'>
|
<FormContainer
|
||||||
|
theme={theme}
|
||||||
|
testID='new-server-view'
|
||||||
|
keyboardShouldPersistTaps='never'
|
||||||
|
>
|
||||||
<FormContainerInner>
|
<FormContainerInner>
|
||||||
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Join_your_workspace')}</Text>
|
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Join_your_workspace')}</Text>
|
||||||
<TextInput
|
<ServerInput
|
||||||
label='Enter workspace URL'
|
text={text}
|
||||||
placeholder='Ex. your-company.rocket.chat'
|
|
||||||
containerStyle={styles.inputContainer}
|
|
||||||
value={text}
|
|
||||||
returnKeyType='send'
|
|
||||||
onChangeText={this.onChangeText}
|
|
||||||
testID='new-server-view-input'
|
|
||||||
onSubmitEditing={this.submit}
|
|
||||||
clearButtonMode='while-editing'
|
|
||||||
keyboardType='url'
|
|
||||||
textContentType='URL'
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
serversHistory={serversHistory}
|
||||||
|
onChangeText={this.onChangeText}
|
||||||
|
onSubmit={this.submit}
|
||||||
|
onDelete={this.deleteServerHistory}
|
||||||
|
onPressServerHistory={this.onPressServerHistory}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
title={I18n.t('Connect')}
|
title={I18n.t('Connect')}
|
||||||
|
@ -325,7 +374,7 @@ class NewServerView extends React.Component {
|
||||||
testID='new-server-view-open'
|
testID='new-server-view-open'
|
||||||
/>
|
/>
|
||||||
</FormContainerInner>
|
</FormContainerInner>
|
||||||
{ isIOS ? this.renderCertificatePicker() : null }
|
{isIOS ? this.renderCertificatePicker() : null}
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -338,7 +387,7 @@ const mapStateToProps = state => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)),
|
connectServer: (server, certificate, username, fromServerHistory) => dispatch(serverRequest(server, certificate, username, fromServerHistory)),
|
||||||
selectServer: server => dispatch(selectServerRequest(server)),
|
selectServer: server => dispatch(selectServerRequest(server)),
|
||||||
inviteLinksClear: () => dispatch(inviteLinksClearAction())
|
inviteLinksClear: () => dispatch(inviteLinksClearAction())
|
||||||
});
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
const {
|
||||||
|
device, expect, element, by, waitFor
|
||||||
|
} = require('detox');
|
||||||
|
const { login, navigateToLogin, logout, tapBack } = require('../../helpers/app');
|
||||||
|
const data = require('../../data');
|
||||||
|
|
||||||
|
describe('Server history', () => {
|
||||||
|
before(async() => {
|
||||||
|
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Usage', () => {
|
||||||
|
it('should login, save server as history and logout', async() => {
|
||||||
|
await navigateToLogin();
|
||||||
|
await login(data.users.regular.username, data.users.regular.password);
|
||||||
|
await logout();
|
||||||
|
await element(by.id('join-workspace')).tap();
|
||||||
|
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(60000);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show servers history', async() => {
|
||||||
|
await element(by.id('new-server-view-input')).tap();
|
||||||
|
await waitFor(element(by.id(`server-history-${ data.server }`))).toBeVisible().withTimeout(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should tap on a server history and navigate to login', async() => {
|
||||||
|
await element(by.id(`server-history-${ data.server }`)).tap();
|
||||||
|
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000);
|
||||||
|
await expect(element(by.id('login-view-email'))).toHaveText(data.users.regular.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete server from history', async() => {
|
||||||
|
await tapBack();
|
||||||
|
await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(2000);
|
||||||
|
await tapBack();
|
||||||
|
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
|
||||||
|
await element(by.id('new-server-view-input')).tap();
|
||||||
|
await waitFor(element(by.id(`server-history-${ data.server }`))).toBeVisible().withTimeout(2000);
|
||||||
|
await element(by.id(`server-history-delete-${ data.server }`)).tap();
|
||||||
|
await element(by.id('new-server-view-input')).tap();
|
||||||
|
await waitFor(element(by.id(`server-history-${ data.server }`))).toBeNotVisible().withTimeout(2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue