From 214a39c3d31b30f463cf3beb76d335cef4c6fe32 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 13 Feb 2018 18:26:33 +0000 Subject: [PATCH 1/3] All: Improved the way settings are changed. Should also fixed issue with sync context being accidentally broken. --- ElectronClient/app/gui/ConfigScreen.jsx | 30 ++----------- README.md | 1 + .../lib/components/screens/config.js | 35 ++------------- .../lib/components/shared/config-shared.js | 44 +++++++++++++++++++ docs/index.html | 9 ++-- 5 files changed, 58 insertions(+), 61 deletions(-) diff --git a/ElectronClient/app/gui/ConfigScreen.jsx b/ElectronClient/app/gui/ConfigScreen.jsx index 53d4ac9e3..9eac98456 100644 --- a/ElectronClient/app/gui/ConfigScreen.jsx +++ b/ElectronClient/app/gui/ConfigScreen.jsx @@ -15,10 +15,6 @@ class ConfigScreenComponent extends React.Component { constructor() { super(); - this.state = { - settings: {}, - }; - shared.init(this); this.checkSyncConfig_ = async () => { @@ -68,9 +64,7 @@ class ConfigScreenComponent extends React.Component { }; const updateSettingValue = (key, value) => { - const settings = Object.assign({}, this.state.settings); - settings[key] = Setting.formatValue(key, value); - this.setState({ settings: settings }); + return shared.updateSettingValue(this, key, value); } // Component key needs to be key+value otherwise it doesn't update when the settings change. @@ -142,10 +136,7 @@ class ConfigScreenComponent extends React.Component { } onSaveClick() { - for (let n in this.state.settings) { - if (!this.state.settings.hasOwnProperty(n)) continue; - Setting.setValue(n, this.state.settings[n]); - } + shared.saveSettings(this); this.props.dispatch({ type: 'NAV_BACK' }); } @@ -167,24 +158,11 @@ class ConfigScreenComponent extends React.Component { }; const buttonStyle = { - display: this.state.settings === this.props.settings ? 'none' : 'inline-block', + display: this.state.changedSettingKeys.length ? 'inline-block' : 'none', marginRight: 10, } - let settingComps = []; - let keys = Setting.keys(true, 'desktop'); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (!(key in settings)) { - console.warn('Missing setting: ' + key); - continue; - } - const md = Setting.settingMetadata(key); - if (md.show && !md.show(settings)) continue; - const comp = this.settingToComponent(key, settings[key]); - if (!comp) continue; - settingComps.push(comp); - } + const settingComps = shared.settingsToComponents(this, 'desktop', settings); const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']); diff --git a/README.md b/README.md index 4a94cdebc..0f2f15c30 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Select the "WebDAV" synchronisation target and follow the same instructions as f Known compatible services that use WebDAV: +- [Box.com](https://www.box.com/) - [DriveHQ](https://www.drivehq.com) - [Zimbra](https://www.zimbra.com/) diff --git a/ReactNativeClient/lib/components/screens/config.js b/ReactNativeClient/lib/components/screens/config.js index fd28acf96..f705693ef 100644 --- a/ReactNativeClient/lib/components/screens/config.js +++ b/ReactNativeClient/lib/components/screens/config.js @@ -20,11 +20,6 @@ class ConfigScreenComponent extends BaseScreenComponent { super(); this.styles_ = {}; - this.state = { - settings: {}, - settingsChanged: false, - }; - shared.init(this); this.checkSyncConfig_ = async () => { @@ -32,11 +27,7 @@ class ConfigScreenComponent extends BaseScreenComponent { } this.saveButton_press = () => { - for (let n in this.state.settings) { - if (!this.state.settings.hasOwnProperty(n)) continue; - Setting.setValue(n, this.state.settings[n]); - } - this.setState({settingsChanged:false}); + return shared.saveSettings(this); }; } @@ -113,12 +104,7 @@ class ConfigScreenComponent extends BaseScreenComponent { let output = null; const updateSettingValue = (key, value) => { - const settings = Object.assign({}, this.state.settings); - settings[key] = Setting.formatValue(key, value); - this.setState({ - settings: settings, - settingsChanged: true, - }); + return shared.updateSettingValue(this, key, value); } const md = Setting.settingMetadata(key); @@ -187,20 +173,7 @@ class ConfigScreenComponent extends BaseScreenComponent { render() { const settings = this.state.settings; - const keys = Setting.keys(true, 'mobile'); - let settingComps = []; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - //if (key == 'sync.target' && !settings.showAdvancedOptions) continue; - if (!Setting.isPublic(key)) continue; - - const md = Setting.settingMetadata(key); - if (md.show && !md.show(settings)) continue; - - const comp = this.settingToComponent(key, settings[key]); - if (!comp) continue; - settingComps.push(comp); - } + const settingComps = shared.settingsToComponents(this, 'mobile', settings); const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']); @@ -244,7 +217,7 @@ class ConfigScreenComponent extends BaseScreenComponent { diff --git a/ReactNativeClient/lib/components/shared/config-shared.js b/ReactNativeClient/lib/components/shared/config-shared.js index f6c72ac0e..60a1ddd7a 100644 --- a/ReactNativeClient/lib/components/shared/config-shared.js +++ b/ReactNativeClient/lib/components/shared/config-shared.js @@ -7,6 +7,8 @@ const shared = {} shared.init = function(comp) { if (!comp.state) comp.state = {}; comp.state.checkSyncConfigResult = null; + comp.state.settings = {}; + comp.state.changedSettingKeys = []; } shared.checkSyncConfig = async function(comp, settings) { @@ -34,4 +36,46 @@ shared.checkSyncConfigMessages = function(comp) { return output; } +shared.updateSettingValue = function(comp, key, value) { + const settings = Object.assign({}, comp.state.settings); + const changedSettingKeys = comp.state.changedSettingKeys.slice(); + settings[key] = Setting.formatValue(key, value); + if (changedSettingKeys.indexOf(key) < 0) changedSettingKeys.push(key); + + comp.setState({ + settings: settings, + changedSettingKeys: changedSettingKeys, + }); +} + +shared.saveSettings = function(comp) { + for (let key in comp.state.settings) { + if (!comp.state.settings.hasOwnProperty(key)) continue; + if (comp.state.changedSettingKeys.indexOf(key) < 0) continue; + console.info("Saving", key, comp.state.settings[key]); + Setting.setValue(key, comp.state.settings[key]); + } + + comp.setState({ changedSettingKeys: [] }); +} + +shared.settingsToComponents = function(comp, device, settings) { + const keys = Setting.keys(true, device); + const settingComps = []; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (!Setting.isPublic(key)) continue; + + const md = Setting.settingMetadata(key); + if (md.show && !md.show(settings)) continue; + + const settingComp = comp.settingToComponent(key, settings[key]); + if (!settingComp) continue; + settingComps.push(settingComp); + } + + return settingComps +} + module.exports = shared; \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index bb9f7e3db..c0665865d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -310,6 +310,7 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin

Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.

Known compatible services that use WebDAV:

@@ -390,14 +391,14 @@ $$ Croatian hr_HR -Hrvoje Mandić trbuhom@net.hr +Hrvoje Mandić trbuhom@net.hr 72% Deutsch de_DE -Tobias Strobel git@strobeltobias.de +Tobias Strobel git@strobeltobias.de 91% @@ -453,14 +454,14 @@ $$ Русский ru_RU -Artyom Karlov artyom.karlov@gmail.com +Artyom Karlov artyom.karlov@gmail.com 94% 中文 (简体) zh_CN -RCJacH RCJacH@outlook.com +RCJacH RCJacH@outlook.com 75% From 18711230665796e1a5930a1d160c13947ad32642 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 14 Feb 2018 19:08:07 +0000 Subject: [PATCH 2/3] All: Improved WebDAV driver compatibility with some services (eg. Seafile) --- README_terminal.md | 2 +- ReactNativeClient/lib/WebDavApi.js | 36 +++++++++++++++++++ .../lib/components/screens/config.js | 9 +++-- .../lib/file-api-driver-webdav.js | 32 +++++++++++++---- ReactNativeClient/lib/models/Setting.js | 2 +- docs/index.html | 8 ++--- docs/terminal/index.html | 2 +- joplin.sublime-project | 2 ++ 8 files changed, 77 insertions(+), 16 deletions(-) diff --git a/README_terminal.md b/README_terminal.md index b7153e235..23be47990 100644 --- a/README_terminal.md +++ b/README_terminal.md @@ -244,7 +244,7 @@ The following commands are available in [command-line mode](#command-line-mode): Possible values: HH:mm (20:30), h:mm A (8:30 PM). Default: "HH:mm" - uncompletedTodosOnTop Show uncompleted todos on top of the lists. + uncompletedTodosOnTop Show uncompleted to-dos on top of the lists. Type: bool. Default: true diff --git a/ReactNativeClient/lib/WebDavApi.js b/ReactNativeClient/lib/WebDavApi.js index 9cd80b8e0..a8c1ceb1f 100644 --- a/ReactNativeClient/lib/WebDavApi.js +++ b/ReactNativeClient/lib/WebDavApi.js @@ -133,6 +133,42 @@ class WebDavApi { return this.valueFromJson(json, keys, 'array'); } + resourcePropByName(resource, outputType, propName) { + const propStats = resource['d:propstat']; + let output = null; + if (!Array.isArray(propStats)) throw new Error('Missing d:propstat property'); + for (let i = 0; i < propStats.length; i++) { + const props = propStats[i]['d:prop']; + if (!Array.isArray(props) || !props.length) continue; + const prop = props[0]; + if (Array.isArray(prop[propName])) { + output = prop[propName]; + break; + } + } + + if (outputType === 'string') { + // If the XML has not attribute the value is directly a string + // If the XML node has attributes, the value is under "_". + // Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}: + // Thu, 01 Feb 2018 17:24:05 GMT + // For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT" + // Thu, 01 Feb 2018 17:24:05 GMT + + output = output[0]; + + if (typeof output === 'object' && '_' in output) output = output['_']; + if (typeof output !== 'string') return null; + return output; + } + + if (outputType === 'array') { + return output; + } + + throw new Error('Invalid output type: ' + outputType); + } + async execPropFind(path, depth, fields = null, options = null) { if (fields === null) fields = ['d:getlastmodified']; diff --git a/ReactNativeClient/lib/components/screens/config.js b/ReactNativeClient/lib/components/screens/config.js index f705693ef..b96407441 100644 --- a/ReactNativeClient/lib/components/screens/config.js +++ b/ReactNativeClient/lib/components/screens/config.js @@ -65,6 +65,11 @@ class ConfigScreenComponent extends BaseScreenComponent { fontSize: theme.fontSize, flex: 1, }, + descriptionText: { + color: theme.color, + fontSize: theme.fontSize, + flex: 1, + }, settingControl: { color: theme.color, flex: 1, @@ -181,8 +186,8 @@ class ConfigScreenComponent extends BaseScreenComponent { const messages = shared.checkSyncConfigMessages(this); const statusComp = !messages.length ? null : ( - {messages[0]} - {messages.length >= 1 ? ({messages[1]}) : null} + {messages[0]} + {messages.length >= 1 ? ({messages[1]}) : null} ); settingComps.push( diff --git a/ReactNativeClient/lib/file-api-driver-webdav.js b/ReactNativeClient/lib/file-api-driver-webdav.js index a409a13c1..d2dea8a67 100644 --- a/ReactNativeClient/lib/file-api-driver-webdav.js +++ b/ReactNativeClient/lib/file-api-driver-webdav.js @@ -27,7 +27,7 @@ class FileApiDriverWebDav { const result = await this.api().execPropFind(path, 0, [ 'd:getlastmodified', 'd:resourcetype', - 'd:getcontentlength', // Remove this once PUT call issue is sorted out + // 'd:getcontentlength', // Remove this once PUT call issue is sorted out ]); const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]); @@ -39,22 +39,40 @@ class FileApiDriverWebDav { } statFromResource_(resource, path) { - const isCollection = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]); - const lastModifiedString = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]); + // WebDAV implementations are always slighly different from one server to another but, at the minimum, + // a resource should have a propstat key - if not it's probably an error. + const propStat = this.api().arrayFromJson(resource, ['d:propstat']); + if (!Array.isArray(propStat)) throw new Error('Invalid WebDAV resource format: ' + JSON.stringify(resource)); + + const resourceTypes = this.api().resourcePropByName(resource, 'array', 'd:resourcetype'); + let isDir = false; + if (Array.isArray(resourceTypes)) { + for (let i = 0; i < resourceTypes.length; i++) { + const t = resourceTypes[i]; + if (typeof t === 'object' && 'd:collection' in t) { + isDir = true; + break; + } + } + } + + const lastModifiedString = this.api().resourcePropByName(resource, 'string', 'd:getlastmodified'); // const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0])); // if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource)); - if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource)); - const lastModifiedDate = new Date(lastModifiedString); + // Note: Not all WebDAV servers return a getlastmodified date (eg. Seafile, which doesn't return the + // property for folders) so we can only throw an error if it's a file. + if (!lastModifiedString && !isDir) throw new Error('Could not get lastModified date for resource: ' + JSON.stringify(resource)); + const lastModifiedDate = lastModifiedString ? new Date(lastModifiedString) : new Date(); if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString); return { path: path, // created_time: lastModifiedDate.getTime(), updated_time: lastModifiedDate.getTime(), - isDir: isCollection === '', + isDir: isDir, // sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on. }; } @@ -260,7 +278,7 @@ class FileApiDriverWebDav { ]); const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']); - const stats = this.statsFromResources_(resources) + const stats = this.statsFromResources_(resources); return { items: stats, diff --git a/ReactNativeClient/lib/models/Setting.js b/ReactNativeClient/lib/models/Setting.js index c100ce524..0e8bc5da0 100644 --- a/ReactNativeClient/lib/models/Setting.js +++ b/ReactNativeClient/lib/models/Setting.js @@ -58,7 +58,7 @@ class Setting extends BaseModel { // recent: _('Non-completed and recently completed ones'), // nonCompleted: _('Non-completed ones only'), // })}, - 'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') }, + 'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted to-dos on top of the lists') }, 'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') }, 'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => { return { diff --git a/docs/index.html b/docs/index.html index c0665865d..c2f448f37 100644 --- a/docs/index.html +++ b/docs/index.html @@ -391,14 +391,14 @@ $$ Croatian hr_HR -Hrvoje Mandić trbuhom@net.hr +Hrvoje Mandić trbuhom@net.hr 72% Deutsch de_DE -Tobias Strobel git@strobeltobias.de +Tobias Strobel git@strobeltobias.de 91% @@ -454,14 +454,14 @@ $$ Русский ru_RU -Artyom Karlov artyom.karlov@gmail.com +Artyom Karlov artyom.karlov@gmail.com 94% 中文 (简体) zh_CN -RCJacH RCJacH@outlook.com +RCJacH RCJacH@outlook.com 75% diff --git a/docs/terminal/index.html b/docs/terminal/index.html index dbca7dcd2..d3f114309 100644 --- a/docs/terminal/index.html +++ b/docs/terminal/index.html @@ -395,7 +395,7 @@ Possible keys/values: Possible values: HH:mm (20:30), h:mm A (8:30 PM). Default: "HH:mm" - uncompletedTodosOnTop Show uncompleted todos on top of the lists. + uncompletedTodosOnTop Show uncompleted to-dos on top of the lists. Type: bool. Default: true diff --git a/joplin.sublime-project b/joplin.sublime-project index e971e5f55..1c24e1815 100755 --- a/joplin.sublime-project +++ b/joplin.sublime-project @@ -18,6 +18,8 @@ "*.min.js", "ElectronClient/app/gui/note-viewer/highlight/*.pack.js", "ElectronClient/app/css/font-awesome.min.css", + "docs/*.html", + "docs/*.svg", ], "folder_exclude_patterns": [ From 414e57ec559c6a5d3204905eacd0cce96f9f17b3 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 14 Feb 2018 19:08:24 +0000 Subject: [PATCH 3/3] Electron release v1.0.63 --- ElectronClient/app/package-lock.json | 2 +- ElectronClient/app/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json index 3deb4b461..55f821c51 100644 --- a/ElectronClient/app/package-lock.json +++ b/ElectronClient/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "Joplin", - "version": "1.0.62", + "version": "1.0.63", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json index 0317e6cba..3c3f89681 100644 --- a/ElectronClient/app/package.json +++ b/ElectronClient/app/package.json @@ -1,6 +1,6 @@ { "name": "Joplin", - "version": "1.0.62", + "version": "1.0.63", "description": "Joplin for Desktop", "main": "main.js", "scripts": {