1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-13 22:12:50 +02:00

All: Display last sync error unless it's a timeout or network error

This commit is contained in:
Laurent Cozic
2018-03-09 19:51:01 +00:00
parent 8555ecce87
commit 1acffce62d
3 changed files with 217 additions and 118 deletions

View File

@@ -1,20 +1,19 @@
const React = require('react'); const React = require("react");
const { connect } = require('react-redux'); const { connect } = require("react-redux");
const shared = require('lib/components/shared/side-menu-shared.js'); const shared = require("lib/components/shared/side-menu-shared.js");
const { Synchronizer } = require('lib/synchronizer.js'); const { Synchronizer } = require("lib/synchronizer.js");
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require("lib/BaseModel.js");
const Folder = require('lib/models/Folder.js'); const Folder = require("lib/models/Folder.js");
const Note = require('lib/models/Note.js'); const Note = require("lib/models/Note.js");
const Tag = require('lib/models/Tag.js'); const Tag = require("lib/models/Tag.js");
const { _ } = require('lib/locale.js'); const { _ } = require("lib/locale.js");
const { themeStyle } = require('../theme.js'); const { themeStyle } = require("../theme.js");
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require("electron").remote.require("./bridge");
const Menu = bridge().Menu; const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem; const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require('../InteropServiceHelper.js'); const InteropServiceHelper = require("../InteropServiceHelper.js");
class SideBarComponent extends React.Component { class SideBarComponent extends React.Component {
style() { style() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
@@ -28,63 +27,65 @@ class SideBarComponent extends React.Component {
height: itemHeight, height: itemHeight,
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: theme.fontSize, fontSize: theme.fontSize,
textDecoration: 'none', textDecoration: "none",
boxSizing: 'border-box', boxSizing: "border-box",
color: theme.color2, color: theme.color2,
paddingLeft: 14, paddingLeft: 14,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
cursor: 'default', cursor: "default",
opacity: 0.8, opacity: 0.8,
whiteSpace: 'nowrap', whiteSpace: "nowrap",
}, },
listItemSelected: { listItemSelected: {
backgroundColor: theme.selectedColor2, backgroundColor: theme.selectedColor2,
}, },
conflictFolder: { conflictFolder: {
color: theme.colorError2, color: theme.colorError2,
fontWeight: 'bold', fontWeight: "bold",
}, },
header: { header: {
height: itemHeight * 1.8, height: itemHeight * 1.8,
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: theme.fontSize * 1.3, fontSize: theme.fontSize * 1.3,
textDecoration: 'none', textDecoration: "none",
boxSizing: 'border-box', boxSizing: "border-box",
color: theme.color2, color: theme.color2,
paddingLeft: 8, paddingLeft: 8,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
}, },
button: { button: {
padding: 6, padding: 6,
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: theme.fontSize, fontSize: theme.fontSize,
textDecoration: 'none', textDecoration: "none",
boxSizing: 'border-box', boxSizing: "border-box",
color: theme.color2, color: theme.color2,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
border: "1px solid rgba(255,255,255,0.2)", border: "1px solid rgba(255,255,255,0.2)",
marginTop: 10, marginTop: 10,
marginLeft: 5, marginLeft: 5,
marginRight: 5, marginRight: 5,
cursor: 'default', cursor: "default",
}, },
syncReport: { syncReport: {
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: Math.round(theme.fontSize * .9), fontSize: Math.round(theme.fontSize * 0.9),
color: theme.color2, color: theme.color2,
opacity: .5, opacity: 0.5,
display: 'flex', display: "flex",
alignItems: 'left', alignItems: "left",
justifyContent: 'top', justifyContent: "top",
flexDirection: 'column', flexDirection: "column",
marginTop: 10, marginTop: 10,
marginLeft: 5, marginLeft: 5,
marginRight: 5, marginRight: 5,
minHeight: 70, minHeight: 70,
wordWrap: "break-word",
width: "100%",
}, },
}; };
@@ -92,19 +93,19 @@ class SideBarComponent extends React.Component {
} }
itemContextMenu(event) { itemContextMenu(event) {
const itemId = event.target.getAttribute('data-id'); const itemId = event.target.getAttribute("data-id");
if (itemId === Folder.conflictFolderId()) return; if (itemId === Folder.conflictFolderId()) return;
const itemType = Number(event.target.getAttribute('data-type')); const itemType = Number(event.target.getAttribute("data-type"));
if (!itemId || !itemType) throw new Error('No data on element'); if (!itemId || !itemType) throw new Error("No data on element");
let deleteMessage = ''; let deleteMessage = "";
if (itemType === BaseModel.TYPE_FOLDER) { if (itemType === BaseModel.TYPE_FOLDER) {
deleteMessage = _('Delete notebook? All notes within this notebook will also be deleted.'); deleteMessage = _("Delete notebook? All notes within this notebook will also be deleted.");
} else if (itemType === BaseModel.TYPE_TAG) { } else if (itemType === BaseModel.TYPE_TAG) {
deleteMessage = _('Remove this tag from all the notes?'); deleteMessage = _("Remove this tag from all the notes?");
} else if (itemType === BaseModel.TYPE_SEARCH) { } else if (itemType === BaseModel.TYPE_SEARCH) {
deleteMessage = _('Remove this search from the sidebar?'); deleteMessage = _("Remove this search from the sidebar?");
} }
const menu = new Menu(); const menu = new Menu();
@@ -114,40 +115,55 @@ class SideBarComponent extends React.Component {
item = BaseModel.byId(this.props.folders, itemId); item = BaseModel.byId(this.props.folders, itemId);
} }
menu.append(new MenuItem({label: _('Delete'), click: async () => { menu.append(
const ok = bridge().showConfirmMessageBox(deleteMessage); new MenuItem({
if (!ok) return; label: _("Delete"),
click: async () => {
const ok = bridge().showConfirmMessageBox(deleteMessage);
if (!ok) return;
if (itemType === BaseModel.TYPE_FOLDER) { if (itemType === BaseModel.TYPE_FOLDER) {
await Folder.delete(itemId); await Folder.delete(itemId);
} else if (itemType === BaseModel.TYPE_TAG) { } else if (itemType === BaseModel.TYPE_TAG) {
await Tag.untagAll(itemId); await Tag.untagAll(itemId);
} else if (itemType === BaseModel.TYPE_SEARCH) { } else if (itemType === BaseModel.TYPE_SEARCH) {
this.props.dispatch({ this.props.dispatch({
type: 'SEARCH_DELETE', type: "SEARCH_DELETE",
id: itemId, id: itemId,
}); });
} }
}})) },
})
);
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(new MenuItem({label: _('Rename'), click: async () => { menu.append(
this.props.dispatch({ new MenuItem({
type: 'WINDOW_COMMAND', label: _("Rename"),
name: 'renameFolder', click: async () => {
id: itemId, this.props.dispatch({
}); type: "WINDOW_COMMAND",
}})); name: "renameFolder",
id: itemId,
});
},
})
);
menu.append(new MenuItem({ type: 'separator' })); menu.append(new MenuItem({ type: "separator" }));
const InteropService = require('lib/services/InteropService.js'); const InteropService = require("lib/services/InteropService.js");
menu.append(new MenuItem({label: _('Export'), click: async () => { menu.append(
const ioService = new InteropService(); new MenuItem({
const module = ioService.moduleByFormat_('exporter', 'jex'); label: _("Export"),
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] }); click: async () => {
}})); const ioService = new InteropService();
const module = ioService.moduleByFormat_("exporter", "jex");
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] });
},
})
);
} }
menu.popup(bridge().window()); menu.popup(bridge().window());
@@ -155,21 +171,21 @@ class SideBarComponent extends React.Component {
folderItem_click(folder) { folderItem_click(folder) {
this.props.dispatch({ this.props.dispatch({
type: 'FOLDER_SELECT', type: "FOLDER_SELECT",
id: folder ? folder.id : null, id: folder ? folder.id : null,
}); });
} }
tagItem_click(tag) { tagItem_click(tag) {
this.props.dispatch({ this.props.dispatch({
type: 'TAG_SELECT', type: "TAG_SELECT",
id: tag ? tag.id : null, id: tag ? tag.id : null,
}); });
} }
searchItem_click(search) { searchItem_click(search) {
this.props.dispatch({ this.props.dispatch({
type: 'SEARCH_SELECT', type: "SEARCH_SELECT",
id: search ? search.id : null, id: search ? search.id : null,
}); });
} }
@@ -184,105 +200,180 @@ class SideBarComponent extends React.Component {
if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder); if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder);
const onDragOver = (event, folder) => { const onDragOver = (event, folder) => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault(); if (event.dataTransfer.types.indexOf("text/x-jop-note-ids") >= 0) event.preventDefault();
} };
const onDrop = async (event, folder) => { const onDrop = async (event, folder) => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') < 0) return; if (event.dataTransfer.types.indexOf("text/x-jop-note-ids") < 0) return;
event.preventDefault(); event.preventDefault();
const noteIds = JSON.parse(event.dataTransfer.getData('text/x-jop-note-ids')); const noteIds = JSON.parse(event.dataTransfer.getData("text/x-jop-note-ids"));
for (let i = 0; i < noteIds.length; i++) { for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folder.id); await Note.moveToFolder(noteIds[i], folder.id);
} }
} };
const itemTitle = Folder.displayTitle(folder); const itemTitle = Folder.displayTitle(folder);
return <a return (
className="list-item" <a
onDragOver={(event) => { onDragOver(event, folder) } } className="list-item"
onDrop={(event) => { onDrop(event, folder) } } onDragOver={event => {
href="#" onDragOver(event, folder);
data-id={folder.id} }}
data-type={BaseModel.TYPE_FOLDER} onDrop={event => {
onContextMenu={(event) => this.itemContextMenu(event)} onDrop(event, folder);
key={folder.id} }}
style={style} onClick={() => {this.folderItem_click(folder)}}>{itemTitle} href="#"
</a> data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={event => this.itemContextMenu(event)}
key={folder.id}
style={style}
onClick={() => {
this.folderItem_click(folder);
}}
>
{itemTitle}
</a>
);
} }
tagItem(tag, selected) { tagItem(tag, selected) {
let style = Object.assign({}, this.style().listItem); let style = Object.assign({}, this.style().listItem);
if (selected) style = Object.assign(style, this.style().listItemSelected); if (selected) style = Object.assign(style, this.style().listItemSelected);
return <a className="list-item" href="#" data-id={tag.id} data-type={BaseModel.TYPE_TAG} onContextMenu={(event) => this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{Tag.displayTitle(tag)}</a> return (
<a
className="list-item"
href="#"
data-id={tag.id}
data-type={BaseModel.TYPE_TAG}
onContextMenu={event => this.itemContextMenu(event)}
key={tag.id}
style={style}
onClick={() => {
this.tagItem_click(tag);
}}
>
{Tag.displayTitle(tag)}
</a>
);
} }
searchItem(search, selected) { searchItem(search, selected) {
let style = Object.assign({}, this.style().listItem); let style = Object.assign({}, this.style().listItem);
if (selected) style = Object.assign(style, this.style().listItemSelected); if (selected) style = Object.assign(style, this.style().listItemSelected);
return <a className="list-item" href="#" data-id={search.id} data-type={BaseModel.TYPE_SEARCH} onContextMenu={(event) => this.itemContextMenu(event)} key={search.id} style={style} onClick={() => {this.searchItem_click(search)}}>{search.title}</a> return (
<a
className="list-item"
href="#"
data-id={search.id}
data-type={BaseModel.TYPE_SEARCH}
onContextMenu={event => this.itemContextMenu(event)}
key={search.id}
style={style}
onClick={() => {
this.searchItem_click(search);
}}
>
{search.title}
</a>
);
} }
makeDivider(key) { makeDivider(key) {
return <div style={{height:2, backgroundColor:'blue' }} key={key}></div> return <div style={{ height: 2, backgroundColor: "blue" }} key={key} />;
} }
makeHeader(key, label, iconName) { makeHeader(key, label, iconName) {
const style = this.style().header; const style = this.style().header;
const icon = <i style={{fontSize: style.fontSize * 1.2, marginRight: 5}} className={"fa " + iconName}></i> const icon = <i style={{ fontSize: style.fontSize * 1.2, marginRight: 5 }} className={"fa " + iconName} />;
return <div style={style} key={key}>{icon}{label}</div> return (
<div style={style} key={key}>
{icon}
{label}
</div>
);
} }
synchronizeButton(type) { synchronizeButton(type) {
const style = this.style().button; const style = this.style().button;
const iconName = type === 'sync' ? 'fa-refresh' : 'fa-times'; const iconName = type === "sync" ? "fa-refresh" : "fa-times";
const label = type === 'sync' ? _('Synchronise') : _('Cancel'); const label = type === "sync" ? _("Synchronise") : _("Cancel");
const icon = <i style={{fontSize: style.fontSize, marginRight: 5}} className={"fa " + iconName}></i> const icon = <i style={{ fontSize: style.fontSize, marginRight: 5 }} className={"fa " + iconName} />;
return <a className="synchronize-button" style={style} href="#" key="sync_button" onClick={() => {this.sync_click()}}>{icon}{label}</a> return (
<a
className="synchronize-button"
style={style}
href="#"
key="sync_button"
onClick={() => {
this.sync_click();
}}
>
{icon}
{label}
</a>
);
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
const style = Object.assign({}, this.style().root, this.props.style, { const style = Object.assign({}, this.style().root, this.props.style, {
overflowX: 'hidden', overflowX: "hidden",
overflowY: 'auto', overflowY: "auto",
}); });
let items = []; let items = [];
items.push(this.makeHeader('folderHeader', _('Notebooks'), 'fa-folder-o')); items.push(this.makeHeader("folderHeader", _("Notebooks"), "fa-folder-o"));
if (this.props.folders.length) { if (this.props.folders.length) {
const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this)); const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this));
items = items.concat(folderItems); items = items.concat(folderItems);
} }
items.push(this.makeHeader('tagHeader', _('Tags'), 'fa-tags')); items.push(this.makeHeader("tagHeader", _("Tags"), "fa-tags"));
if (this.props.tags.length) { if (this.props.tags.length) {
const tagItems = shared.renderTags(this.props, this.tagItem.bind(this)); const tagItems = shared.renderTags(this.props, this.tagItem.bind(this));
items.push(<div className="tags" key="tag_items">{tagItems}</div>); items.push(
<div className="tags" key="tag_items">
{tagItems}
</div>
);
} }
if (this.props.searches.length) { if (this.props.searches.length) {
items.push(this.makeHeader('searchHeader', _('Searches'), 'fa-search')); items.push(this.makeHeader("searchHeader", _("Searches"), "fa-search"));
const searchItems = shared.renderSearches(this.props, this.searchItem.bind(this)); const searchItems = shared.renderSearches(this.props, this.searchItem.bind(this));
items.push(<div className="searches" key="search_items">{searchItems}</div>); items.push(
<div className="searches" key="search_items">
{searchItems}
</div>
);
} }
let lines = Synchronizer.reportToLines(this.props.syncReport); let lines = Synchronizer.reportToLines(this.props.syncReport);
const syncReportText = []; const syncReportText = [];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
syncReportText.push(<div key={i}>{lines[i]}</div>); syncReportText.push(
<div key={i} style={{ wordWrap: "break-word", width: "100%" }}>
{lines[i]}
</div>
);
} }
items.push(this.synchronizeButton(this.props.syncStarted ? 'cancel' : 'sync')); items.push(this.synchronizeButton(this.props.syncStarted ? "cancel" : "sync"));
items.push(<div style={this.style().syncReport} key='sync_report'>{syncReportText}</div>); items.push(
<div style={this.style().syncReport} key="sync_report">
{syncReportText}
</div>
);
return ( return (
<div className="side-bar" style={style}> <div className="side-bar" style={style}>
@@ -290,10 +381,9 @@ class SideBarComponent extends React.Component {
</div> </div>
); );
} }
} }
const mapStateToProps = (state) => { const mapStateToProps = state => {
return { return {
folders: state.folders, folders: state.folders,
tags: state.tags, tags: state.tags,

View File

@@ -53,7 +53,7 @@ shim.isElectron = () => {
// Node requests can go wrong is so many different ways and with so // Node requests can go wrong is so many different ways and with so
// many different error messages... This handler inspects the error // many different error messages... This handler inspects the error
// and decides whether the request can safely be repeated or not. // and decides whether the request can safely be repeated or not.
function fetchRequestCanBeRetried(error) { shim.fetchRequestCanBeRetried = function(error) {
if (!error) return false; if (!error) return false;
// Unfortunately the error 'Network request failed' doesn't have a type // Unfortunately the error 'Network request failed' doesn't have a type
@@ -86,7 +86,7 @@ function fetchRequestCanBeRetried(error) {
if (error.code === "ETIMEDOUT") return true; if (error.code === "ETIMEDOUT") return true;
return false; return false;
} };
shim.fetchWithRetry = async function(fetchFn, options = null) { shim.fetchWithRetry = async function(fetchFn, options = null) {
const { time } = require("lib/time-utils.js"); const { time } = require("lib/time-utils.js");
@@ -101,7 +101,7 @@ shim.fetchWithRetry = async function(fetchFn, options = null) {
const response = await fetchFn(); const response = await fetchFn();
return response; return response;
} catch (error) { } catch (error) {
if (fetchRequestCanBeRetried(error)) { if (shim.fetchRequestCanBeRetried(error)) {
retryCount++; retryCount++;
if (retryCount > options.maxRetry) throw error; if (retryCount > options.maxRetry) throw error;
await time.sleep(retryCount * 3); await time.sleep(retryCount * 3);

View File

@@ -71,9 +71,10 @@ class Synchronizer {
if (report.deleteLocal) lines.push(_("Deleted local items: %d.", report.deleteLocal)); if (report.deleteLocal) lines.push(_("Deleted local items: %d.", report.deleteLocal));
if (report.deleteRemote) lines.push(_("Deleted remote items: %d.", report.deleteRemote)); if (report.deleteRemote) lines.push(_("Deleted remote items: %d.", report.deleteRemote));
if (report.fetchingTotal && report.fetchingProcessed) lines.push(_("Fetched items: %d/%d.", report.fetchingProcessed, report.fetchingTotal)); if (report.fetchingTotal && report.fetchingProcessed) lines.push(_("Fetched items: %d/%d.", report.fetchingProcessed, report.fetchingTotal));
if (!report.completedTime && report.state) lines.push(_('State: "%s".', report.state)); if (!report.completedTime && report.state) lines.push(_('State: "%s".', Synchronizer.stateToLabel(report.state)));
if (report.cancelling && !report.completedTime) lines.push(_("Cancelling...")); if (report.cancelling && !report.completedTime) lines.push(_("Cancelling..."));
if (report.completedTime) lines.push(_("Completed: %s", time.unixMsToLocalDateTime(report.completedTime))); if (report.completedTime) lines.push(_("Completed: %s", time.unixMsToLocalDateTime(report.completedTime)));
if (report.errors && report.errors.length) lines.push(_("Last error: %s", report.errors[report.errors.length - 1].toString().substr(0, 500)));
return lines; return lines;
} }
@@ -157,6 +158,12 @@ class Synchronizer {
return this.cancelling_; return this.cancelling_;
} }
static stateToLabel(state) {
if (state === "idle") return _("Idle");
if (state === "in_progress") return _("In progress");
return state;
}
// Synchronisation is done in three major steps: // Synchronisation is done in three major steps:
// //
// 1. UPLOAD: Send to the sync target the items that have changed since the last sync. // 1. UPLOAD: Send to the sync target the items that have changed since the last sync.
@@ -603,7 +610,9 @@ class Synchronizer {
this.logger().info(error.message); this.logger().info(error.message);
} else { } else {
this.logger().error(error); this.logger().error(error);
this.progressReport_.errors.push(error);
// Don't save to the report errors that are due to things like temporary network errors or timeout.
if (!shim.fetchRequestCanBeRetried(error)) this.progressReport_.errors.push(error);
} }
} }