You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	All: Added sync config check to config screens
This commit is contained in:
		| @@ -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(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user