1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

All: Added sync config check to config screens

This commit is contained in:
Laurent Cozic 2018-02-06 18:59:36 +00:00
parent a25fcacace
commit fa5f418c22
13 changed files with 230 additions and 46 deletions

View File

@ -9,9 +9,7 @@ rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data"
if [[ $TEST_FILE == "" ]]; then
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
(cd "$ROOT_DIR" && npm test tests-build/encryption.js)
(cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js)
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js)
else
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
fi

View File

@ -0,0 +1,32 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Setting = require('lib/models/Setting.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('models_Setting', function() {
beforeEach(async (done) => {
done();
});
it('should return only sub-values', asyncTest(async () => {
const settings = {
'sync.5.path': 'http://example.com',
'sync.5.username': 'testing',
}
let output = Setting.subValues('sync.5', settings);
expect(output['path']).toBe('http://example.com');
expect(output['username']).toBe('testing');
output = Setting.subValues('sync.4', settings);
expect('path' in output).toBe(false);
expect('username' in output).toBe(false);
}));
});

View File

@ -19,7 +19,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 35000; // The first test is slow because the database needs to be built
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; // The first test is slow because the database needs to be built
async function allItems() {
let folders = await Folder.all();

View File

@ -7,6 +7,8 @@ const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
const pathUtils = require('lib/path-utils.js');
const { _ } = require('lib/locale.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const shared = require('lib/components/shared/config-shared.js');
class ConfigScreenComponent extends React.Component {
@ -16,6 +18,16 @@ class ConfigScreenComponent extends React.Component {
this.state = {
settings: {},
};
shared.init(this);
this.checkSyncConfig_ = async () => {
await shared.checkSyncConfig(this, this.state.settings);
}
this.rowStyle_ = {
marginBottom: 10,
};
}
componentWillMount() {
@ -44,9 +56,7 @@ class ConfigScreenComponent extends React.Component {
let output = null;
const rowStyle = {
marginBottom: 10,
};
const rowStyle = this.rowStyle_;
const labelStyle = Object.assign({}, theme.textStyle, {
display: 'inline-block',
@ -59,7 +69,7 @@ class ConfigScreenComponent extends React.Component {
const updateSettingValue = (key, value) => {
const settings = Object.assign({}, this.state.settings);
settings[key] = value;
settings[key] = Setting.formatValue(key, value);
this.setState({ settings: settings });
}
@ -104,12 +114,13 @@ class ConfigScreenComponent extends React.Component {
updateSettingValue(key, event.target.value);
}
const inputStyle = Object.assign({}, controlStyle, { width: '50%', minWidth: '20em' });
const inputType = md.secure === true ? 'password' : 'text';
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<input type={inputType} style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
<input type={inputType} style={inputStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
</div>
);
} else if (md.type === Setting.TYPE_INT) {
@ -144,7 +155,7 @@ class ConfigScreenComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
const style = Object.assign({}, this.props.style, { overflow: 'auto' });
const settings = this.state.settings;
const headerStyle = {
@ -175,6 +186,24 @@ class ConfigScreenComponent extends React.Component {
settingComps.push(comp);
}
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
const statusComp = !messages.length ? null : (
<div style={statusStyle}>
{messages[0]}
{messages.length >= 1 ? (<p>{messages[1]}</p>) : null}
</div>);
settingComps.push(
<div key="check_sync_config_button" style={this.rowStyle_}>
<button disabled={this.state.checkSyncConfigResult === 'checking'} onClick={this.checkSyncConfig_}>{_('Check synchronisation configuration')}</button>
{ statusComp }
</div>);
}
return (
<div style={style}>
<Header style={headerStyle} />

View File

@ -10,6 +10,10 @@ class BaseSyncTarget {
this.options_ = options;
}
static supportsConfigCheck() {
return false;
}
option(name, defaultValue = null) {
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
}

View File

@ -1,9 +1,13 @@
// The Nextcloud sync target is essentially a wrapper over the WebDAV sync target,
// thus all the calls to SyncTargetWebDAV to avoid duplicate code.
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetNextcloud extends BaseSyncTarget {
@ -12,9 +16,8 @@ class SyncTargetNextcloud extends BaseSyncTarget {
return 5;
}
constructor(db, options = null) {
super(db, options);
// this.authenticated_ = false;
static supportsConfigCheck() {
return true;
}
static targetName() {
@ -27,21 +30,21 @@ class SyncTargetNextcloud extends BaseSyncTarget {
isAuthenticated() {
return true;
//return this.authenticated_;
}
static async checkConfig(options) {
return SyncTargetWebDAV.checkConfig(options);
}
async initFileApi() {
const options = {
baseUrl: () => Setting.value('sync.5.path'),
username: () => Setting.value('sync.5.username'),
password: () => Setting.value('sync.5.password'),
};
const fileApi = await SyncTargetWebDAV.initFileApi_({
path: Setting.value('sync.5.path'),
username: Setting.value('sync.5.username'),
password: Setting.value('sync.5.password'),
});
const api = new WebDavApi(options);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(SyncTargetNextcloud.id());
fileApi.setLogger(this.logger());
return fileApi;
}

View File

@ -12,6 +12,7 @@ class SyncTargetRegistry {
name: SyncTargetClass.targetName(),
label: SyncTargetClass.label(),
classRef: SyncTargetClass,
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
};
}

View File

@ -12,8 +12,8 @@ class SyncTargetWebDAV extends BaseSyncTarget {
return 6;
}
constructor(db, options = null) {
super(db, options);
static supportsConfigCheck() {
return true;
}
static targetName() {
@ -28,18 +28,49 @@ class SyncTargetWebDAV extends BaseSyncTarget {
return true;
}
async initFileApi() {
const options = {
baseUrl: () => Setting.value('sync.6.path'),
username: () => Setting.value('sync.6.username'),
password: () => Setting.value('sync.6.password'),
static async initFileApi_(options) {
const apiOptions = {
baseUrl: () => options.path,
username: () => options.username,
password: () => options.password,
};
const api = new WebDavApi(options);
const api = new WebDavApi(apiOptions);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(SyncTargetWebDAV.id());
fileApi.setSyncTargetId(this.id());
return fileApi;
}
static async checkConfig(options) {
const fileApi = await SyncTargetWebDAV.initFileApi_(options);
const output = {
ok: false,
errorMessage: '',
};
try {
const result = await fileApi.stat('');
if (!result) throw new Error('Could not access WebDAV directory');
output.ok = true;
} catch (error) {
output.errorMessage = error.message;
if (error.code) output.errorMessage += ' (Code ' + error.code + ')';
}
return output;
}
async initFileApi() {
const fileApi = await SyncTargetWebDAV.initFileApi_({
path: Setting.value('sync.6.path'),
username: Setting.value('sync.6.username'),
password: Setting.value('sync.6.password'),
});
fileApi.setLogger(this.logger());
return fileApi;
}

View File

@ -187,7 +187,7 @@ class WebDavApi {
let response = null;
// console.info('WebDAV', method + ' ' + url, headers, options);
// console.info('WebDAV Call', method + ' ' + url, headers, options);
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
response = await shim.uploadBlob(url, fetchOptions);
@ -199,6 +199,8 @@ class WebDavApi {
const responseText = await response.text();
// console.info('WebDAV Response', responseText);
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = () => {

View File

@ -7,6 +7,8 @@ const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { Dropdown } = require('lib/components/Dropdown.js');
const { themeStyle } = require('lib/components/global-style.js');
const Setting = require('lib/models/Setting.js');
const shared = require('lib/components/shared/config-shared.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
class ConfigScreenComponent extends BaseScreenComponent {
@ -23,6 +25,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
settingsChanged: false,
};
shared.init(this);
this.checkSyncConfig_ = async () => {
await shared.checkSyncConfig(this, this.state.settings);
}
this.saveButton_press = () => {
for (let n in this.state.settings) {
if (!this.state.settings.hasOwnProperty(n)) continue;
@ -193,6 +201,27 @@ class ConfigScreenComponent extends BaseScreenComponent {
if (!comp) continue;
settingComps.push(comp);
}
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusComp = !messages.length ? null : (
<View style={{flex:1, marginTop: 10}}>
<Text>{messages[0]}</Text>
{messages.length >= 1 ? (<Text style={{marginTop:10}}>{messages[1]}</Text>) : null}
</View>);
settingComps.push(
<View key="check_sync_config_button" style={this.styles().settingContainer}>
<View style={{flex:1, flexDirection: 'column'}}>
<View style={{flex:1}}>
<Button title={_('Check synchronisation configuration')} onPress={this.checkSyncConfig_}/>
</View>
{ statusComp }
</View>
</View>);
}
settingComps.push(
<View key="website_link" style={this.styles().settingContainer}>

View File

@ -0,0 +1,38 @@
const Setting = require('lib/models/Setting.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const { _ } = require('lib/locale.js');
const shared = {}
shared.init = function(comp) {
if (!comp.state) comp.state = {};
comp.state.checkSyncConfigResult = null;
}
shared.checkSyncConfig = async function(comp, settings) {
const syncTargetId = settings['sync.target'];
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
const options = Setting.subValues('sync.' + syncTargetId, settings);
comp.setState({ checkSyncConfigResult: 'checking' });
const result = await SyncTargetClass.checkConfig(options);
console.info(result);
comp.setState({ checkSyncConfigResult: result });
}
shared.checkSyncConfigMessages = function(comp) {
const result = comp.state.checkSyncConfigResult;
const output = [];
if (result === 'checking') {
output.push(_('Checking... Please wait.'));
} else if (result && result.ok) {
output.push(_('Success! Synchronisation configuration appears to be correct.'));
} else if (result && !result.ok) {
output.push(_('Error. Please check that URL, username, password, etc. are correct and that the sync target is accessible. The reported error was:'));
output.push(result.errorMessage);
}
return output;
}
module.exports = shared;

View File

@ -43,6 +43,7 @@ class FileApi {
}
setLogger(l) {
if (!l) l = new Logger();
this.logger_ = l;
}

View File

@ -392,26 +392,42 @@ class Setting extends BaseModel {
return !!options[value];
}
// Currently only supports objects with properties one level deep
static object(key) {
// For example, if settings is:
// { sync.5.path: 'http://example', sync.5.username: 'testing' }
// and baseKey is 'sync.5', the function will return
// { path: 'http://example', username: 'testing' }
static subValues(baseKey, settings) {
let output = {};
let keys = this.keys();
for (let i = 0; i < keys.length; i++) {
let k = keys[i].split('.');
if (k[0] == key) {
output[k[1]] = this.value(keys[i]);
for (let key in settings) {
if (!settings.hasOwnProperty(key)) continue;
if (key.indexOf(baseKey) === 0) {
const subKey = key.substr(baseKey.length + 1);
output[subKey] = settings[key];
}
}
return output;
}
// Currently only supports objects with properties one level deep
static setObject(key, object) {
for (let n in object) {
if (!object.hasOwnProperty(n)) continue;
this.setValue(key + '.' + n, object[n]);
}
}
// static object(key) {
// let output = {};
// let keys = this.keys();
// for (let i = 0; i < keys.length; i++) {
// let k = keys[i].split('.');
// if (k[0] == key) {
// output[k[1]] = this.value(keys[i]);
// }
// }
// return output;
// }
// Currently only supports objects with properties one level deep
// static setObject(key, object) {
// for (let n in object) {
// if (!object.hasOwnProperty(n)) continue;
// this.setValue(key + '.' + n, object[n]);
// }
// }
static saveAll() {
if (!this.saveTimeoutId_) return Promise.resolve();