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:
parent
a25fcacace
commit
fa5f418c22
@ -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
|
32
CliClient/tests/models_Setting.js
Normal file
32
CliClient/tests/models_Setting.js
Normal 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);
|
||||
}));
|
||||
|
||||
});
|
@ -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();
|
||||
|
@ -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} />
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ class SyncTargetRegistry {
|
||||
name: SyncTargetClass.targetName(),
|
||||
label: SyncTargetClass.label(),
|
||||
classRef: SyncTargetClass,
|
||||
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 = () => {
|
||||
|
@ -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}>
|
||||
|
38
ReactNativeClient/lib/components/shared/config-shared.js
Normal file
38
ReactNativeClient/lib/components/shared/config-shared.js
Normal 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;
|
@ -43,6 +43,7 @@ class FileApi {
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
if (!l) l = new Logger();
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user