You've already forked joplin
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:
@@ -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,
|
||||||
|
@@ -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);
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user