Use of async/await

This commit is contained in:
Benoit Schweblin 2018-05-13 13:27:33 +00:00
parent e971082768
commit 597c747b00
69 changed files with 4194 additions and 4208 deletions

View File

@ -92,20 +92,19 @@ export default {
return !!this.$store.getters['modal/config']; return !!this.$store.getters['modal/config'];
}, },
}, },
created() { async created() {
syncSvc.init() try {
.then(() => { await syncSvc.init();
networkSvc.init(); await networkSvc.init();
sponsorSvc.init(); await sponsorSvc.init();
this.ready = true; this.ready = true;
tempFileSvc.setReady(); tempFileSvc.setReady();
}) } catch (err) {
.catch((err) => {
if (err && err.message !== 'reload') { if (err && err.message !== 'reload') {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); this.$store.dispatch('notification/error', err);
} }
}); }
}, },
}; };
</script> </script>

View File

@ -97,29 +97,36 @@ export default {
} }
return true; return true;
}, },
submitNewChild(cancel) { async submitNewChild(cancel) {
const { newChildNode } = this.$store.state.explorer; const { newChildNode } = this.$store.state.explorer;
if (!cancel && !newChildNode.isNil && newChildNode.item.name) { if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
try {
if (newChildNode.isFolder) { if (newChildNode.isFolder) {
fileSvc.storeItem(newChildNode.item) const item = await fileSvc.storeItem(newChildNode.item);
.then(item => this.select(item.id), () => { /* cancel */ }); this.select(item.id);
} else { } else {
fileSvc.createFile(newChildNode.item) const item = await fileSvc.createFile(newChildNode.item);
.then(item => this.select(item.id), () => { /* cancel */ }); this.select(item.id);
}
} catch (e) {
// Cancel
} }
} }
this.$store.commit('explorer/setNewItem', null); this.$store.commit('explorer/setNewItem', null);
}, },
submitEdit(cancel) { async submitEdit(cancel) {
const { item } = this.$store.getters['explorer/editingNode']; const { item } = this.$store.getters['explorer/editingNode'];
const value = this.editingValue; const value = this.editingValue;
this.setEditingId(null); this.setEditingId(null);
if (!cancel && item.id && value) { if (!cancel && item.id && value) {
fileSvc.storeItem({ try {
await fileSvc.storeItem({
...item, ...item,
name: value, name: value,
}) });
.catch(() => { /* cancel */ }); } catch (e) {
// Cancel
}
} }
}, },
setDragSourceId(evt) { setDragSourceId(evt) {
@ -140,22 +147,17 @@ export default {
&& !targetNode.isNil && !targetNode.isNil
&& sourceNode.item.id !== targetNode.item.id && sourceNode.item.id !== targetNode.item.id
) { ) {
const patch = { fileSvc.storeItem({
id: sourceNode.item.id, ...sourceNode.item,
parentId: targetNode.item.id, parentId: targetNode.item.id,
}; });
if (sourceNode.isFolder) {
this.$store.commit('folder/patchItem', patch);
} else {
this.$store.commit('file/patchItem', patch);
}
} }
}, },
onContextMenu(evt) { async onContextMenu(evt) {
if (this.select(undefined, false)) { if (this.select(undefined, false)) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
this.$store.dispatch('contextMenu/open', { const item = await this.$store.dispatch('contextMenu/open', {
coordinates: { coordinates: {
left: evt.clientX, left: evt.clientX,
top: evt.clientY, top: evt.clientY,
@ -178,8 +180,8 @@ export default {
name: 'Delete', name: 'Delete',
perform: () => explorerSvc.deleteItem(), perform: () => explorerSvc.deleteItem(),
}], }],
}) });
.then(item => item.perform()); item.perform();
} }
}, },
}, },

View File

@ -175,10 +175,6 @@ export default {
background-color: rgba(160, 160, 160, 0.5); background-color: rgba(160, 160, 160, 0.5);
overflow: auto; overflow: auto;
hr {
margin: 0.5em 0;
}
p { p {
line-height: 1.5; line-height: 1.5;
} }

View File

@ -192,7 +192,7 @@ export default {
editorSvc.pagedownEditor.uiManager.doClick(name); editorSvc.pagedownEditor.uiManager.doClick(name);
} }
}, },
editTitle(toggle) { async editTitle(toggle) {
this.titleFocus = toggle; this.titleFocus = toggle;
if (toggle) { if (toggle) {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length); this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
@ -200,11 +200,14 @@ export default {
const title = this.title.trim(); const title = this.title.trim();
this.title = this.$store.getters['file/current'].name; this.title = this.$store.getters['file/current'].name;
if (title) { if (title) {
fileSvc.storeItem({ try {
await fileSvc.storeItem({
...this.$store.getters['file/current'], ...this.$store.getters['file/current'],
name: title, name: title,
}) });
.catch(() => { /* Cancel */ }); } catch (e) {
// Cancel
}
} }
} }
}, },

View File

@ -47,12 +47,13 @@ export default {
...mapMutations('discussion', [ ...mapMutations('discussion', [
'setIsCommenting', 'setIsCommenting',
]), ]),
removeComment() { async removeComment() {
this.$store.dispatch('modal/commentDeletion') try {
.then( await this.$store.dispatch('modal/commentDeletion');
() => this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment }), this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
() => { /* Cancel */ }, } catch (e) {
); // Cancel
}
}, },
}, },
mounted() { mounted() {

View File

@ -93,14 +93,15 @@ export default {
.start(); .start();
} }
}, },
removeDiscussion() { async removeDiscussion() {
this.$store.dispatch('modal/discussionDeletion') try {
.then( await this.$store.dispatch('modal/discussionDeletion');
() => this.$store.dispatch('discussion/cleanCurrentFile', { this.$store.dispatch('discussion/cleanCurrentFile', {
filterDiscussion: this.currentDiscussion, filterDiscussion: this.currentDiscussion,
}), });
() => { /* Cancel */ }, } catch (e) {
); // Cancel
}
}, },
}, },
}; };

View File

@ -96,12 +96,13 @@ export default {
...mapMutations('content', [ ...mapMutations('content', [
'setRevisionContent', 'setRevisionContent',
]), ]),
signin() { async signin() {
return googleHelper.signin() try {
.then( await googleHelper.signin();
() => syncSvc.requestSync(), syncSvc.requestSync();
() => { /* Cancel */ }, } catch (e) {
); // Cancel
}
}, },
close() { close() {
this.$store.dispatch('data/setSideBarPanel', 'menu'); this.$store.dispatch('data/setSideBarPanel', 'menu');
@ -117,10 +118,15 @@ export default {
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
this.$store.dispatch( this.$store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => Promise.resolve() async () => {
.then(() => this.workspaceProvider try {
.getRevisionContent(syncToken, currentFile.id, revision.id)) const content = await this.workspaceProvider
.then(resolve, reject), .getRevisionContent(syncToken, currentFile.id, revision.id);
resolve(content);
} catch (e) {
reject(e);
}
},
); );
}); });
revisionContentPromises[revision.id] = revisionContentPromise; revisionContentPromises[revision.id] = revisionContentPromise;
@ -181,9 +187,15 @@ export default {
revisionsPromise = new Promise((resolve, reject) => { revisionsPromise = new Promise((resolve, reject) => {
this.$store.dispatch( this.$store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => Promise.resolve() async () => {
.then(() => this.workspaceProvider.listRevisions(syncToken, currentFile.id)) try {
.then(resolve, reject), const revisions = await this.workspaceProvider
.listRevisions(syncToken, currentFile.id);
resolve(revisions);
} catch (e) {
reject(e);
}
},
); );
}) })
.catch(() => { .catch(() => {

View File

@ -104,16 +104,20 @@ export default {
...mapActions('data', { ...mapActions('data', {
setPanel: 'setSideBarPanel', setPanel: 'setSideBarPanel',
}), }),
signin() { async signin() {
return googleHelper.signin() try {
.then( await googleHelper.signin();
() => syncSvc.requestSync(), syncSvc.requestSync();
() => { /* Cancel */ }, } catch (e) {
); // Cancel
}
}, },
fileProperties() { async fileProperties() {
return this.$store.dispatch('modal/open', 'fileProperties') try {
.catch(() => { /* Cancel */ }); await this.$store.dispatch('modal/open', 'fileProperties');
} catch (e) {
// Cancel
}
}, },
print() { print() {
window.print(); window.print();

View File

@ -78,29 +78,33 @@ export default {
document.body.removeChild(iframeElt); document.body.removeChild(iframeElt);
}, 60000); }, 60000);
}, },
settings() { async settings() {
return this.$store.dispatch('modal/open', 'settings') try {
.then( const settings = await this.$store.dispatch('modal/open', 'settings');
settings => this.$store.dispatch('data/setSettings', settings), this.$store.dispatch('data/setSettings', settings);
() => { /* Cancel */ }, } catch (e) {
); // Cancel
}
}, },
templates() { async templates() {
return this.$store.dispatch('modal/open', 'templates') try {
.then( const { templates } = await this.$store.dispatch('modal/open', 'templates');
({ templates }) => this.$store.dispatch('data/setTemplates', templates), this.$store.dispatch('data/setTemplates', templates);
() => { /* Cancel */ }, } catch (e) {
); // Cancel
}
}, },
reset() { async reset() {
return this.$store.dispatch('modal/reset') try {
.then(() => { await this.$store.dispatch('modal/reset');
window.location.href = '#reset=true'; window.location.href = '#reset=true';
window.location.reload(); window.location.reload();
}); } catch (e) {
// Cancel
}
}, },
about() { about() {
return this.$store.dispatch('modal/open', 'about'); this.$store.dispatch('modal/open', 'about');
}, },
}, },
}; };

View File

@ -118,12 +118,15 @@ const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
.filter(token => filter(token)) .filter(token => filter(token))
.sort((token1, token2) => token1.name.localeCompare(token2.name)); .sort((token1, token2) => token1.name.localeCompare(token2.name));
const openPublishModal = (token, type) => store.dispatch('modal/open', { const publishModalOpener = type => async (token) => {
try {
const publishLocation = await store.dispatch('modal/open', {
type, type,
token, token,
}).then(publishLocation => publishSvc.createPublishLocation(publishLocation)); });
publishSvc.createPublishLocation(publishLocation);
const onCancel = () => {}; } catch (e) { /* cancel */ }
};
export default { export default {
components: { components: {
@ -178,74 +181,48 @@ export default {
managePublish() { managePublish() {
return this.$store.dispatch('modal/open', 'publishManagement'); return this.$store.dispatch('modal/open', 'publishManagement');
}, },
addGoogleDriveAccount() { async addGoogleDriveAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'googleDriveAccount', await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess), await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(onCancel);
}, },
addDropboxAccount() { async addDropboxAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'dropboxAccount', await this.$store.dispatch('modal/open', { type: 'dropboxAccount' });
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess), await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(onCancel);
}, },
addGithubAccount() { async addGithubAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'githubAccount', await this.$store.dispatch('modal/open', { type: 'githubAccount' });
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess), await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
}) } catch (e) { /* cancel */ }
.catch(onCancel);
}, },
addWordpressAccount() { async addWordpressAccount() {
return wordpressHelper.addAccount() try {
.catch(onCancel); await wordpressHelper.addAccount();
} catch (e) { /* cancel */ }
}, },
addBloggerAccount() { async addBloggerAccount() {
return googleHelper.addBloggerAccount() try {
.catch(onCancel); await googleHelper.addBloggerAccount();
} catch (e) { /* cancel */ }
}, },
addZendeskAccount() { async addZendeskAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'zendeskAccount', const { subdomain, clientId } = await this.$store.dispatch('modal/open', { type: 'zendeskAccount' });
onResolve: ({ subdomain, clientId }) => zendeskHelper.addAccount(subdomain, clientId), await zendeskHelper.addAccount(subdomain, clientId);
}) } catch (e) { /* cancel */ }
.catch(onCancel);
},
publishGoogleDrive(token) {
return openPublishModal(token, 'googleDrivePublish')
.catch(onCancel);
},
publishDropbox(token) {
return openPublishModal(token, 'dropboxPublish')
.catch(onCancel);
},
publishGithub(token) {
return openPublishModal(token, 'githubPublish')
.catch(onCancel);
},
publishGist(token) {
return openPublishModal(token, 'gistPublish')
.catch(onCancel);
},
publishWordpress(token) {
return openPublishModal(token, 'wordpressPublish')
.catch(onCancel);
},
publishBlogger(token) {
return openPublishModal(token, 'bloggerPublish')
.catch(onCancel);
},
publishBloggerPage(token) {
return openPublishModal(token, 'bloggerPagePublish')
.catch(onCancel);
},
publishZendesk(token) {
return openPublishModal(token, 'zendeskPublish')
.catch(onCancel);
}, },
publishGoogleDrive: publishModalOpener('googleDrivePublish'),
publishDropbox: publishModalOpener('dropboxPublish'),
publishGithub: publishModalOpener('githubPublish'),
publishGist: publishModalOpener('gistPublish'),
publishWordpress: publishModalOpener('wordpressPublish'),
publishBlogger: publishModalOpener('bloggerPublish'),
publishBloggerPage: publishModalOpener('bloggerPagePublish'),
publishZendesk: publishModalOpener('zendeskPublish'),
}, },
}; };
</script> </script>

View File

@ -101,8 +101,6 @@ const openSyncModal = (token, type) => store.dispatch('modal/open', {
token, token,
}).then(syncLocation => syncSvc.createSyncLocation(syncLocation)); }).then(syncLocation => syncSvc.createSyncLocation(syncLocation));
const onCancel = () => {};
export default { export default {
components: { components: {
MenuEntry, MenuEntry,
@ -147,66 +145,79 @@ export default {
manageSync() { manageSync() {
return this.$store.dispatch('modal/open', 'syncManagement'); return this.$store.dispatch('modal/open', 'syncManagement');
}, },
addGoogleDriveAccount() { async addGoogleDriveAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'googleDriveAccount', await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess), await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(onCancel);
}, },
addDropboxAccount() { async addDropboxAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'dropboxAccount', await this.$store.dispatch('modal/open', { type: 'dropboxAccount' });
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess), await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(onCancel);
}, },
addGithubAccount() { async addGithubAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'githubAccount', await this.$store.dispatch('modal/open', { type: 'githubAccount' });
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess), await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
}) } catch (e) { /* cancel */ }
.catch(onCancel);
}, },
openGoogleDrive(token) { async openGoogleDrive(token) {
return googleHelper.openPicker(token, 'doc') const files = await googleHelper.openPicker(token, 'doc');
.then(files => this.$store.dispatch( this.$store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => googleDriveProvider.openFiles(token, files), () => googleDriveProvider.openFiles(token, files),
)); );
}, },
openDropbox(token) { async openDropbox(token) {
return dropboxHelper.openChooser(token) const paths = await dropboxHelper.openChooser(token);
.then(paths => this.$store.dispatch( this.$store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => dropboxProvider.openFiles(token, paths), () => dropboxProvider.openFiles(token, paths),
)); );
}, },
saveGoogleDrive(token) { async saveGoogleDrive(token) {
return openSyncModal(token, 'googleDriveSave') try {
.catch(onCancel); await openSyncModal(token, 'googleDriveSave');
} catch (e) {
// Cancel
}
}, },
saveDropbox(token) { async saveDropbox(token) {
return openSyncModal(token, 'dropboxSave') try {
.catch(onCancel); await openSyncModal(token, 'dropboxSave');
} catch (e) {
// Cancel
}
}, },
openGithub(token) { async openGithub(token) {
return store.dispatch('modal/open', { try {
const syncLocation = await store.dispatch('modal/open', {
type: 'githubOpen', type: 'githubOpen',
token, token,
}) });
.then(syncLocation => this.$store.dispatch( this.$store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => githubProvider.openFile(token, syncLocation), () => githubProvider.openFile(token, syncLocation),
)); );
} catch (e) {
// Cancel
}
}, },
saveGithub(token) { async saveGithub(token) {
return openSyncModal(token, 'githubSave') try {
.catch(onCancel); await openSyncModal(token, 'githubSave');
} catch (e) {
// Cancel
}
}, },
saveGist(token) { async saveGist(token) {
return openSyncModal(token, 'gistSync') try {
.catch(onCancel); await openSyncModal(token, 'gistSync');
} catch (e) {
// Cancel
}
}, },
}, },
}; };

View File

@ -31,8 +31,6 @@ import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
const onCancel = () => {};
export default { export default {
components: { components: {
MenuEntry, MenuEntry,
@ -46,28 +44,37 @@ export default {
]), ]),
}, },
methods: { methods: {
addCouchdbWorkspace() { async addCouchdbWorkspace() {
return this.$store.dispatch('modal/open', { try {
this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace', type: 'couchdbWorkspace',
}) });
.catch(onCancel); } catch (e) {
// Cancel
}
}, },
addGithubWorkspace() { async addGithubWorkspace() {
return this.$store.dispatch('modal/open', { try {
this.$store.dispatch('modal/open', {
type: 'githubWorkspace', type: 'githubWorkspace',
}) });
.catch(onCancel); } catch (e) {
// Cancel
}
}, },
addGoogleDriveWorkspace() { async addGoogleDriveWorkspace() {
return googleHelper.addDriveAccount(true) try {
.then(token => this.$store.dispatch('modal/open', { const token = await googleHelper.addDriveAccount(true);
this.$store.dispatch('modal/open', {
type: 'googleDriveWorkspace', type: 'googleDriveWorkspace',
token, token,
})) });
.catch(onCancel); } catch (e) {
// Cancel
}
}, },
manageWorkspaces() { manageWorkspaces() {
return this.$store.dispatch('modal/open', 'workspaceManagement'); this.$store.dispatch('modal/open', 'workspaceManagement');
}, },
}, },
}; };

View File

@ -2,7 +2,7 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About"> <modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content"> <div class="modal__content">
<div class="logo-background"></div> <div class="logo-background"></div>
<small>v{{version}}<br>© 2013-2018 Dock5 Software</small> <small>© 2013-2018 Dock5 Software<br>v{{version}}</small>
<hr> <hr>
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a> StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
<br> <br>
@ -59,11 +59,12 @@ export default {
.logo-background { .logo-background {
height: 75px; height: 75px;
margin: 0.5rem 0; margin: 0;
} }
small { small {
display: block; display: block;
font-size: 0.75em;
} }
hr { hr {

View File

@ -41,6 +41,9 @@
</form-entry> </form-entry>
<form-entry label="Status"> <form-entry label="Status">
<input slot="field" class="textfield" type="text" v-model.trim="status" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="status" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> draft
</div>
</form-entry> </form-entry>
<form-entry label="Date" info="YYYY-MM-DD"> <form-entry label="Date" info="YYYY-MM-DD">
<input slot="field" class="textfield" type="text" v-model.trim="date" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="date" @keydown.enter="resolve()">

View File

@ -37,12 +37,13 @@ export default modalTemplate({
let timeoutId; let timeoutId;
this.$watch('selectedTemplate', (selectedTemplate) => { this.$watch('selectedTemplate', (selectedTemplate) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(async () => {
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate]) const html = await exportSvc.applyTemplate(
.then((html) => { currentFile.id,
this.allTemplates[selectedTemplate],
);
this.result = html; this.result = html;
});
}, 10); }, 10);
}, { }, {
immediate: true, immediate: true,

View File

@ -61,15 +61,17 @@ export default modalTemplate({
addGooglePhotosAccount() { addGooglePhotosAccount() {
return googleHelper.addPhotosAccount(); return googleHelper.addPhotosAccount();
}, },
openGooglePhotos(token) { async openGooglePhotos(token) {
const { callback } = this.config; const { callback } = this.config;
this.config.reject(); this.config.reject();
googleHelper.openPicker(token, 'img') const res = await googleHelper.openPicker(token, 'img');
.then(res => res[0] && this.$store.dispatch('modal/open', { if (res[0]) {
this.$store.dispatch('modal/open', {
type: 'googlePhoto', type: 'googlePhoto',
url: res[0].url, url: res[0].url,
callback, callback,
})); });
}
}, },
}, },
}); });

View File

@ -38,19 +38,20 @@ export default modalTemplate({
selectedFormat: 'pandocExportFormat', selectedFormat: 'pandocExportFormat',
}, },
methods: { methods: {
resolve() { async resolve() {
this.config.resolve(); this.config.resolve();
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
const currentContent = this.$store.getters['content/current']; const currentContent = this.$store.getters['content/current'];
const { selectedFormat } = this; const { selectedFormat } = this;
this.$store.dispatch('queue/enqueue', () => Promise.all([ const [sponsorToken, token] = await this.$store.dispatch('queue/enqueue', () => Promise.all([
Promise.resolve().then(() => { Promise.resolve().then(() => {
const sponsorToken = this.$store.getters['workspace/sponsorToken']; const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
return sponsorToken && googleHelper.refreshToken(sponsorToken); return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}), }),
sponsorSvc.getToken(), sponsorSvc.getToken(),
]) ]));
.then(([sponsorToken, token]) => networkSvc.request({ try {
const { body } = await networkSvc.request({
method: 'POST', method: 'POST',
url: 'pandocExport', url: 'pandocExport',
params: { params: {
@ -63,20 +64,16 @@ export default modalTemplate({
body: JSON.stringify(editorSvc.getPandocAst()), body: JSON.stringify(editorSvc.getPandocAst()),
blob: true, blob: true,
timeout: 60000, timeout: 60000,
}) });
.then((res) => { FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
FileSaver.saveAs(res.body, `${currentFile.name}.${selectedFormat}`); } catch (err) {
}, (err) => { if (err.status === 401) {
if (err.status !== 401) { this.$store.dispatch('modal/sponsorOnly');
throw err; } else {
}
this.$store.dispatch('modal/sponsorOnly')
.catch(() => { /* Cancel */ });
}))
.catch((err) => {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); this.$store.dispatch('notification/error', err);
})); }
}
}, },
}, },
}); });

View File

@ -33,13 +33,14 @@ export default modalTemplate({
selectedTemplate: 'pdfExportTemplate', selectedTemplate: 'pdfExportTemplate',
}, },
methods: { methods: {
resolve() { async resolve() {
this.config.resolve(); this.config.resolve();
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
this.$store.dispatch('queue/enqueue', () => Promise.all([ const [sponsorToken, token, html] = await this.$store
.dispatch('queue/enqueue', () => Promise.all([
Promise.resolve().then(() => { Promise.resolve().then(() => {
const sponsorToken = this.$store.getters['workspace/sponsorToken']; const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
return sponsorToken && googleHelper.refreshToken(sponsorToken); return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}), }),
sponsorSvc.getToken(), sponsorSvc.getToken(),
exportSvc.applyTemplate( exportSvc.applyTemplate(
@ -47,8 +48,9 @@ export default modalTemplate({
this.allTemplates[this.selectedTemplate], this.allTemplates[this.selectedTemplate],
true, true,
), ),
]) ]));
.then(([sponsorToken, token, html]) => networkSvc.request({ try {
const { body } = await networkSvc.request({
method: 'POST', method: 'POST',
url: 'pdfExport', url: 'pdfExport',
params: { params: {
@ -59,20 +61,16 @@ export default modalTemplate({
body: html, body: html,
blob: true, blob: true,
timeout: 60000, timeout: 60000,
}) });
.then((res) => { FileSaver.saveAs(body, `${currentFile.name}.pdf`);
FileSaver.saveAs(res.body, `${currentFile.name}.pdf`); } catch (err) {
}, (err) => { if (err.status === 401) {
if (err.status !== 401) { this.$store.dispatch('modal/sponsorOnly');
throw err; } else {
}
this.$store.dispatch('modal/sponsorOnly')
.catch(() => { /* Cancel */ });
}))
.catch((err) => {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); this.$store.dispatch('notification/error', err);
})); }
}
}, },
}, },
}); });

View File

@ -75,12 +75,13 @@ export default {
} }
this.editedId = null; this.editedId = null;
}, },
remove(id) { async remove(id) {
return this.$store.dispatch('modal/removeWorkspace') try {
.then( await this.$store.dispatch('modal/removeWorkspace');
() => localDbSvc.removeWorkspace(id), localDbSvc.removeWorkspace(id);
() => { /* Cancel */ }, } catch (e) {
); // Cancel
}
}, },
}, },
}; };

View File

@ -29,20 +29,18 @@ export default {
}, },
}, },
methods: { methods: {
sponsor() { async sponsor() {
Promise.resolve() try {
.then(() => !this.$store.getters['workspace/sponsorToken'] && if (!this.$store.getters['workspace/sponsorToken']) {
// If user has to sign in // User has to sign in
this.$store.dispatch('modal/signInForSponsorship', { await this.$store.dispatch('modal/signInForSponsorship');
onResolve: () => googleHelper.signin() await googleHelper.signin();
.then(() => syncSvc.requestSync()), syncSvc.requestSync();
}))
.then(() => {
if (!this.$store.getters.isSponsor) {
this.$store.dispatch('modal/open', 'sponsor');
} }
}) if (!this.$store.getters.isSponsor) {
.catch(() => { /* Cancel */ }); await this.$store.dispatch('modal/open', 'sponsor');
}
} catch (e) { /* cancel */ }
}, },
}, },
}; };

View File

@ -63,17 +63,15 @@ export default (desc) => {
return sortedTemplates; return sortedTemplates;
}; };
// Make use of `function` to have `this` bound to the component // Make use of `function` to have `this` bound to the component
component.methods.configureTemplates = function () { // eslint-disable-line func-names component.methods.configureTemplates = async function () { // eslint-disable-line func-names
store.dispatch('modal/open', { const { templates, selectedId } = await store.dispatch('modal/open', {
type: 'templates', type: 'templates',
selectedId: this.selectedTemplate, selectedId: this.selectedTemplate,
}) });
.then(({ templates, selectedId }) => {
store.dispatch('data/setTemplates', templates); store.dispatch('data/setTemplates', templates);
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
[id]: selectedId, [id]: selectedId,
}); });
});
}; };
} }
}); });

View File

@ -18,14 +18,12 @@ OfflinePluginRuntime.install({
// Tells to new SW to take control immediately // Tells to new SW to take control immediately
OfflinePluginRuntime.applyUpdate(); OfflinePluginRuntime.applyUpdate();
}, },
onUpdated: () => { onUpdated: async () => {
if (!store.state.light) { if (!store.state.light) {
localDbSvc.sync() await localDbSvc.sync();
.then(() => {
localStorage.updated = true; localStorage.updated = true;
// Reload the webpage to load into the new version // Reload the webpage to load into the new version
window.location.reload(); window.location.reload();
});
} }
}, },
}); });

View File

@ -49,20 +49,26 @@ export default {
} }
}); });
await utils.awaitSequence(Object.keys(folderNameMap), async externalId => fileSvc.storeItem({ await utils.awaitSequence(
Object.keys(folderNameMap),
async externalId => fileSvc.setOrPatchItem({
id: folderIdMap[externalId], id: folderIdMap[externalId],
type: 'folder', type: 'folder',
name: folderNameMap[externalId], name: folderNameMap[externalId],
parentId: folderIdMap[parentIdMap[externalId]], parentId: folderIdMap[parentIdMap[externalId]],
}, true)); }),
);
await utils.awaitSequence(Object.keys(fileNameMap), async externalId => fileSvc.createFile({ await utils.awaitSequence(
Object.keys(fileNameMap),
async externalId => fileSvc.createFile({
name: fileNameMap[externalId], name: fileNameMap[externalId],
parentId: folderIdMap[parentIdMap[externalId]], parentId: folderIdMap[parentIdMap[externalId]],
text: textMap[externalId], text: textMap[externalId],
properties: propertiesMap[externalId], properties: propertiesMap[externalId],
discussions: discussionsMap[externalId], discussions: discussionsMap[externalId],
comments: commentsMap[externalId], comments: commentsMap[externalId],
}, true)); }, true),
);
}, },
}; };

View File

@ -120,7 +120,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
/** /**
* Refresh the preview with the result of `convert()` * Refresh the preview with the result of `convert()`
*/ */
refreshPreview() { async refreshPreview() {
const sectionDescList = []; const sectionDescList = [];
let sectionPreviewElt; let sectionPreviewElt;
let sectionTocElt; let sectionTocElt;
@ -222,10 +222,10 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
img.onerror = resolve; img.onerror = resolve;
img.src = imgElt.src; img.src = imgElt.src;
})); }));
await Promise.all(loadedPromises);
Promise.all(loadedPromises)
// Debounce if sections have already been measured // Debounce if sections have already been measured
.then(() => this.measureSectionDimensions(!!this.previewCtxMeasured)); this.measureSectionDimensions(!!this.previewCtxMeasured);
}, },
/** /**

View File

@ -15,32 +15,36 @@ export default {
parentId, parentId,
}); });
}, },
deleteItem() { async deleteItem() {
const selectedNode = store.getters['explorer/selectedNode']; const selectedNode = store.getters['explorer/selectedNode'];
if (selectedNode.isNil) { if (selectedNode.isNil) {
return Promise.resolve(); return;
} }
if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') { if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
return store.dispatch('modal/trashDeletion').catch(() => { /* Cancel */ }); try {
await store.dispatch('modal/trashDeletion');
} catch (e) {
// Cancel
}
return;
} }
// See if we have a dialog to show // See if we have a confirmation dialog to show
let modalAction;
let moveToTrash = true; let moveToTrash = true;
try {
if (selectedNode.isTemp) { if (selectedNode.isTemp) {
modalAction = 'modal/tempFolderDeletion'; await store.dispatch('modal/tempFolderDeletion', selectedNode.item);
moveToTrash = false; moveToTrash = false;
} else if (selectedNode.item.parentId === 'temp') { } else if (selectedNode.item.parentId === 'temp') {
modalAction = 'modal/tempFileDeletion'; await store.dispatch('modal/tempFileDeletion', selectedNode.item);
moveToTrash = false; moveToTrash = false;
} else if (selectedNode.isFolder) { } else if (selectedNode.isFolder) {
modalAction = 'modal/folderDeletion'; await store.dispatch('modal/folderDeletion', selectedNode.item);
}
} catch (e) {
return; // cancel
} }
return (modalAction
? store.dispatch(modalAction, selectedNode.item)
: Promise.resolve())
.then(() => {
const deleteFile = (id) => { const deleteFile = (id) => {
if (moveToTrash) { if (moveToTrash) {
store.commit('file/patchItem', { store.commit('file/patchItem', {
@ -80,6 +84,5 @@ export default {
}); });
} }
} }
}, () => { /* Cancel */ });
}, },
}; };

View File

@ -42,13 +42,12 @@ export default {
/** /**
* Apply the template to the file content * Apply the template to the file content
*/ */
applyTemplate(fileId, template = { async applyTemplate(fileId, template = {
value: '{{{files.0.content.text}}}', value: '{{{files.0.content.text}}}',
helpers: '', helpers: '',
}, pdf = false) { }, pdf = false) {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemMap[fileId];
return localDbSvc.loadItem(`${fileId}/content`) const content = await localDbSvc.loadItem(`${fileId}/content`);
.then((content) => {
const properties = utils.computeProperties(content.properties); const properties = utils.computeProperties(content.properties);
const options = extensionSvc.getOptions(properties); const options = extensionSvc.getOptions(properties);
const converter = markdownConversionSvc.createConverter(options, true); const converter = markdownConversionSvc.createConverter(options, true);
@ -109,19 +108,17 @@ export default {
}); });
worker.postMessage([template.value, view, template.helpers]); worker.postMessage([template.value, view, template.helpers]);
}); });
});
}, },
/** /**
* Export a file to disk. * Export a file to disk.
*/ */
exportToDisk(fileId, type, template) { async exportToDisk(fileId, type, template) {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemMap[fileId];
return this.applyTemplate(fileId, template) const html = await this.applyTemplate(fileId, template);
.then((html) => {
const blob = new Blob([html], { const blob = new Blob([html], {
type: 'text/plain;charset=utf-8', type: 'text/plain;charset=utf-8',
}); });
FileSaver.saveAs(blob, `${file.name}.${type}`); FileSaver.saveAs(blob, `${file.name}.${type}`);
});
}, },
}; };

View File

@ -7,7 +7,7 @@ export default {
/** /**
* Create a file in the store with the specified fields. * Create a file in the store with the specified fields.
*/ */
createFile({ async createFile({
name, name,
parentId, parentId,
text, text,
@ -29,56 +29,55 @@ export default {
discussions: discussions || {}, discussions: discussions || {},
comments: comments || {}, comments: comments || {},
}; };
const nameStripped = file.name !== utils.defaultName && file.name !== name;
// Check if there is a path conflict
const workspaceUniquePaths = store.getters['workspace/hasUniquePaths']; const workspaceUniquePaths = store.getters['workspace/hasUniquePaths'];
let pathConflict;
// Show warning dialogs
if (!background) {
// If name is being stripped
if (file.name !== utils.defaultName && file.name !== name) {
await store.dispatch('modal/stripName', name);
}
// Check if there is already a file with that path
if (workspaceUniquePaths) { if (workspaceUniquePaths) {
const parentPath = store.getters.itemPaths[file.parentId] || ''; const parentPath = store.getters.itemPaths[file.parentId] || '';
const path = parentPath + file.name; const path = parentPath + file.name;
pathConflict = !!store.getters.pathItems[path]; if (store.getters.pathItems[path]) {
await store.dispatch('modal/pathConflict', name);
}
}
} }
// Show warning dialogs and then save in the store // Save file and content in the store
return Promise.resolve()
.then(() => !background && nameStripped && store.dispatch('modal/stripName', name))
.then(() => !background && pathConflict && store.dispatch('modal/pathConflict', name))
.then(() => {
store.commit('content/setItem', content); store.commit('content/setItem', content);
store.commit('file/setItem', file); store.commit('file/setItem', file);
if (workspaceUniquePaths) { if (workspaceUniquePaths) {
this.makePathUnique(id); this.makePathUnique(id);
} }
// Return the new file item
return store.state.file.itemMap[id]; return store.state.file.itemMap[id];
});
}, },
/** /**
* Make sanity checks and then create/update the folder/file in the store. * Make sanity checks and then create/update the folder/file in the store.
*/ */
async storeItem(item, background = false) { async storeItem(item) {
const id = item.id || utils.uid(); const id = item.id || utils.uid();
const sanitizedName = utils.sanitizeName(item.name); const sanitizedName = utils.sanitizeName(item.name);
if (item.type === 'folder' && forbiddenFolderNameMatcher.exec(sanitizedName)) { if (item.type === 'folder' && forbiddenFolderNameMatcher.exec(sanitizedName)) {
if (background) {
return null;
}
await store.dispatch('modal/unauthorizedName', item.name); await store.dispatch('modal/unauthorizedName', item.name);
throw new Error('Unauthorized name.'); throw new Error('Unauthorized name.');
} }
const workspaceUniquePaths = store.getters['workspace/hasUniquePaths'];
// Show warning dialogs // Show warning dialogs
if (!background) {
// If name has been stripped // If name has been stripped
if (sanitizedName !== utils.defaultName && sanitizedName !== item.name) { if (sanitizedName !== utils.defaultName && sanitizedName !== item.name) {
await store.dispatch('modal/stripName', item.name); await store.dispatch('modal/stripName', item.name);
} }
// Check if there is a path conflict // Check if there is a path conflict
if (workspaceUniquePaths) { if (store.getters['workspace/hasUniquePaths']) {
const parentPath = store.getters.itemPaths[item.parentId] || ''; const parentPath = store.getters.itemPaths[item.parentId] || '';
const path = parentPath + sanitizedName; const path = parentPath + sanitizedName;
const pathItems = store.getters.pathItems[path] || []; const pathItems = store.getters.pathItems[path] || [];
@ -86,20 +85,43 @@ export default {
await store.dispatch('modal/pathConflict', item.name); await store.dispatch('modal/pathConflict', item.name);
} }
} }
return this.setOrPatchItem({
...item,
id,
});
},
/**
* Create/update the folder/file in the store and make sure its path is unique.
*/
setOrPatchItem(patch) {
const item = {
...store.getters.allItemMap[patch.id] || patch,
};
if (!item.id) {
return null;
}
if (patch.parentId !== undefined) {
item.parentId = patch.parentId || null;
}
if (patch.name) {
const sanitizedName = utils.sanitizeName(patch.name);
if (item.type !== 'folder' || !forbiddenFolderNameMatcher.exec(sanitizedName)) {
item.name = sanitizedName;
}
} }
// Save item in the store // Save item in the store
store.commit(`${item.type}/setItem`, { store.commit(`${item.type}/setItem`, item);
id,
parentId: item.parentId || null,
name: sanitizedName,
});
// Ensure path uniqueness // Ensure path uniqueness
if (workspaceUniquePaths) { if (store.getters['workspace/hasUniquePaths']) {
this.makePathUnique(id); this.makePathUnique(item.id);
} }
return store.getters.allItemMap[id];
return store.getters.allItemMap[item.id];
}, },
/** /**

View File

@ -136,7 +136,7 @@ const localDbSvc = {
* localDb will be finished. Effectively, open a transaction, then read and apply all changes * localDb will be finished. Effectively, open a transaction, then read and apply all changes
* from the DB since the previous transaction, then write all the changes from the store. * from the DB since the previous transaction, then write all the changes from the store.
*/ */
sync() { async sync() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Create the DB transaction // Create the DB transaction
this.connection.createTx((tx) => { this.connection.createTx((tx) => {
@ -275,7 +275,7 @@ const localDbSvc = {
/** /**
* Retrieve an item from the DB and put it in the store. * Retrieve an item from the DB and put it in the store.
*/ */
loadItem(id) { async loadItem(id) {
// Check if item is in the store // Check if item is in the store
const itemInStore = store.getters.allItemMap[id]; const itemInStore = store.getters.allItemMap[id];
if (itemInStore) { if (itemInStore) {
@ -307,9 +307,8 @@ const localDbSvc = {
/** /**
* Unload from the store contents that haven't been opened recently * Unload from the store contents that haven't been opened recently
*/ */
unloadContents() { async unloadContents() {
return this.sync() await this.sync();
.then(() => {
// Keep only last opened files in memory // Keep only last opened files in memory
const lastOpenedFileIdSet = new Set(store.getters['data/lastOpenedIds']); const lastOpenedFileIdSet = new Set(store.getters['data/lastOpenedIds']);
Object.keys(contentTypes).forEach((type) => { Object.keys(contentTypes).forEach((type) => {
@ -321,58 +320,50 @@ const localDbSvc = {
} }
}); });
}); });
});
}, },
/** /**
* Drop the database and clean the localStorage for the specified workspaceId. * Drop the database and clean the localStorage for the specified workspaceId.
*/ */
removeWorkspace(id) { async removeWorkspace(id) {
const workspaces = { const workspaces = {
...store.getters['data/workspaces'], ...store.getters['data/workspaces'],
}; };
delete workspaces[id]; delete workspaces[id];
store.dispatch('data/setWorkspaces', workspaces); store.dispatch('data/setWorkspaces', workspaces);
this.syncLocalStorage(); this.syncLocalStorage();
return new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const dbName = getDbName(id); const dbName = getDbName(id);
const request = indexedDB.deleteDatabase(dbName); const request = indexedDB.deleteDatabase(dbName);
request.onerror = reject; request.onerror = reject;
request.onsuccess = resolve; request.onsuccess = resolve;
}) });
.then(() => {
localStorage.removeItem(`${id}/lastSyncActivity`); localStorage.removeItem(`${id}/lastSyncActivity`);
localStorage.removeItem(`${id}/lastWindowFocus`); localStorage.removeItem(`${id}/lastWindowFocus`);
});
}, },
/** /**
* Create the connection and start syncing. * Create the connection and start syncing.
*/ */
init() { async init() {
return Promise.resolve()
.then(() => {
// Reset the app if reset flag was passed // Reset the app if reset flag was passed
if (resetApp) { if (resetApp) {
return Promise.all(Object.keys(store.getters['data/workspaces']) await Promise.all(Object.keys(store.getters['data/workspaces'])
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId))) .map(workspaceId => localDbSvc.removeWorkspace(workspaceId)));
.then(() => utils.localStorageDataIds.forEach((id) => { utils.localStorageDataIds.forEach((id) => {
// Clean data stored in localStorage // Clean data stored in localStorage
localStorage.removeItem(`data/${id}`); localStorage.removeItem(`data/${id}`);
})) });
.then(() => {
window.location.reload(); window.location.reload();
throw new Error('reload'); throw new Error('reload');
});
} }
// Create the connection // Create the connection
this.connection = new Connection(); this.connection = new Connection();
// Load the DB // Load the DB
return localDbSvc.sync(); await localDbSvc.sync();
})
.then(() => {
// If exportWorkspace parameter was provided // If exportWorkspace parameter was provided
if (exportWorkspace) { if (exportWorkspace) {
const backup = JSON.stringify(store.getters.allItemMap); const backup = JSON.stringify(store.getters.allItemMap);
@ -427,7 +418,7 @@ const localDbSvc = {
// watch current file changing // watch current file changing
store.watch( store.watch(
() => store.getters['file/current'].id, () => store.getters['file/current'].id,
() => { async () => {
// See if currentFile is real, ie it has an ID // See if currentFile is real, ie it has an ID
const currentFile = store.getters['file/current']; const currentFile = store.getters['file/current'];
// If current file has no ID, get the most recent file // If current file has no ID, get the most recent file
@ -438,50 +429,43 @@ const localDbSvc = {
store.commit('file/setCurrentId', recentFile.id); store.commit('file/setCurrentId', recentFile.id);
} else { } else {
// If still no ID, create a new file // If still no ID, create a new file
fileSvc.createFile({ const newFile = await fileSvc.createFile({
name: 'Welcome file', name: 'Welcome file',
text: welcomeFile, text: welcomeFile,
}, true) }, true);
// Set it as the current file // Set it as the current file
.then(newFile => store.commit('file/setCurrentId', newFile.id)); store.commit('file/setCurrentId', newFile.id);
} }
} else { } else {
Promise.resolve() try {
// Load contentState from DB // Load contentState from DB
.then(() => localDbSvc.loadContentState(currentFile.id)) await localDbSvc.loadContentState(currentFile.id);
// Load syncedContent from DB // Load syncedContent from DB
.then(() => localDbSvc.loadSyncedContent(currentFile.id)) await localDbSvc.loadSyncedContent(currentFile.id);
// Load content from DB // Load content from DB
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`)) try {
.then( await localDbSvc.loadItem(`${currentFile.id}/content`);
() => { } catch (err) {
// Set last opened file
store.dispatch('data/setLastOpenedId', currentFile.id);
// Cancel new discussion
store.commit('discussion/setCurrentDiscussionId');
// Open the gutter if file contains discussions
store.commit(
'discussion/setCurrentDiscussionId',
store.getters['discussion/nextDiscussionId'],
);
},
(err) => {
// Failure (content is not available), go back to previous file // Failure (content is not available), go back to previous file
const lastOpenedFile = store.getters['file/lastOpened']; const lastOpenedFile = store.getters['file/lastOpened'];
store.commit('file/setCurrentId', lastOpenedFile.id); store.commit('file/setCurrentId', lastOpenedFile.id);
throw err; throw err;
}, }
) // Set last opened file
.catch((err) => { store.dispatch('data/setLastOpenedId', currentFile.id);
// Cancel new discussion and open the gutter if file contains discussions
store.commit(
'discussion/setCurrentDiscussionId',
store.getters['discussion/nextDiscussionId'],
);
} catch (err) {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err); store.dispatch('notification/error', err);
});
} }
}, { }
immediate: true,
}, },
{ immediate: true },
); );
});
}, },
}; };

View File

@ -7,6 +7,27 @@ const networkTimeout = 30 * 1000; // 30 sec
let isConnectionDown = false; let isConnectionDown = false;
const userInactiveAfter = 2 * 60 * 1000; // 2 minutes const userInactiveAfter = 2 * 60 * 1000; // 2 minutes
function parseHeaders(xhr) {
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
const headers = {};
pairs.forEach((header) => {
const split = header.trim().split(':');
const key = split.shift().trim().toLowerCase();
const value = split.join(':').trim();
headers[key] = value;
});
return headers;
}
function isRetriable(err) {
if (err.status === 403) {
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
}
return err.status === 429 || (err.status >= 500 && err.status < 600);
}
export default { export default {
init() { init() {
// Keep track of the last user activity // Keep track of the last user activity
@ -31,37 +52,34 @@ export default {
window.addEventListener('focus', setLastFocus); window.addEventListener('focus', setLastFocus);
// Check browser is online periodically // Check browser is online periodically
const checkOffline = () => { const checkOffline = async () => {
const isBrowserOffline = window.navigator.onLine === false; const isBrowserOffline = window.navigator.onLine === false;
if (!isBrowserOffline && if (!isBrowserOffline &&
store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() && store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() &&
this.isUserActive() this.isUserActive()
) { ) {
store.commit('updateLastOfflineCheck'); store.commit('updateLastOfflineCheck');
new Promise((resolve, reject) => {
const script = document.createElement('script'); const script = document.createElement('script');
let timeout; let timeout;
let clean = (cb) => { try {
clearTimeout(timeout); await new Promise((resolve, reject) => {
document.head.removeChild(script); script.onload = resolve;
clean = () => {}; // Prevent from cleaning several times script.onerror = reject;
cb();
};
script.onload = () => clean(resolve);
script.onerror = () => clean(reject);
script.src = `https://apis.google.com/js/api.js?${Date.now()}`; script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
try { try {
document.head.appendChild(script); // This can fail with bad network document.head.appendChild(script); // This can fail with bad network
timeout = setTimeout(() => clean(reject), networkTimeout); timeout = setTimeout(reject, networkTimeout);
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
})
.then(() => {
isConnectionDown = false;
}, () => {
isConnectionDown = true;
}); });
isConnectionDown = false;
} catch (e) {
isConnectionDown = true;
} finally {
clearTimeout(timeout);
document.head.removeChild(script);
}
} }
const offline = isBrowserOffline || isConnectionDown; const offline = isBrowserOffline || isConnectionDown;
if (store.state.offline !== offline) { if (store.state.offline !== offline) {
@ -88,7 +106,7 @@ export default {
isUserActive() { isUserActive() {
return this.lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused(); return this.lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused();
}, },
loadScript(url) { async loadScript(url) {
if (!scriptLoadingPromises[url]) { if (!scriptLoadingPromises[url]) {
scriptLoadingPromises[url] = new Promise((resolve, reject) => { scriptLoadingPromises[url] = new Promise((resolve, reject) => {
const script = document.createElement('script'); const script = document.createElement('script');
@ -103,7 +121,7 @@ export default {
} }
return scriptLoadingPromises[url]; return scriptLoadingPromises[url];
}, },
startOauth2(url, params = {}, silent = false) { async startOauth2(url, params = {}, silent = false) {
// Build the authorize URL // Build the authorize URL
const state = utils.uid(); const state = utils.uid();
params.state = state; params.state = state;
@ -125,47 +143,29 @@ export default {
} }
} }
return new Promise((resolve, reject) => {
let checkClosedInterval; let checkClosedInterval;
let closeTimeout; let closeTimeout;
let msgHandler; let msgHandler;
let clean = () => { try {
clearInterval(checkClosedInterval); return await new Promise((resolve, reject) => {
if (!silent && !wnd.closed) {
wnd.close();
}
if (iframeElt) {
document.body.removeChild(iframeElt);
}
clearTimeout(closeTimeout);
window.removeEventListener('message', msgHandler);
clean = () => Promise.resolve(); // Prevent from cleaning several times
return Promise.resolve();
};
if (silent) { if (silent) {
iframeElt.onerror = () => clean() iframeElt.onerror = () => {
.then(() => reject(new Error('Unknown error.'))); reject(new Error('Unknown error.'));
closeTimeout = setTimeout( };
() => clean() closeTimeout = setTimeout(() => {
.then(() => {
isConnectionDown = true; isConnectionDown = true;
store.commit('setOffline', true); store.commit('setOffline', true);
store.commit('updateLastOfflineCheck'); store.commit('updateLastOfflineCheck');
reject(new Error('You are offline.')); reject(new Error('You are offline.'));
}), }, networkTimeout);
networkTimeout,
);
} else { } else {
closeTimeout = setTimeout( closeTimeout = setTimeout(() => {
() => clean() reject(new Error('Timeout.'));
.then(() => reject(new Error('Timeout.'))), }, oauth2AuthorizationTimeout);
oauth2AuthorizationTimeout,
);
} }
msgHandler = event => event.source === wnd && event.origin === utils.origin && clean() msgHandler = (event) => {
.then(() => { if (event.source === wnd && event.origin === utils.origin) {
const data = utils.parseQueryParams(`${event.data}`.slice(1)); const data = utils.parseQueryParams(`${event.data}`.slice(1));
if (data.error || data.state !== state) { if (data.error || data.state !== state) {
console.error(data); // eslint-disable-line no-console console.error(data); // eslint-disable-line no-console
@ -178,16 +178,31 @@ export default {
expiresIn: data.expires_in, expiresIn: data.expires_in,
}); });
} }
}); }
};
window.addEventListener('message', msgHandler); window.addEventListener('message', msgHandler);
if (!silent) { if (!silent) {
checkClosedInterval = setInterval(() => wnd.closed && clean() checkClosedInterval = setInterval(() => {
.then(() => reject(new Error('Authorize window was closed.'))), 250); if (wnd.closed) {
reject(new Error('Authorize window was closed.'));
}
}, 250);
} }
}); });
} finally {
clearInterval(checkClosedInterval);
if (!silent && !wnd.closed) {
wnd.close();
}
if (iframeElt) {
document.body.removeChild(iframeElt);
}
clearTimeout(closeTimeout);
window.removeEventListener('message', msgHandler);
}
}, },
request(configParam, offlineCheck = false) { async request(configParam, offlineCheck = false) {
let retryAfter = 500; // 500 ms let retryAfter = 500; // 500 ms
const maxRetryAfter = 10 * 1000; // 10 sec const maxRetryAfter = 10 * 1000; // 10 sec
const config = Object.assign({}, configParam); const config = Object.assign({}, configParam);
@ -198,27 +213,9 @@ export default {
config.headers['Content-Type'] = 'application/json'; config.headers['Content-Type'] = 'application/json';
} }
function parseHeaders(xhr) { const attempt = async () => {
const pairs = xhr.getAllResponseHeaders().trim().split('\n'); try {
return pairs.reduce((headers, header) => { await new Promise((resolve, reject) => {
const split = header.trim().split(':');
const key = split.shift().trim().toLowerCase();
const value = split.join(':').trim();
headers[key] = value;
return headers;
}, {});
}
function isRetriable(err) {
if (err.status === 403) {
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
}
return err.status === 429 || (err.status >= 500 && err.status < 600);
}
const attempt =
() => new Promise((resolve, reject) => {
if (offlineCheck) { if (offlineCheck) {
store.commit('updateLastOfflineCheck'); store.commit('updateLastOfflineCheck');
} }
@ -280,19 +277,20 @@ export default {
xhr.responseType = 'blob'; xhr.responseType = 'blob';
} }
xhr.send(config.body || null); xhr.send(config.body || null);
}) });
.catch((err) => { } catch (err) {
// Try again later in case of retriable error // Try again later in case of retriable error
if (isRetriable(err) && retryAfter < maxRetryAfter) { if (isRetriable(err) && retryAfter < maxRetryAfter) {
return new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, retryAfter); setTimeout(resolve, retryAfter);
// Exponential backoff // Exponential backoff
retryAfter *= 2; retryAfter *= 2;
}) });
.then(attempt); attempt();
} }
throw err; throw err;
}); }
};
return attempt(); return attempt();
}, },

View File

@ -15,24 +15,21 @@ export default new Provider({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.pageId}${location.blogUrl}${token.name}`; return `${location.pageId}${location.blogUrl}${token.name}`;
}, },
publish(token, html, metadata, publishLocation) { async publish(token, html, metadata, publishLocation) {
return googleHelper.uploadBlogger( const page = await googleHelper.uploadBlogger({
token, token,
publishLocation.blogUrl, blogUrl: publishLocation.blogUrl,
publishLocation.blogId, blogId: publishLocation.blogId,
publishLocation.pageId, postId: publishLocation.pageId,
metadata.title, title: metadata.title,
html, content: html,
null, isPage: true,
null, });
null, return {
true,
)
.then(page => ({
...publishLocation, ...publishLocation,
blogId: page.blog.id, blogId: page.blog.id,
pageId: page.id, pageId: page.id,
})); };
}, },
makeLocation(token, blogUrl, pageId) { makeLocation(token, blogUrl, pageId) {
const location = { const location = {

View File

@ -15,23 +15,21 @@ export default new Provider({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.postId}${location.blogUrl}${token.name}`; return `${location.postId}${location.blogUrl}${token.name}`;
}, },
publish(token, html, metadata, publishLocation) { async publish(token, html, metadata, publishLocation) {
return googleHelper.uploadBlogger( const post = await googleHelper.uploadBlogger({
...publishLocation,
token, token,
publishLocation.blogUrl, title: metadata.title,
publishLocation.blogId, content: html,
publishLocation.postId, labels: metadata.tags,
metadata.title, isDraft: metadata.status === 'draft',
html, published: metadata.date,
metadata.tags, });
metadata.status === 'draft', return {
metadata.date,
)
.then(post => ({
...publishLocation, ...publishLocation,
blogId: post.blog.id, blogId: post.blog.id,
postId: post.id, postId: post.id,
})); };
}, },
makeLocation(token, blogUrl, postId) { makeLocation(token, blogUrl, postId) {
const location = { const location = {

View File

@ -2,6 +2,7 @@ import providerRegistry from './providerRegistry';
import emptyContent from '../../../data/emptyContent'; import emptyContent from '../../../data/emptyContent';
import utils from '../../utils'; import utils from '../../utils';
import store from '../../../store'; import store from '../../../store';
import fileSvc from '../../fileSvc';
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/; const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
@ -66,6 +67,14 @@ export default class Provider {
return utils.addItemHash(result); return utils.addItemHash(result);
} }
static getContentSyncData(fileId) {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
if (!syncData) {
throw new Error(); // No need for a proper error message.
}
return syncData;
}
/** /**
* Find and open a file with location that meets the criteria * Find and open a file with location that meets the criteria
*/ */
@ -73,13 +82,13 @@ export default class Provider {
const location = utils.search(allLocations, criteria); const location = utils.search(allLocations, criteria);
if (location) { if (location) {
// Found one, open it if it exists // Found one, open it if it exists
const file = store.state.file.itemMap[location.fileId]; const item = store.state.file.itemMap[location.fileId];
if (file) { if (item) {
store.commit('file/setCurrentId', file.id); store.commit('file/setCurrentId', item.id);
// If file is in the trash, restore it // If file is in the trash, restore it
if (file.parentId === 'trash') { if (item.parentId === 'trash') {
store.commit('file/patchItem', { fileSvc.setOrPatchItem({
...file, ...item,
parentId: null, parentId: null,
}); });
} }

View File

@ -3,13 +3,6 @@ import couchdbHelper from './helpers/couchdbHelper';
import Provider from './common/Provider'; import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
const getSyncData = (fileId) => {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
return syncData
? Promise.resolve(syncData)
: Promise.reject(); // No need for a proper error message.
};
let syncLastSeq; let syncLastSeq;
export default new Provider({ export default new Provider({
@ -17,7 +10,7 @@ export default new Provider({
getToken() { getToken() {
return store.getters['workspace/syncToken']; return store.getters['workspace/syncToken'];
}, },
initWorkspace() { async initWorkspace() {
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing / const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
const workspaceParams = { const workspaceParams = {
providerId: this.id, providerId: this.id,
@ -35,9 +28,16 @@ export default new Provider({
}); });
} }
return Promise.resolve() // Create the workspace
.then(() => getWorkspace() || couchdbHelper.getDb(getToken()) let workspace = getWorkspace();
.then((db) => { if (!workspace) {
// Make sure the database exists and retrieve its name
let db;
try {
db = await couchdbHelper.getDb(getToken());
} catch (e) {
throw new Error(`${dbUrl} is not accessible. Make sure you have the proper permissions.`);
}
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspaces', {
[workspaceId]: { [workspaceId]: {
id: workspaceId, id: workspaceId,
@ -46,11 +46,9 @@ export default new Provider({
dbUrl, dbUrl,
}, },
}); });
return getWorkspace(); workspace = getWorkspace();
}, () => { }
throw new Error(`${dbUrl} is not accessible. Make sure you have the right permissions.`);
}))
.then((workspace) => {
// Fix the URL hash // Fix the URL hash
utils.setQueryParams(workspaceParams); utils.setQueryParams(workspaceParams);
if (workspace.url !== window.location.href) { if (workspace.url !== window.location.href) {
@ -62,13 +60,11 @@ export default new Provider({
}); });
} }
return getWorkspace(); return getWorkspace();
});
}, },
getChanges() { async getChanges() {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const lastSeq = store.getters['data/localSettings'].syncLastSeq; const lastSeq = store.getters['data/localSettings'].syncLastSeq;
return couchdbHelper.getChanges(syncToken, lastSeq) const result = await couchdbHelper.getChanges(syncToken, lastSeq);
.then((result) => {
const changes = result.changes.filter((change) => { const changes = result.changes.filter((change) => {
if (!change.deleted && change.doc) { if (!change.deleted && change.doc) {
change.item = change.doc.item; change.item = change.doc.item;
@ -89,31 +85,28 @@ export default new Provider({
}); });
syncLastSeq = result.lastSeq; syncLastSeq = result.lastSeq;
return changes; return changes;
});
}, },
onChangesApplied() { onChangesApplied() {
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
syncLastSeq, syncLastSeq,
}); });
}, },
saveSimpleItem(item, syncData) { async saveSimpleItem(item, syncData) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return couchdbHelper.uploadDocument( const { id, rev } = couchdbHelper.uploadDocument({
syncToken, token: syncToken,
item, item,
undefined, documentId: syncData && syncData.id,
undefined, rev: syncData && syncData.rev,
syncData && syncData.id, });
syncData && syncData.rev, return {
)
.then(res => ({
// Build sync data // Build sync data
id: res.id, id,
itemId: item.id, itemId: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
rev: res.rev, rev,
})); };
}, },
removeItem(syncData) { removeItem(syncData) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
@ -122,14 +115,13 @@ export default new Provider({
downloadContent(token, syncLocation) { downloadContent(token, syncLocation) {
return this.downloadData(`${syncLocation.fileId}/content`); return this.downloadData(`${syncLocation.fileId}/content`);
}, },
downloadData(dataId) { async downloadData(dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId]; const syncData = store.getters['data/syncDataByItemId'][dataId];
if (!syncData) { if (!syncData) {
return Promise.resolve(); return Promise.resolve();
} }
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return couchdbHelper.retrieveDocumentWithAttachments(syncToken, syncData.id) const body = await couchdbHelper.retrieveDocumentWithAttachments(syncToken, syncData.id);
.then((body) => {
let item; let item;
if (body.item.type === 'content') { if (body.item.type === 'content') {
item = Provider.parseContent(body.attachments.data, body.item.id); item = Provider.parseContent(body.attachments.data, body.item.id);
@ -147,17 +139,14 @@ export default new Provider({
}); });
} }
return item; return item;
});
}, },
uploadContent(token, content, syncLocation) { async uploadContent(token, content, syncLocation) {
return this.uploadData(content) await this.uploadData(content);
.then(() => syncLocation); return syncLocation;
}, },
uploadData(item) { async uploadData(item) {
const syncData = store.getters['data/syncDataByItemId'][item.id]; const syncData = store.getters['data/syncDataByItemId'][item.id];
if (syncData && syncData.hash === item.hash) { if (!syncData || syncData.hash !== item.hash) {
return Promise.resolve();
}
let data; let data;
let dataType; let dataType;
if (item.type === 'content') { if (item.type === 'content') {
@ -168,19 +157,19 @@ export default new Provider({
dataType = 'application/json'; dataType = 'application/json';
} }
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return couchdbHelper.uploadDocument( const res = await couchdbHelper.uploadDocument({
syncToken, token: syncToken,
{ item: {
id: item.id, id: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}, },
data, data,
dataType, dataType,
syncData && syncData.id, documentId: syncData && syncData.id,
syncData && syncData.rev, rev: syncData && syncData.rev,
) });
.then(res => store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
[res.id]: { [res.id]: {
// Build sync data // Build sync data
id: res.id, id: res.id,
@ -189,12 +178,12 @@ export default new Provider({
hash: item.hash, hash: item.hash,
rev: res.rev, rev: res.rev,
}, },
})); });
}
}, },
listRevisions(token, fileId) { async listRevisions(token, fileId) {
return getSyncData(fileId) const syncData = Provider.getContentSyncData(fileId);
.then(syncData => couchdbHelper.retrieveDocumentWithRevisions(token, syncData.id)) const body = await couchdbHelper.retrieveDocumentWithRevisions(token, syncData.id);
.then((body) => {
const revisions = []; const revisions = [];
body._revs_info.forEach((revInfo) => { // eslint-disable-line no-underscore-dangle body._revs_info.forEach((revInfo) => { // eslint-disable-line no-underscore-dangle
if (revInfo.status === 'available') { if (revInfo.status === 'available') {
@ -206,20 +195,17 @@ export default new Provider({
} }
}); });
return revisions; return revisions;
});
}, },
loadRevision(token, fileId, revision) { async loadRevision(token, fileId, revision) {
return getSyncData(fileId) const syncData = Provider.getContentSyncData(fileId);
.then(syncData => couchdbHelper.retrieveDocument(token, syncData.id, revision.id)) const body = await couchdbHelper.retrieveDocument(token, syncData.id, revision.id);
.then((body) => {
revision.sub = body.sub; revision.sub = body.sub;
revision.created = body.time || 1; // Has to be truthy to prevent from loading several times revision.created = body.time || 1; // Has to be truthy to prevent from loading several times
});
}, },
getRevisionContent(token, fileId, revisionId) { async getRevisionContent(token, fileId, revisionId) {
return getSyncData(fileId) const syncData = Provider.getContentSyncData(fileId);
.then(syncData => couchdbHelper const body = await couchdbHelper
.retrieveDocumentWithAttachments(token, syncData.id, revisionId)) .retrieveDocumentWithAttachments(token, syncData.id, revisionId);
.then(body => Provider.parseContent(body.attachments.data, body.item.id)); return Provider.parseContent(body.attachments.data, body.item.id);
}, },
}); });

View File

@ -34,61 +34,62 @@ export default new Provider({
checkPath(path) { checkPath(path) {
return path && path.match(/^\/[^\\<>:"|?*]+$/); return path && path.match(/^\/[^\\<>:"|?*]+$/);
}, },
downloadContent(token, syncLocation) { async downloadContent(token, syncLocation) {
return dropboxHelper.downloadFile( const { content } = await dropboxHelper.downloadFile({
token, token,
makePathRelative(token, syncLocation.path), path: makePathRelative(token, syncLocation.path),
syncLocation.dropboxFileId, fileId: syncLocation.dropboxFileId,
) });
.then(({ content }) => Provider.parseContent(content, `${syncLocation.fileId}/content`)); return Provider.parseContent(content, `${syncLocation.fileId}/content`);
}, },
uploadContent(token, content, syncLocation) { async uploadContent(token, content, syncLocation) {
return dropboxHelper.uploadFile( const dropboxFile = await dropboxHelper.uploadFile({
token, token,
makePathRelative(token, syncLocation.path), path: makePathRelative(token, syncLocation.path),
Provider.serializeContent(content), content: Provider.serializeContent(content),
syncLocation.dropboxFileId, fileId: syncLocation.dropboxFileId,
) });
.then(dropboxFile => ({ return {
...syncLocation, ...syncLocation,
path: makePathAbsolute(token, dropboxFile.path_display), path: makePathAbsolute(token, dropboxFile.path_display),
dropboxFileId: dropboxFile.id, dropboxFileId: dropboxFile.id,
})); };
}, },
publish(token, html, metadata, publishLocation) { async publish(token, html, metadata, publishLocation) {
return dropboxHelper.uploadFile( const dropboxFile = await dropboxHelper.uploadFile({
token, token,
publishLocation.path, path: publishLocation.path,
html, content: html,
publishLocation.dropboxFileId, fileId: publishLocation.dropboxFileId,
) });
.then(dropboxFile => ({ return {
...publishLocation, ...publishLocation,
path: makePathAbsolute(token, dropboxFile.path_display), path: makePathAbsolute(token, dropboxFile.path_display),
dropboxFileId: dropboxFile.id, dropboxFileId: dropboxFile.id,
})); };
}, },
openFiles(token, paths) { async openFiles(token, paths) {
const openOneFile = () => { await utils.awaitSequence(paths, async (path) => {
const path = paths.pop(); // Check if the file exists and open it
if (!path) { if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], {
return null;
}
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], {
providerId: this.id, providerId: this.id,
path, path,
})) { })) {
// File exists and has just been opened. Next... // Download content from Dropbox
return openOneFile();
}
// Download content from Dropbox and create the file
const syncLocation = { const syncLocation = {
path, path,
providerId: this.id, providerId: this.id,
sub: token.sub, sub: token.sub,
}; };
return this.downloadContent(token, syncLocation) let content;
.then((content) => { try {
content = await this.downloadContent(token, syncLocation);
} catch (e) {
store.dispatch('notification/error', `Could not open file ${path}.`);
return;
}
// Create the file
let name = path; let name = path;
const slashPos = name.lastIndexOf('/'); const slashPos = name.lastIndexOf('/');
if (slashPos > -1 && slashPos < name.length - 1) { if (slashPos > -1 && slashPos < name.length - 1) {
@ -98,7 +99,7 @@ export default new Provider({
if (dotPos > 0 && slashPos < name.length) { if (dotPos > 0 && slashPos < name.length) {
name = name.slice(0, dotPos); name = name.slice(0, dotPos);
} }
return fileSvc.createFile({ const item = await fileSvc.createFile({
name, name,
parentId: store.getters['file/current'].parentId, parentId: store.getters['file/current'].parentId,
text: content.text, text: content.text,
@ -106,8 +107,6 @@ export default new Provider({
discussions: content.discussions, discussions: content.discussions,
comments: content.comments, comments: content.comments,
}, true); }, true);
})
.then((item) => {
store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
store.commit('syncLocation/setItem', { store.commit('syncLocation/setItem', {
...syncLocation, ...syncLocation,
@ -115,13 +114,8 @@ export default new Provider({
fileId: item.id, fileId: item.id,
}); });
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Dropbox.`); store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Dropbox.`);
}) }
.catch(() => { });
store.dispatch('notification/error', `Could not open file ${path}.`);
})
.then(() => openOneFile());
};
return Promise.resolve(openOneFile());
}, },
makeLocation(token, path) { makeLocation(token, path) {
return { return {

View File

@ -15,39 +15,38 @@ export default new Provider({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.filename}${location.gistId}${token.name}`; return `${location.filename}${location.gistId}${token.name}`;
}, },
downloadContent(token, syncLocation) { async downloadContent(token, syncLocation) {
return githubHelper.downloadGist(token, syncLocation.gistId, syncLocation.filename) const content = await githubHelper.downloadGist({
.then(content => Provider.parseContent(content, `${syncLocation.fileId}/content`)); ...syncLocation,
token,
});
return Provider.parseContent(content, `${syncLocation.fileId}/content`);
}, },
uploadContent(token, content, syncLocation) { async uploadContent(token, content, syncLocation) {
const file = store.state.file.itemMap[syncLocation.fileId]; const file = store.state.file.itemMap[syncLocation.fileId];
const description = utils.sanitizeName(file && file.name); const description = utils.sanitizeName(file && file.name);
return githubHelper.uploadGist( const gist = await githubHelper.uploadGist({
...syncLocation,
token, token,
description, description,
syncLocation.filename, content: Provider.serializeContent(content),
Provider.serializeContent(content), });
syncLocation.isPublic, return {
syncLocation.gistId,
)
.then(gist => ({
...syncLocation, ...syncLocation,
gistId: gist.id, gistId: gist.id,
})); };
}, },
publish(token, html, metadata, publishLocation) { async publish(token, html, metadata, publishLocation) {
return githubHelper.uploadGist( const gist = await githubHelper.uploadGist({
...publishLocation,
token, token,
metadata.title, description: metadata.title,
publishLocation.filename, content: html,
html, });
publishLocation.isPublic, return {
publishLocation.gistId,
)
.then(gist => ({
...publishLocation, ...publishLocation,
gistId: gist.id, gistId: gist.id,
})); };
}, },
makeLocation(token, filename, isPublic, gistId) { makeLocation(token, filename, isPublic, gistId) {
return { return {

View File

@ -18,68 +18,58 @@ export default new Provider({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.path}${location.owner}/${location.repo}${token.name}`; return `${location.path}${location.owner}/${location.repo}${token.name}`;
}, },
downloadContent(token, syncLocation) { async downloadContent(token, syncLocation) {
return githubHelper.downloadFile( try {
const { sha, content } = await githubHelper.downloadFile({
...syncLocation,
token, token,
syncLocation.owner, });
syncLocation.repo,
syncLocation.branch,
syncLocation.path,
)
.then(({ sha, content }) => {
savedSha[syncLocation.id] = sha; savedSha[syncLocation.id] = sha;
return Provider.parseContent(content, `${syncLocation.fileId}/content`); return Provider.parseContent(content, `${syncLocation.fileId}/content`);
}) } catch (e) {
.catch(() => null); // Ignore error, upload is going to fail anyway // Ignore error, upload is going to fail anyway
},
uploadContent(token, content, syncLocation) {
let result = Promise.resolve();
if (!savedSha[syncLocation.id]) {
result = this.downloadContent(token, syncLocation); // Get the last sha
}
return result
.then(() => {
const sha = savedSha[syncLocation.id];
delete savedSha[syncLocation.id];
return githubHelper.uploadFile(
token,
syncLocation.owner,
syncLocation.repo,
syncLocation.branch,
syncLocation.path,
Provider.serializeContent(content),
sha,
);
})
.then(() => syncLocation);
},
publish(token, html, metadata, publishLocation) {
return this.downloadContent(token, publishLocation) // Get the last sha
.then(() => {
const sha = savedSha[publishLocation.id];
delete savedSha[publishLocation.id];
return githubHelper.uploadFile(
token,
publishLocation.owner,
publishLocation.repo,
publishLocation.branch,
publishLocation.path,
html,
sha,
);
})
.then(() => publishLocation);
},
openFile(token, syncLocation) {
return Promise.resolve()
.then(() => {
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], syncLocation)) {
// File exists and has just been opened. Next...
return null; return null;
} }
// Download content from GitHub and create the file },
return this.downloadContent(token, syncLocation) async uploadContent(token, content, syncLocation) {
.then((content) => { if (!savedSha[syncLocation.id]) {
await this.downloadContent(token, syncLocation); // Get the last sha
}
const sha = savedSha[syncLocation.id];
delete savedSha[syncLocation.id];
await githubHelper.uploadFile({
...syncLocation,
token,
content: Provider.serializeContent(content),
sha,
});
return syncLocation;
},
async publish(token, html, metadata, publishLocation) {
await this.downloadContent(token, publishLocation); // Get the last sha
const sha = savedSha[publishLocation.id];
delete savedSha[publishLocation.id];
await githubHelper.uploadFile({
...publishLocation,
token,
content: html,
sha,
});
return publishLocation;
},
async openFile(token, syncLocation) {
// Check if the file exists and open it
if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], syncLocation)) {
// Download content from GitHub
let content;
try {
content = await this.downloadContent(token, syncLocation);
} catch (e) {
store.dispatch('notification/error', `Could not open file ${syncLocation.path}.`);
return;
}
// Create the file
let name = syncLocation.path; let name = syncLocation.path;
const slashPos = name.lastIndexOf('/'); const slashPos = name.lastIndexOf('/');
if (slashPos > -1 && slashPos < name.length - 1) { if (slashPos > -1 && slashPos < name.length - 1) {
@ -89,7 +79,7 @@ export default new Provider({
if (dotPos > 0 && slashPos < name.length) { if (dotPos > 0 && slashPos < name.length) {
name = name.slice(0, dotPos); name = name.slice(0, dotPos);
} }
return fileSvc.createFile({ const item = await fileSvc.createFile({
name, name,
parentId: store.getters['file/current'].parentId, parentId: store.getters['file/current'].parentId,
text: content.text, text: content.text,
@ -97,8 +87,6 @@ export default new Provider({
discussions: content.discussions, discussions: content.discussions,
comments: content.comments, comments: content.comments,
}, true); }, true);
})
.then((item) => {
store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
store.commit('syncLocation/setItem', { store.commit('syncLocation/setItem', {
...syncLocation, ...syncLocation,
@ -106,11 +94,7 @@ export default new Provider({
fileId: item.id, fileId: item.id,
}); });
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from GitHub.`); store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from GitHub.`);
}) }
.catch(() => {
store.dispatch('notification/error', `Could not open file ${syncLocation.path}.`);
});
});
}, },
parseRepoUrl(url) { parseRepoUrl(url) {
const parsedRepo = url && url.match(/([^/:]+)\/([^/]+?)(?:\.git|\/)?$/); const parsedRepo = url && url.match(/([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);

View File

@ -4,15 +4,8 @@ import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
import userSvc from '../userSvc'; import userSvc from '../userSvc';
const getSyncData = (fileId) => {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
return syncData
? Promise.resolve(syncData)
: Promise.reject(); // No need for a proper error message.
};
const getAbsolutePath = syncData => const getAbsolutePath = syncData =>
(store.getters['workspace/currentWorkspace'].path || '') + syncData.id; `${store.getters['workspace/currentWorkspace'].path || ''}${syncData.id}`;
const getWorkspaceWithOwner = () => { const getWorkspaceWithOwner = () => {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
@ -38,7 +31,7 @@ export default new Provider({
getToken() { getToken() {
return store.getters['workspace/syncToken']; return store.getters['workspace/syncToken'];
}, },
initWorkspace() { async initWorkspace() {
const [owner, repo] = (utils.queryParams.repo || '').split('/'); const [owner, repo] = (utils.queryParams.repo || '').split('/');
const { branch } = utils.queryParams; const { branch } = utils.queryParams;
const workspaceParams = { const workspaceParams = {
@ -55,23 +48,17 @@ export default new Provider({
const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspaceId = utils.makeWorkspaceId(workspaceParams);
let workspace = store.getters['data/sanitizedWorkspaces'][workspaceId]; let workspace = store.getters['data/sanitizedWorkspaces'][workspaceId];
return Promise.resolve()
.then(() => {
// See if we already have a token // See if we already have a token
let token;
if (workspace) { if (workspace) {
// Token sub is in the workspace // Token sub is in the workspace
const token = store.getters['data/githubTokens'][workspace.sub]; token = store.getters['data/githubTokens'][workspace.sub];
if (token) {
return token;
} }
if (!token) {
await store.dispatch('modal/open', { type: 'githubAccount' });
token = await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} }
// If no token has been found, popup an authorize window and get one
return store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
});
})
.then((token) => {
if (!workspace) { if (!workspace) {
const pathEntries = (path || '').split('/'); const pathEntries = (path || '').split('/');
const name = pathEntries[pathEntries.length - 2] || repo; // path ends with `/` const name = pathEntries[pathEntries.length - 2] || repo; // path ends with `/`
@ -82,6 +69,7 @@ export default new Provider({
name, name,
}; };
} }
// Fix the URL hash // Fix the URL hash
utils.setQueryParams(workspaceParams); utils.setQueryParams(workspaceParams);
if (workspace.url !== window.location.href) { if (workspace.url !== window.location.href) {
@ -93,13 +81,16 @@ export default new Provider({
}); });
} }
return store.getters['data/sanitizedWorkspaces'][workspaceId]; return store.getters['data/sanitizedWorkspaces'][workspaceId];
});
}, },
getChanges() { async getChanges() {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner(); const { owner, repo, branch } = getWorkspaceWithOwner();
return githubHelper.getHeadTree(syncToken, owner, repo, branch) const tree = await githubHelper.getTree({
.then((tree) => { token: syncToken,
owner,
repo,
branch,
});
const workspacePath = store.getters['workspace/currentWorkspace'].path || ''; const workspacePath = store.getters['workspace/currentWorkspace'].path || '';
const syncDataByPath = store.getters['data/syncData']; const syncDataByPath = store.getters['data/syncData'];
const syncDataByItemId = store.getters['data/syncDataByItemId']; const syncDataByItemId = store.getters['data/syncDataByItemId'];
@ -296,14 +287,10 @@ export default new Provider({
}); });
return changes; return changes;
});
}, },
saveSimpleItem(item) { async saveSimpleItem(item) {
const path = store.getters.itemPaths[item.fileId || item.id]; const path = store.getters.itemPaths[item.fileId || item.id];
return Promise.resolve()
.then(() => {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner();
const syncData = { const syncData = {
itemId: item.id, itemId: item.id,
type: item.type, type: item.type,
@ -312,10 +299,10 @@ export default new Provider({
if (item.type === 'file') { if (item.type === 'file') {
syncData.id = `${path}.md`; syncData.id = `${path}.md`;
} else if (item.type === 'folder') { return syncData;
syncData.id = path;
} }
if (syncData.id) { if (item.type === 'folder') {
syncData.id = path;
return syncData; return syncData;
} }
@ -328,42 +315,38 @@ export default new Provider({
}), true); }), true);
const extension = item.type === 'syncLocation' ? 'sync' : 'publish'; const extension = item.type === 'syncLocation' ? 'sync' : 'publish';
syncData.id = `${path}.${data}.${extension}`; syncData.id = `${path}.${data}.${extension}`;
return githubHelper.uploadFile( await githubHelper.uploadFile({
syncToken, ...getWorkspaceWithOwner(),
owner, token: syncToken,
repo, path: getAbsolutePath(syncData),
branch, content: '',
getAbsolutePath(syncData), sha: treeShaMap[syncData.id],
'',
treeShaMap[syncData.id],
).then(() => syncData);
}); });
return syncData;
}, },
removeItem(syncData) { async removeItem(syncData) {
// Ignore content deletion // Ignore content deletion
if (syncData.type === 'content') { if (syncData.type !== 'content') {
return Promise.resolve();
}
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner(); await githubHelper.removeFile({
return githubHelper.removeFile( ...getWorkspaceWithOwner(),
syncToken, token: syncToken,
owner, path: getAbsolutePath(syncData),
repo, sha: treeShaMap[syncData.id],
branch, });
getAbsolutePath(syncData), }
treeShaMap[syncData.id],
);
}, },
downloadContent(token, syncLocation) { async downloadContent(token, syncLocation) {
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId]; const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId];
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`]; const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (!syncData || !contentSyncData) { if (!syncData || !contentSyncData) {
return Promise.resolve(); return null;
} }
const { owner, repo, branch } = getWorkspaceWithOwner(); const { sha, content } = await githubHelper.downloadFile({
return githubHelper.downloadFile(token, owner, repo, branch, getAbsolutePath(syncData)) ...getWorkspaceWithOwner(),
.then(({ sha, content }) => { token,
path: getAbsolutePath(syncData),
});
const item = Provider.parseContent(content, `${syncLocation.fileId}/content`); const item = Provider.parseContent(content, `${syncLocation.fileId}/content`);
if (item.hash !== contentSyncData.hash) { if (item.hash !== contentSyncData.hash) {
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
@ -375,17 +358,18 @@ export default new Provider({
}); });
} }
return item; return item;
});
}, },
downloadData(dataId) { async downloadData(dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId]; const syncData = store.getters['data/syncDataByItemId'][dataId];
if (!syncData) { if (!syncData) {
return Promise.resolve(); return null;
} }
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner(); const { sha, content } = await githubHelper.downloadFile({
return githubHelper.downloadFile(syncToken, owner, repo, branch, getAbsolutePath(syncData)) ...getWorkspaceWithOwner(),
.then(({ sha, content }) => { token: syncToken,
path: getAbsolutePath(syncData),
});
const item = JSON.parse(content); const item = JSON.parse(content);
if (item.hash !== syncData.hash) { if (item.hash !== syncData.hash) {
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
@ -397,26 +381,20 @@ export default new Provider({
}); });
} }
return item; return item;
});
}, },
uploadContent(token, content, syncLocation) { async uploadContent(token, content, syncLocation) {
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`]; const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (contentSyncData && contentSyncData.hash === content.hash) { if (!contentSyncData || contentSyncData.hash !== content.hash) {
return Promise.resolve(syncLocation); const path = `${store.getters.itemPaths[syncLocation.fileId]}.md`;
} const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId]; const id = `/${path}`;
const { owner, repo, branch } = getWorkspaceWithOwner(); const res = await githubHelper.uploadFile({
return githubHelper.uploadFile( ...getWorkspaceWithOwner(),
token, token,
owner, path: absolutePath,
repo, content: Provider.serializeContent(content),
branch, sha: treeShaMap[id],
getAbsolutePath(syncData), });
Provider.serializeContent(content),
treeShaMap[syncData.id],
)
.then((res) => {
const id = `/${syncData.id}`;
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
[id]: { [id]: {
// Build sync data // Build sync data
@ -427,14 +405,12 @@ export default new Provider({
sha: res.content.sha, sha: res.content.sha,
}, },
}); });
return syncLocation;
});
},
uploadData(item) {
const oldSyncData = store.getters['data/syncDataByItemId'][item.id];
if (oldSyncData && oldSyncData.hash === item.hash) {
return Promise.resolve();
} }
return syncLocation;
},
async uploadData(item) {
const oldSyncData = store.getters['data/syncDataByItemId'][item.id];
if (!oldSyncData || oldSyncData.hash !== item.hash) {
const syncData = { const syncData = {
id: `.stackedit-data/${item.id}.json`, id: `.stackedit-data/${item.id}.json`,
itemId: item.id, itemId: item.id,
@ -442,22 +418,20 @@ export default new Provider({
hash: item.hash, hash: item.hash,
}; };
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner(); const res = await githubHelper.uploadFile({
return githubHelper.uploadFile( ...getWorkspaceWithOwner(),
syncToken, token: syncToken,
owner, path: getAbsolutePath(syncData),
repo, content: JSON.stringify(item),
branch, sha: oldSyncData && oldSyncData.sha,
getAbsolutePath(syncData), });
JSON.stringify(item), store.dispatch('data/patchSyncData', {
oldSyncData && oldSyncData.sha,
)
.then(res => store.dispatch('data/patchSyncData', {
[syncData.id]: { [syncData.id]: {
...syncData, ...syncData,
sha: res.content.sha, sha: res.content.sha,
}, },
})); });
}
}, },
onSyncEnd() { onSyncEnd() {
// Clean up // Clean up
@ -468,34 +442,48 @@ export default new Provider({
treeSyncLocationMap = null; treeSyncLocationMap = null;
treePublishLocationMap = null; treePublishLocationMap = null;
}, },
listRevisions(token, fileId) { async listRevisions(token, fileId) {
const { owner, repo, branch } = getWorkspaceWithOwner(); const { owner, repo, branch } = getWorkspaceWithOwner();
return getSyncData(fileId) const syncData = Provider.getContentSyncData(fileId);
.then(syncData => githubHelper.getCommits(token, owner, repo, branch, syncData.id)) const entries = await githubHelper.getCommits({
.then(entries => entries.map((entry) => { token,
owner,
repo,
sha: branch,
path: syncData.id,
});
return entries.map(({
author,
committer,
commit,
sha,
}) => {
let user; let user;
if (entry.author && entry.author.login) { if (author && author.login) {
user = entry.author; user = author;
} else if (entry.committer && entry.committer.login) { } else if (committer && committer.login) {
user = entry.committer; user = committer;
} }
const sub = `gh:${user.id}`; const sub = `gh:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (entry.commit.author && entry.commit.author.date) const date = (commit.author && commit.author.date)
|| (entry.commit.committer && entry.commit.committer.date); || (commit.committer && commit.committer.date);
return { return {
id: entry.sha, id: sha,
sub, sub,
created: date ? new Date(date).getTime() : 1, created: date ? new Date(date).getTime() : 1,
}; };
}) })
.sort((revision1, revision2) => revision2.created - revision1.created)); .sort((revision1, revision2) => revision2.created - revision1.created);
}, },
getRevisionContent(token, fileId, revisionId) { async getRevisionContent(token, fileId, revisionId) {
const { owner, repo } = getWorkspaceWithOwner(); const syncData = Provider.getContentSyncData(fileId);
return getSyncData(fileId) const { content } = await githubHelper.downloadFile({
.then(syncData => githubHelper ...getWorkspaceWithOwner(),
.downloadFile(token, owner, repo, revisionId, getAbsolutePath(syncData))) token,
.then(({ content }) => Provider.parseContent(content, `${fileId}/content`)); branch: revisionId,
path: getAbsolutePath(syncData),
});
return Provider.parseContent(content, `${fileId}/content`);
}, },
}); });

View File

@ -10,21 +10,17 @@ export default new Provider({
getToken() { getToken() {
return store.getters['workspace/syncToken']; return store.getters['workspace/syncToken'];
}, },
initWorkspace() { async initWorkspace() {
// Nothing much to do since the main workspace isn't necessarily synchronized // Nothing much to do since the main workspace isn't necessarily synchronized
return Promise.resolve()
.then(() => {
// Remove the URL hash // Remove the URL hash
utils.setQueryParams(); utils.setQueryParams();
// Return the main workspace // Return the main workspace
return store.getters['data/workspaces'].main; return store.getters['data/workspaces'].main;
});
}, },
getChanges() { async getChanges() {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const startPageToken = store.getters['data/localSettings'].syncStartPageToken; const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
return googleHelper.getChanges(syncToken, startPageToken, true) const result = await googleHelper.getChanges(syncToken, startPageToken, true);
.then((result) => {
const changes = result.changes.filter((change) => { const changes = result.changes.filter((change) => {
if (change.file) { if (change.file) {
// Parse item from file name // Parse item from file name
@ -46,29 +42,27 @@ export default new Provider({
}); });
syncStartPageToken = result.startPageToken; syncStartPageToken = result.startPageToken;
return changes; return changes;
});
}, },
onChangesApplied() { onChangesApplied() {
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
syncStartPageToken, syncStartPageToken,
}); });
}, },
saveSimpleItem(item, syncData, ifNotTooLate) { async saveSimpleItem(item, syncData, ifNotTooLate) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return googleHelper.uploadAppDataFile( const file = await googleHelper.uploadAppDataFile({
syncToken, token: syncToken,
JSON.stringify(item), name: JSON.stringify(item),
undefined, fileId: syncData && syncData.id,
syncData && syncData.id,
ifNotTooLate, ifNotTooLate,
) });
.then(file => ({
// Build sync data // Build sync data
return {
id: file.id, id: file.id,
itemId: item.id, itemId: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
})); };
}, },
removeItem(syncData, ifNotTooLate) { removeItem(syncData, ifNotTooLate) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
@ -77,14 +71,13 @@ export default new Provider({
downloadContent(token, syncLocation) { downloadContent(token, syncLocation) {
return this.downloadData(`${syncLocation.fileId}/content`); return this.downloadData(`${syncLocation.fileId}/content`);
}, },
downloadData(dataId) { async downloadData(dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId]; const syncData = store.getters['data/syncDataByItemId'][dataId];
if (!syncData) { if (!syncData) {
return Promise.resolve(); return null;
} }
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return googleHelper.downloadAppDataFile(syncToken, syncData.id) const data = await googleHelper.downloadAppDataFile(syncToken, syncData.id);
.then((data) => {
const item = utils.addItemHash(JSON.parse(data)); const item = utils.addItemHash(JSON.parse(data));
if (item.hash !== syncData.hash) { if (item.hash !== syncData.hash) {
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
@ -95,30 +88,27 @@ export default new Provider({
}); });
} }
return item; return item;
});
}, },
uploadContent(token, content, syncLocation, ifNotTooLate) { async uploadContent(token, content, syncLocation, ifNotTooLate) {
return this.uploadData(content, ifNotTooLate) await this.uploadData(content, ifNotTooLate);
.then(() => syncLocation); return syncLocation;
}, },
uploadData(item, ifNotTooLate) { async uploadData(item, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][item.id]; const syncData = store.getters['data/syncDataByItemId'][item.id];
if (syncData && syncData.hash === item.hash) { if (!syncData || syncData.hash !== item.hash) {
return Promise.resolve();
}
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return googleHelper.uploadAppDataFile( const file = await googleHelper.uploadAppDataFile({
syncToken, token: syncToken,
JSON.stringify({ name: JSON.stringify({
id: item.id, id: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}), }),
JSON.stringify(item), media: JSON.stringify(item),
syncData && syncData.id, fileId: syncData && syncData.id,
ifNotTooLate, ifNotTooLate,
) });
.then(file => store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
[file.id]: { [file.id]: {
// Build sync data // Build sync data
id: file.id, id: file.id,
@ -126,27 +116,22 @@ export default new Provider({
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}, },
})); });
},
listRevisions(token, fileId) {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
if (!syncData) {
return Promise.reject(); // No need for a proper error message.
} }
return googleHelper.getAppDataFileRevisions(token, syncData.id) },
.then(revisions => revisions.map(revision => ({ async listRevisions(token, fileId) {
const syncData = Provider.getContentSyncData(fileId);
const revisions = await googleHelper.getAppDataFileRevisions(token, syncData.id);
return revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: revision.lastModifyingUser && `go:${revision.lastModifyingUser.permissionId}`, sub: revision.lastModifyingUser && `go:${revision.lastModifyingUser.permissionId}`,
created: new Date(revision.modifiedTime).getTime(), created: new Date(revision.modifiedTime).getTime(),
})) }))
.sort((revision1, revision2) => revision2.created - revision1.created)); .sort((revision1, revision2) => revision2.created - revision1.created);
}, },
getRevisionContent(token, fileId, revisionId) { async getRevisionContent(token, fileId, revisionId) {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`]; const syncData = Provider.getContentSyncData(fileId);
if (!syncData) { const content = await googleHelper.downloadAppDataFileRevision(token, syncData.id, revisionId);
return Promise.reject(); // No need for a proper error message. return JSON.parse(content);
}
return googleHelper.downloadAppDataFileRevision(token, syncData.id, revisionId)
.then(content => JSON.parse(content));
}, },
}); });

View File

@ -17,22 +17,20 @@ export default new Provider({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.driveFileId}${token.name}`; return `${location.driveFileId}${token.name}`;
}, },
initAction() { async initAction() {
const state = googleHelper.driveState || {}; const state = googleHelper.driveState || {};
return state.userId && Promise.resolve() if (state.userId) {
.then(() => {
// Try to find the token corresponding to the user ID // Try to find the token corresponding to the user ID
const token = store.getters['data/googleTokens'][state.userId]; let token = store.getters['data/googleTokens'][state.userId];
// If not found or not enough permission, popup an OAuth2 window // If not found or not enough permission, popup an OAuth2 window
return token && token.isDrive ? token : store.dispatch('modal/open', { if (!token || !token.isDrive) {
type: 'googleDriveAccount', await store.dispatch('modal/open', { type: 'googleDriveAccount' });
onResolve: () => googleHelper.addDriveAccount( token = await googleHelper.addDriveAccount(
!store.getters['data/localSettings'].googleDriveRestrictedAccess, !store.getters['data/localSettings'].googleDriveRestrictedAccess,
state.userId, state.userId,
), );
}); }
})
.then((token) => {
const openWorkspaceIfExists = (file) => { const openWorkspaceIfExists = (file) => {
const folderId = file const folderId = file
&& file.appProperties && file.appProperties
@ -56,131 +54,121 @@ export default new Provider({
case 'create': case 'create':
default: default:
// See if folder is part of a workspace we can open // See if folder is part of a workspace we can open
return googleHelper.getFile(token, state.folderId) try {
.then((folder) => { const folder = await googleHelper.getFile(token, state.folderId);
folder.appProperties = folder.appProperties || {}; folder.appProperties = folder.appProperties || {};
googleHelper.driveActionFolder = folder; googleHelper.driveActionFolder = folder;
openWorkspaceIfExists(folder); openWorkspaceIfExists(folder);
}, (err) => { } catch (err) {
if (!err || err.status !== 404) { if (!err || err.status !== 404) {
throw err; throw err;
} }
// We received an HTTP 404 meaning we have no permission to read the folder // We received an HTTP 404 meaning we have no permission to read the folder
googleHelper.driveActionFolder = { id: state.folderId }; googleHelper.driveActionFolder = { id: state.folderId };
}); }
break;
case 'open': { case 'open': {
const getOneFile = (ids = state.ids || []) => { await utils.awaitSequence(state.ids || [], async (id) => {
const id = ids.shift(); const file = await googleHelper.getFile(token, id);
return id && googleHelper.getFile(token, id)
.then((file) => {
file.appProperties = file.appProperties || {}; file.appProperties = file.appProperties || {};
googleHelper.driveActionFiles.push(file); googleHelper.driveActionFiles.push(file);
return getOneFile(ids);
}); });
};
return getOneFile()
// Check if first file is part of a workspace // Check if first file is part of a workspace
.then(() => openWorkspaceIfExists(googleHelper.driveActionFiles[0])); openWorkspaceIfExists(googleHelper.driveActionFiles[0]);
}
} }
} }
});
}, },
performAction() { async performAction() {
return Promise.resolve()
.then(() => {
const state = googleHelper.driveState || {}; const state = googleHelper.driveState || {};
const token = store.getters['data/googleTokens'][state.userId]; const token = store.getters['data/googleTokens'][state.userId];
switch (token && state.action) { switch (token && state.action) {
case 'create': case 'create': {
return fileSvc.createFile({}, true) const file = await fileSvc.createFile({}, true);
.then((file) => {
store.commit('file/setCurrentId', file.id); store.commit('file/setCurrentId', file.id);
// Return a new syncLocation // Return a new syncLocation
return this.makeLocation(token, null, googleHelper.driveActionFolder.id); return this.makeLocation(token, null, googleHelper.driveActionFolder.id);
}); }
case 'open': case 'open':
return store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => this.openFiles(token, googleHelper.driveActionFiles), () => this.openFiles(token, googleHelper.driveActionFiles),
); );
return null;
default: default:
return null; return null;
} }
});
}, },
downloadContent(token, syncLocation) { async downloadContent(token, syncLocation) {
return googleHelper.downloadFile(token, syncLocation.driveFileId) const content = await googleHelper.downloadFile(token, syncLocation.driveFileId);
.then(content => Provider.parseContent(content, `${syncLocation.fileId}/content`)); return Provider.parseContent(content, `${syncLocation.fileId}/content`);
}, },
uploadContent(token, content, syncLocation, ifNotTooLate) { async uploadContent(token, content, syncLocation, ifNotTooLate) {
const file = store.state.file.itemMap[syncLocation.fileId]; const file = store.state.file.itemMap[syncLocation.fileId];
const name = utils.sanitizeName(file && file.name); const name = utils.sanitizeName(file && file.name);
const parents = []; const parents = [];
if (syncLocation.driveParentId) { if (syncLocation.driveParentId) {
parents.push(syncLocation.driveParentId); parents.push(syncLocation.driveParentId);
} }
return googleHelper.uploadFile( const driveFile = await googleHelper.uploadFile({
token, token,
name, name,
parents, parents,
undefined, media: Provider.serializeContent(content),
Provider.serializeContent(content), fileId: syncLocation.driveFileId,
undefined,
syncLocation.driveFileId,
undefined,
ifNotTooLate, ifNotTooLate,
) });
.then(driveFile => ({ return {
...syncLocation, ...syncLocation,
driveFileId: driveFile.id, driveFileId: driveFile.id,
})); };
}, },
publish(token, html, metadata, publishLocation) { async publish(token, html, metadata, publishLocation) {
return googleHelper.uploadFile( const driveFile = await googleHelper.uploadFile({
token, token,
metadata.title, name: metadata.title,
[], parents: [],
undefined, media: html,
html, mediaType: publishLocation.templateId ? 'text/html' : undefined,
publishLocation.templateId ? 'text/html' : undefined, fileId: publishLocation.driveFileId,
publishLocation.driveFileId, });
) return {
.then(driveFile => ({
...publishLocation, ...publishLocation,
driveFileId: driveFile.id, driveFileId: driveFile.id,
})); };
}, },
openFiles(token, driveFiles) { async openFiles(token, driveFiles) {
const openOneFile = () => { return utils.awaitSequence(driveFiles, async (driveFile) => {
const driveFile = driveFiles.shift(); // Check if the file exists and open it
if (!driveFile) { if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], {
return null;
}
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], {
providerId: this.id, providerId: this.id,
driveFileId: driveFile.id, driveFileId: driveFile.id,
})) { })) {
// File exists and has just been opened. Next... // Download content from Google Drive
return openOneFile();
}
// Download content from Google Drive and create the file
const syncLocation = { const syncLocation = {
driveFileId: driveFile.id, driveFileId: driveFile.id,
providerId: this.id, providerId: this.id,
sub: token.sub, sub: token.sub,
}; };
return this.downloadContent(token, syncLocation) let content;
.then(content => fileSvc.createFile({ try {
content = await this.downloadContent(token, syncLocation);
} catch (e) {
store.dispatch('notification/error', `Could not open file ${driveFile.id}.`);
return;
}
// Create the file
const item = await fileSvc.createFile({
name: driveFile.name, name: driveFile.name,
parentId: store.getters['file/current'].parentId, parentId: store.getters['file/current'].parentId,
text: content.text, text: content.text,
properties: content.properties, properties: content.properties,
discussions: content.discussions, discussions: content.discussions,
comments: content.comments, comments: content.comments,
}, true)) }, true);
.then((item) => {
store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
store.commit('syncLocation/setItem', { store.commit('syncLocation/setItem', {
...syncLocation, ...syncLocation,
@ -188,13 +176,8 @@ export default new Provider({
fileId: item.id, fileId: item.id,
}); });
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Google Drive.`); store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Google Drive.`);
}) }
.catch(() => { });
store.dispatch('notification/error', `Could not open file ${driveFile.id}.`);
})
.then(() => openOneFile());
};
return Promise.resolve(openOneFile());
}, },
makeLocation(token, fileId, folderId) { makeLocation(token, fileId, folderId) {
const location = { const location = {

View File

@ -4,13 +4,6 @@ import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
import fileSvc from '../fileSvc'; import fileSvc from '../fileSvc';
const getSyncData = (fileId) => {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
return syncData
? Promise.resolve(syncData)
: Promise.reject(); // No need for a proper error message.
};
let fileIdToOpen; let fileIdToOpen;
let syncStartPageToken; let syncStartPageToken;
@ -19,7 +12,7 @@ export default new Provider({
getToken() { getToken() {
return store.getters['workspace/syncToken']; return store.getters['workspace/syncToken'];
}, },
initWorkspace() { async initWorkspace() {
const makeWorkspaceParams = folderId => ({ const makeWorkspaceParams = folderId => ({
providerId: this.id, providerId: this.id,
folderId, folderId,
@ -31,67 +24,48 @@ export default new Provider({
const getWorkspace = folderId => const getWorkspace = folderId =>
store.getters['data/sanitizedWorkspaces'][makeWorkspaceId(folderId)]; store.getters['data/sanitizedWorkspaces'][makeWorkspaceId(folderId)];
const initFolder = (token, folder) => Promise.resolve({ const initFolder = async (token, folder) => {
const appProperties = {
folderId: folder.id, folderId: folder.id,
dataFolderId: folder.appProperties.dataFolderId, dataFolderId: folder.appProperties.dataFolderId,
trashFolderId: folder.appProperties.trashFolderId, trashFolderId: folder.appProperties.trashFolderId,
}) };
.then((properties) => {
// Make sure data folder exists // Make sure data folder exists
if (properties.dataFolderId) { if (!appProperties.dataFolderId) {
return properties; appProperties.dataFolderId = (await googleHelper.uploadFile({
}
return googleHelper.uploadFile(
token, token,
'.stackedit-data', name: '.stackedit-data',
[folder.id], parents: [folder.id],
{ folderId: folder.id }, appProperties: { folderId: folder.id },
undefined, mediaType: googleHelper.folderMimeType,
googleHelper.folderMimeType, })).id;
) }
.then(dataFolder => ({
...properties,
dataFolderId: dataFolder.id,
}));
})
.then((properties) => {
// Make sure trash folder exists // Make sure trash folder exists
if (properties.trashFolderId) { if (!appProperties.trashFolderId) {
return properties; appProperties.trashFolderId = (await googleHelper.uploadFile({
}
return googleHelper.uploadFile(
token, token,
'.stackedit-trash', name: '.stackedit-trash',
[folder.id], parents: [folder.id],
{ folderId: folder.id }, appProperties: { folderId: folder.id },
undefined, mediaType: googleHelper.folderMimeType,
googleHelper.folderMimeType, })).id;
) }
.then(trashFolder => ({
...properties,
trashFolderId: trashFolder.id,
}));
})
.then((properties) => {
// Update workspace if some properties are missing // Update workspace if some properties are missing
if (properties.folderId === folder.appProperties.folderId if (appProperties.folderId !== folder.appProperties.folderId
&& properties.dataFolderId === folder.appProperties.dataFolderId || appProperties.dataFolderId !== folder.appProperties.dataFolderId
&& properties.trashFolderId === folder.appProperties.trashFolderId || appProperties.trashFolderId !== folder.appProperties.trashFolderId
) { ) {
return properties; await googleHelper.uploadFile({
}
return googleHelper.uploadFile(
token, token,
undefined, appProperties,
undefined, mediaType: googleHelper.folderMimeType,
properties, fileId: folder.id,
undefined, });
googleHelper.folderMimeType, }
folder.id,
)
.then(() => properties);
})
.then((properties) => {
// Update workspace in the store // Update workspace in the store
const workspaceId = makeWorkspaceId(folder.id); const workspaceId = makeWorkspaceId(folder.id);
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspaces', {
@ -103,58 +77,56 @@ export default new Provider({
url: window.location.href, url: window.location.href,
folderId: folder.id, folderId: folder.id,
teamDriveId: folder.teamDriveId, teamDriveId: folder.teamDriveId,
dataFolderId: properties.dataFolderId, dataFolderId: appProperties.dataFolderId,
trashFolderId: properties.trashFolderId, trashFolderId: appProperties.trashFolderId,
}, },
}); });
};
// Return the workspace
return store.getters['data/sanitizedWorkspaces'][workspaceId];
});
return Promise.resolve()
.then(() => {
const workspace = getWorkspace(utils.queryParams.folderId);
// See if we already have a token
const googleTokens = store.getters['data/googleTokens'];
// Token sub is in the workspace or in the url if workspace is about to be created // Token sub is in the workspace or in the url if workspace is about to be created
const token = workspace ? googleTokens[workspace.sub] : googleTokens[utils.queryParams.sub]; const { sub } = getWorkspace(utils.queryParams.folderId) || utils.queryParams;
if (token && token.isDrive && token.driveFullAccess) { // See if we already have a token
return token; let token = store.getters['data/googleTokens'][sub];
}
// If no token has been found, popup an authorize window and get one // If no token has been found, popup an authorize window and get one
return store.dispatch('modal/workspaceGoogleRedirection', { if (!token || !token.isDrive || !token.driveFullAccess) {
onResolve: () => googleHelper.addDriveAccount(true, utils.queryParams.sub), await store.dispatch('modal/workspaceGoogleRedirection');
}); token = await googleHelper.addDriveAccount(true, utils.queryParams.sub);
}) }
.then(token => Promise.resolve()
let { folderId } = utils.queryParams;
// If no folderId is provided, create one // If no folderId is provided, create one
.then(() => utils.queryParams.folderId || googleHelper.uploadFile( if (!folderId) {
const folder = await googleHelper.uploadFile({
token, token,
'StackEdit workspace', name: 'StackEdit workspace',
[], parents: [],
undefined, mediaType: googleHelper.folderMimeType,
undefined, });
googleHelper.folderMimeType, await initFolder(token, {
)
.then(folder => initFolder(token, {
...folder, ...folder,
appProperties: {}, appProperties: {},
}) });
.then(() => folder.id))) folderId = folder.id;
// If workspace does not exist, initialize one }
.then(folderId => getWorkspace(folderId) || googleHelper.getFile(token, folderId)
.then((folder) => { // Init workspace
let workspace = getWorkspace(folderId);
if (!workspace) {
let folder;
try {
folder = googleHelper.getFile(token, folderId);
} catch (err) {
throw new Error(`Folder ${folderId} is not accessible. Make sure you have the right permissions.`);
}
folder.appProperties = folder.appProperties || {}; folder.appProperties = folder.appProperties || {};
const folderIdProperty = folder.appProperties.folderId; const folderIdProperty = folder.appProperties.folderId;
if (folderIdProperty && folderIdProperty !== folderId) { if (folderIdProperty && folderIdProperty !== folderId) {
throw new Error(`Folder ${folderId} is part of another workspace.`); throw new Error(`Folder ${folderId} is part of another workspace.`);
} }
return initFolder(token, folder); await initFolder(token, folder);
}, () => { workspace = getWorkspace(folderId);
throw new Error(`Folder ${folderId} is not accessible. Make sure you have the right permissions.`); }
}))
.then((workspace) => {
// Fix the URL hash // Fix the URL hash
utils.setQueryParams(makeWorkspaceParams(workspace.folderId)); utils.setQueryParams(makeWorkspaceParams(workspace.folderId));
if (workspace.url !== window.location.href) { if (workspace.url !== window.location.href) {
@ -166,17 +138,12 @@ export default new Provider({
}); });
} }
return store.getters['data/sanitizedWorkspaces'][workspace.id]; return store.getters['data/sanitizedWorkspaces'][workspace.id];
}));
}, },
performAction() { async performAction() {
return Promise.resolve()
.then(() => {
const state = googleHelper.driveState || {}; const state = googleHelper.driveState || {};
const token = this.getToken(); const token = this.getToken();
switch (token && state.action) { switch (token && state.action) {
case 'create': case 'create': {
return Promise.resolve()
.then(() => {
const driveFolder = googleHelper.driveActionFolder; const driveFolder = googleHelper.driveActionFolder;
let syncData = store.getters['data/syncData'][driveFolder.id]; let syncData = store.getters['data/syncData'][driveFolder.id];
if (!syncData && driveFolder.appProperties.id) { if (!syncData && driveFolder.appProperties.id) {
@ -196,17 +163,14 @@ export default new Provider({
[syncData.id]: syncData, [syncData.id]: syncData,
}); });
} }
return fileSvc.createFile({ const file = await fileSvc.createFile({
parentId: syncData && syncData.itemId, parentId: syncData && syncData.itemId,
}, true) }, true);
.then((file) => {
store.commit('file/setCurrentId', file.id); store.commit('file/setCurrentId', file.id);
// File will be created on next workspace sync // File will be created on next workspace sync
}); break;
}); }
case 'open': case 'open': {
return Promise.resolve()
.then(() => {
// open first file only // open first file only
const firstFile = googleHelper.driveActionFiles[0]; const firstFile = googleHelper.driveActionFiles[0];
const syncData = store.getters['data/syncData'][firstFile.id]; const syncData = store.getters['data/syncData'][firstFile.id];
@ -215,24 +179,24 @@ export default new Provider({
} else { } else {
store.commit('file/setCurrentId', syncData.itemId); store.commit('file/setCurrentId', syncData.itemId);
} }
}); break;
default: }
return null; default:
} }
});
}, },
getChanges() { async getChanges() {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const startPageToken = store.getters['data/localSettings'].syncStartPageToken; const lastStartPageToken = store.getters['data/localSettings'].syncStartPageToken;
return googleHelper.getChanges(syncToken, startPageToken, false, workspace.teamDriveId) const { changes, startPageToken } = await googleHelper
.then((result) => { .getChanges(syncToken, lastStartPageToken, false, workspace.teamDriveId);
// Collect possible parent IDs // Collect possible parent IDs
const parentIds = {}; const parentIds = {};
Object.entries(store.getters['data/syncDataByItemId']).forEach(([id, syncData]) => { Object.entries(store.getters['data/syncDataByItemId']).forEach(([id, syncData]) => {
parentIds[syncData.id] = id; parentIds[syncData.id] = id;
}); });
result.changes.forEach((change) => { changes.forEach((change) => {
const { id } = (change.file || {}).appProperties || {}; const { id } = (change.file || {}).appProperties || {};
if (id) { if (id) {
parentIds[change.fileId] = id; parentIds[change.fileId] = id;
@ -240,8 +204,8 @@ export default new Provider({
}); });
// Collect changes // Collect changes
const changes = []; const result = [];
result.changes.forEach((change) => { changes.forEach((change) => {
// Ignore changes on StackEdit own folders // Ignore changes on StackEdit own folders
if (change.fileId === workspace.folderId if (change.fileId === workspace.folderId
|| change.fileId === workspace.dataFolderId || change.fileId === workspace.dataFolderId
@ -335,41 +299,37 @@ export default new Provider({
// Push change // Push change
change.syncDataId = change.fileId; change.syncDataId = change.fileId;
changes.push(change); result.push(change);
if (contentChange) { if (contentChange) {
changes.push(contentChange); result.push(contentChange);
} }
}); });
syncStartPageToken = result.startPageToken; syncStartPageToken = startPageToken;
return changes; return result;
});
}, },
onChangesApplied() { onChangesApplied() {
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
syncStartPageToken, syncStartPageToken,
}); });
}, },
saveSimpleItem(item, syncData, ifNotTooLate) { async saveSimpleItem(item, syncData, ifNotTooLate) {
return Promise.resolve()
.then(() => {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
let file;
if (item.type !== 'file' && item.type !== 'folder') { if (item.type !== 'file' && item.type !== 'folder') {
return googleHelper.uploadFile( // For sync/publish locations, store item as filename
syncToken, file = await googleHelper.uploadFile({
JSON.stringify(item), token: syncToken,
[workspace.dataFolderId], name: JSON.stringify(item),
{ parents: [workspace.dataFolderId],
appProperties: {
folderId: workspace.folderId, folderId: workspace.folderId,
}, },
undefined, fileId: syncData && syncData.id,
undefined, oldParents: syncData && syncData.parentIds,
syncData && syncData.id,
syncData && syncData.parentIds,
ifNotTooLate, ifNotTooLate,
); });
} } else {
// For type `file` or `folder` // For type `file` or `folder`
const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId]; const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId];
let parentId; let parentId;
@ -381,45 +341,42 @@ export default new Provider({
parentId = workspace.folderId; parentId = workspace.folderId;
} }
return googleHelper.uploadFile( file = await googleHelper.uploadFile({
syncToken, token: syncToken,
item.name, name: item.name,
[parentId], parents: [parentId],
{ appProperties: {
id: item.id, id: item.id,
folderId: workspace.folderId, folderId: workspace.folderId,
}, },
undefined, mediaType: item.type === 'folder' ? googleHelper.folderMimeType : undefined,
item.type === 'folder' ? googleHelper.folderMimeType : undefined, fileId: syncData && syncData.id,
syncData && syncData.id, oldParents: syncData && syncData.parentIds,
syncData && syncData.parentIds,
ifNotTooLate, ifNotTooLate,
); });
}) }
.then(file => ({
// Build sync data // Build sync data
return {
id: file.id, id: file.id,
itemId: item.id, itemId: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
})); };
}, },
removeItem(syncData, ifNotTooLate) { async removeItem(syncData, ifNotTooLate) {
// Ignore content deletion // Ignore content deletion
if (syncData.type === 'content') { if (syncData.type !== 'content') {
return Promise.resolve();
}
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return googleHelper.removeFile(syncToken, syncData.id, ifNotTooLate); await googleHelper.removeFile(syncToken, syncData.id, ifNotTooLate);
}
}, },
downloadContent(token, syncLocation) { async downloadContent(token, syncLocation) {
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId]; const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId];
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`]; const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (!syncData || !contentSyncData) { if (!syncData || !contentSyncData) {
return Promise.resolve(); return null;
} }
return googleHelper.downloadFile(token, syncData.id) const content = await googleHelper.downloadFile(token, syncData.id);
.then((content) => {
const item = Provider.parseContent(content, `${syncLocation.fileId}/content`); const item = Provider.parseContent(content, `${syncLocation.fileId}/content`);
if (item.hash !== contentSyncData.hash) { if (item.hash !== contentSyncData.hash) {
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
@ -429,6 +386,7 @@ export default new Provider({
}, },
}); });
} }
// Open the file requested by action if it wasn't synced yet // Open the file requested by action if it wasn't synced yet
if (fileIdToOpen && fileIdToOpen === syncData.id) { if (fileIdToOpen && fileIdToOpen === syncData.id) {
fileIdToOpen = null; fileIdToOpen = null;
@ -438,16 +396,14 @@ export default new Provider({
}, 10); }, 10);
} }
return item; return item;
});
}, },
downloadData(dataId) { async downloadData(dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId]; const syncData = store.getters['data/syncDataByItemId'][dataId];
if (!syncData) { if (!syncData) {
return Promise.resolve(); return null;
} }
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return googleHelper.downloadFile(syncToken, syncData.id) const content = await googleHelper.downloadFile(syncToken, syncData.id);
.then((content) => {
const item = JSON.parse(content); const item = JSON.parse(content);
if (item.hash !== syncData.hash) { if (item.hash !== syncData.hash) {
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
@ -458,50 +414,37 @@ export default new Provider({
}); });
} }
return item; return item;
});
}, },
uploadContent(token, content, syncLocation, ifNotTooLate) { async uploadContent(token, content, syncLocation, ifNotTooLate) {
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`]; const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (contentSyncData && contentSyncData.hash === content.hash) { if (!contentSyncData || contentSyncData.hash !== content.hash) {
return Promise.resolve(syncLocation);
}
return Promise.resolve()
.then(() => {
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId]; const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId];
let file;
if (syncData) { if (syncData) {
// Only update file media // Only update file media
return googleHelper.uploadFile( file = await googleHelper.uploadFile({
token, token,
undefined, media: Provider.serializeContent(content),
undefined, fileId: syncData.id,
undefined,
Provider.serializeContent(content),
undefined,
syncData.id,
undefined,
ifNotTooLate, ifNotTooLate,
); });
} } else {
// Create file with media // Create file with media
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
const item = utils.deepCopy(store.state.file.itemMap[syncLocation.fileId]); const item = utils.deepCopy(store.state.file.itemMap[syncLocation.fileId]);
const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId]; const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId];
return googleHelper.uploadFile( file = await googleHelper.uploadFile({
token, token,
item.name, name: item.name,
[parentSyncData ? parentSyncData.id : workspace.folderId], parents: [parentSyncData ? parentSyncData.id : workspace.folderId],
{ appProperties: {
id: item.id, id: item.id,
folderId: workspace.folderId, folderId: workspace.folderId,
}, },
Provider.serializeContent(content), media: Provider.serializeContent(content),
undefined,
undefined,
undefined,
ifNotTooLate, ifNotTooLate,
) });
.then((file) => {
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
[file.id]: { [file.id]: {
id: file.id, id: file.id,
@ -510,10 +453,8 @@ export default new Provider({
hash: item.hash, hash: item.hash,
}, },
}); });
return file; }
}); store.dispatch('data/patchSyncData', {
})
.then(file => store.dispatch('data/patchSyncData', {
[`${file.id}/content`]: { [`${file.id}/content`]: {
// Build sync data // Build sync data
id: `${file.id}/content`, id: `${file.id}/content`,
@ -521,34 +462,32 @@ export default new Provider({
type: content.type, type: content.type,
hash: content.hash, hash: content.hash,
}, },
})) });
.then(() => syncLocation);
},
uploadData(item, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][item.id];
if (syncData && syncData.hash === item.hash) {
return Promise.resolve();
} }
return syncLocation;
},
async uploadData(item, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][item.id];
if (!syncData || syncData.hash !== item.hash) {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return googleHelper.uploadFile( const file = await googleHelper.uploadFile({
syncToken, token: syncToken,
JSON.stringify({ name: JSON.stringify({
id: item.id, id: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}), }),
[workspace.dataFolderId], parents: [workspace.dataFolderId],
{ appProperties: {
folderId: workspace.folderId, folderId: workspace.folderId,
}, },
JSON.stringify(item), media: JSON.stringify(item),
undefined, fileId: syncData && syncData.id,
syncData && syncData.id, oldParents: syncData && syncData.parentIds,
syncData && syncData.parentIds,
ifNotTooLate, ifNotTooLate,
) });
.then(file => store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
[file.id]: { [file.id]: {
// Build sync data // Build sync data
id: file.id, id: file.id,
@ -556,21 +495,22 @@ export default new Provider({
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}, },
})); });
}
}, },
listRevisions(token, fileId) { async listRevisions(token, fileId) {
return getSyncData(fileId) const syncData = Provider.getContentSyncData(fileId);
.then(syncData => googleHelper.getFileRevisions(token, syncData.id)) const revisions = await googleHelper.getFileRevisions(token, syncData.id);
.then(revisions => revisions.map(revision => ({ return revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId, sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
created: new Date(revision.modifiedTime).getTime(), created: new Date(revision.modifiedTime).getTime(),
})) }))
.sort((revision1, revision2) => revision2.created - revision1.created)); .sort((revision1, revision2) => revision2.created - revision1.created);
}, },
getRevisionContent(token, fileId, revisionId) { async getRevisionContent(token, fileId, revisionId) {
return getSyncData(fileId) const syncData = Provider.getContentSyncData(fileId);
.then(syncData => googleHelper.downloadFileRevision(token, syncData.id, revisionId)) const content = await googleHelper.downloadFileRevision(token, syncData.id, revisionId);
.then(content => Provider.parseContent(content, `${fileId}/content`)); return Provider.parseContent(content, `${fileId}/content`);
}, },
}); });

View File

@ -2,31 +2,37 @@ import networkSvc from '../../networkSvc';
import utils from '../../utils'; import utils from '../../utils';
import store from '../../../store'; import store from '../../../store';
const request = (token, options = {}) => { const request = async (token, options = {}) => {
const baseUrl = `${token.dbUrl}/`; const baseUrl = `${token.dbUrl}/`;
const getLastToken = () => store.getters['data/couchdbTokens'][token.sub]; const getLastToken = () => store.getters['data/couchdbTokens'][token.sub];
const ifUnauthorized = cb => (err) => { const assertUnauthorized = (err) => {
if (err.status !== 401) { if (err.status !== 401) {
throw err; throw err;
} }
return cb(err);
}; };
const onUnauthorized = () => networkSvc.request({ const onUnauthorized = async () => {
try {
const { name, password } = getLastToken();
await networkSvc.request({
method: 'POST', method: 'POST',
url: utils.resolveUrl(baseUrl, '../_session'), url: utils.resolveUrl(baseUrl, '../_session'),
withCredentials: true, withCredentials: true,
body: { body: {
name: getLastToken().name, name,
password: getLastToken().password, password,
}, },
}) });
.catch(ifUnauthorized(() => store.dispatch('modal/open', { } catch (err) {
assertUnauthorized(err);
await store.dispatch('modal/open', {
type: 'couchdbCredentials', type: 'couchdbCredentials',
token: getLastToken(), token: getLastToken(),
}) });
.then(onUnauthorized))); await onUnauthorized();
}
};
const config = { const config = {
...options, ...options,
@ -38,55 +44,75 @@ const request = (token, options = {}) => {
withCredentials: true, withCredentials: true,
}; };
return networkSvc.request(config) try {
.catch(ifUnauthorized(() => onUnauthorized() let res;
.then(() => networkSvc.request(config)))) try {
.then(res => res.body) res = await networkSvc.request(config);
.catch((err) => { } catch (err) {
assertUnauthorized(err);
await onUnauthorized();
res = await networkSvc.request(config);
}
return res.body;
} catch (err) {
if (err.status === 409) { if (err.status === 409) {
throw new Error('TOO_LATE'); throw new Error('TOO_LATE');
} }
throw err; throw err;
}); }
}; };
export default { export default {
/**
* http://docs.couchdb.org/en/2.1.1/api/database/common.html#db
*/
getDb(token) { getDb(token) {
return request(token); return request(token);
}, },
getChanges(token, lastSeq) {
/**
* http://docs.couchdb.org/en/2.1.1/api/database/changes.html#db-changes
*/
async getChanges(token, lastSeq) {
const result = { const result = {
changes: [], changes: [],
lastSeq,
}; };
const getPage = (since = 0) => request(token, { const getPage = async () => {
const body = await request(token, {
method: 'GET', method: 'GET',
path: '_changes', path: '_changes',
params: { params: {
since, since: result.lastSeq || 0,
include_docs: true, include_docs: true,
limit: 1000, limit: 1000,
}, },
})
.then((body) => {
result.changes = result.changes.concat(body.results);
if (body.pending) {
return getPage(body.last_seq);
}
result.lastSeq = body.last_seq;
return result;
}); });
result.changes = [...result.changes, ...body.results];
result.lastSeq = body.last_seq;
if (body.pending) {
return getPage();
}
return result;
};
return getPage(lastSeq); return getPage();
}, },
uploadDocument(
/**
* http://docs.couchdb.org/en/2.1.1/api/database/common.html#post--db
* http://docs.couchdb.org/en/2.1.1/api/document/common.html#put--db-docid
*/
async uploadDocument({
token, token,
item, item,
data = null, data = null,
dataType = null, dataType = null,
documentId = null, documentId = null,
rev = null, rev = null,
) { }) {
const options = { const options = {
method: 'POST', method: 'POST',
body: { item, time: Date.now() }, body: { item, time: Date.now() },
@ -110,34 +136,48 @@ export default {
} }
return request(token, options); return request(token, options);
}, },
removeDocument(token, documentId, rev) {
/**
* http://docs.couchdb.org/en/2.1.1/api/document/common.html#delete--db-docid
*/
async removeDocument(token, documentId, rev) {
return request(token, { return request(token, {
method: 'DELETE', method: 'DELETE',
path: documentId, path: documentId,
params: { rev }, params: { rev },
}); });
}, },
retrieveDocument(token, documentId, rev) {
/**
* http://docs.couchdb.org/en/2.1.1/api/document/common.html#get--db-docid
*/
async retrieveDocument(token, documentId, rev) {
return request(token, { return request(token, {
path: documentId, path: documentId,
params: { rev }, params: { rev },
}); });
}, },
retrieveDocumentWithAttachments(token, documentId, rev) {
return request(token, { /**
* http://docs.couchdb.org/en/2.1.1/api/document/common.html#get--db-docid
*/
async retrieveDocumentWithAttachments(token, documentId, rev) {
const body = await request(token, {
path: documentId, path: documentId,
params: { attachments: true, rev }, params: { attachments: true, rev },
}) });
.then((body) => {
body.attachments = {}; body.attachments = {};
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
Object.entries(body._attachments).forEach(([name, attachment]) => { Object.entries(body._attachments).forEach(([name, attachment]) => {
body.attachments[name] = utils.decodeBase64(attachment.data); body.attachments[name] = utils.decodeBase64(attachment.data);
}); });
return body; return body;
});
}, },
retrieveDocumentWithRevisions(token, documentId) {
/**
* http://docs.couchdb.org/en/2.1.1/api/document/common.html#get--db-docid
*/
async retrieveDocumentWithRevisions(token, documentId) {
return request(token, { return request(token, {
path: documentId, path: documentId,
params: { params: {

View File

@ -1,8 +1,6 @@
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
let Dropbox;
const getAppKey = (fullAccess) => { const getAppKey = (fullAccess) => {
if (fullAccess) { if (fullAccess) {
return 'lq6mwopab8wskas'; return 'lq6mwopab8wskas';
@ -22,89 +20,105 @@ const request = (token, options, args) => networkSvc.request({
}); });
export default { export default {
startOauth2(fullAccess, sub = null, silent = false) {
return networkSvc.startOauth2( /**
* https://www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize
*/
async startOauth2(fullAccess, sub = null, silent = false) {
const { accessToken } = await networkSvc.startOauth2(
'https://www.dropbox.com/oauth2/authorize', 'https://www.dropbox.com/oauth2/authorize',
{ {
client_id: getAppKey(fullAccess), client_id: getAppKey(fullAccess),
response_type: 'token', response_type: 'token',
}, },
silent, silent,
) );
// Call the user info endpoint // Call the user info endpoint
.then(({ accessToken }) => request({ accessToken }, { const { body } = await request({ accessToken }, {
method: 'POST', method: 'POST',
url: 'https://api.dropboxapi.com/2/users/get_current_account', url: 'https://api.dropboxapi.com/2/users/get_current_account',
}) });
.then((res) => {
// Check the returned sub consistency // Check the returned sub consistency
if (sub && `${res.body.account_id}` !== sub) { if (sub && `${body.account_id}` !== sub) {
throw new Error('Dropbox account ID not expected.'); throw new Error('Dropbox account ID not expected.');
} }
// Build token object including scopes and sub // Build token object including scopes and sub
const token = { const token = {
accessToken, accessToken,
name: res.body.name.display_name, name: body.name.display_name,
sub: `${res.body.account_id}`, sub: `${body.account_id}`,
fullAccess, fullAccess,
}; };
// Add token to dropboxTokens // Add token to dropboxTokens
store.dispatch('data/setDropboxToken', token); store.dispatch('data/setDropboxToken', token);
return token; return token;
}));
},
loadClientScript() {
if (Dropbox) {
return Promise.resolve();
}
return networkSvc.loadScript('https://www.dropbox.com/static/api/2/dropins.js')
.then(() => {
({ Dropbox } = window);
});
}, },
addAccount(fullAccess = false) { addAccount(fullAccess = false) {
return this.startOauth2(fullAccess); return this.startOauth2(fullAccess);
}, },
uploadFile(token, path, content, fileId) {
return request(token, { /**
* https://www.dropbox.com/developers/documentation/http/documentation#files-upload
*/
async uploadFile({
token,
path,
content,
fileId,
}) {
return (await request(token, {
method: 'POST', method: 'POST',
url: 'https://content.dropboxapi.com/2/files/upload', url: 'https://content.dropboxapi.com/2/files/upload',
body: content, body: content,
}, { }, {
path: fileId || path, path: fileId || path,
mode: 'overwrite', mode: 'overwrite',
}) })).body;
.then(res => res.body);
}, },
downloadFile(token, path, fileId) {
return request(token, { /**
* https://www.dropbox.com/developers/documentation/http/documentation#files-download
*/
async downloadFile({
token,
path,
fileId,
}) {
const res = await request(token, {
method: 'POST', method: 'POST',
url: 'https://content.dropboxapi.com/2/files/download', url: 'https://content.dropboxapi.com/2/files/download',
raw: true, raw: true,
}, { }, {
path: fileId || path, path: fileId || path,
}) });
.then(res => ({ return {
id: JSON.parse(res.headers['dropbox-api-result']).id, id: JSON.parse(res.headers['dropbox-api-result']).id,
content: res.body, content: res.body,
})); };
}, },
openChooser(token) {
return this.loadClientScript() /**
.then(() => new Promise((resolve) => { * https://www.dropbox.com/developers/chooser
Dropbox.appKey = getAppKey(token.fullAccess); */
Dropbox.choose({ async openChooser(token) {
if (!window.Dropbox) {
await networkSvc.loadScript('https://www.dropbox.com/static/api/2/dropins.js');
}
return new Promise((resolve) => {
window.Dropbox.appKey = getAppKey(token.fullAccess);
window.Dropbox.choose({
multiselect: true, multiselect: true,
linkType: 'direct', linkType: 'direct',
success: (files) => { success: files => resolve(files.map((file) => {
const paths = files.map((file) => {
const path = file.link.replace(/.*\/view\/[^/]*/, ''); const path = file.link.replace(/.*\/view\/[^/]*/, '');
return decodeURI(path); return decodeURI(path);
}); })),
resolve(paths);
},
cancel: () => resolve([]), cancel: () => resolve([]),
}); });
})); });
}, },
}; };

View File

@ -20,7 +20,8 @@ const request = (token, options) => networkSvc.request({
const repoRequest = (token, owner, repo, options) => request(token, { const repoRequest = (token, owner, repo, options) => request(token, {
...options, ...options,
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${options.url}`, url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${options.url}`,
}); })
.then(res => res.body);
const getCommitMessage = (name, path) => { const getCommitMessage = (name, path) => {
const message = store.getters['data/computedSettings'].github[name]; const message = store.getters['data/computedSettings'].github[name];
@ -28,95 +29,131 @@ const getCommitMessage = (name, path) => {
}; };
export default { export default {
startOauth2(scopes, sub = null, silent = false) {
return networkSvc.startOauth2( /**
* https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/
*/
async startOauth2(scopes, sub = null, silent = false) {
const { code } = await networkSvc.startOauth2(
'https://github.com/login/oauth/authorize', 'https://github.com/login/oauth/authorize',
{ {
client_id: clientId, client_id: clientId,
scope: scopes.join(' '), scope: scopes.join(' '),
}, },
silent, silent,
) );
// Exchange code with token // Exchange code with token
.then(data => networkSvc.request({ const accessToken = (await networkSvc.request({
method: 'GET', method: 'GET',
url: 'oauth2/githubToken', url: 'oauth2/githubToken',
params: { params: {
clientId, clientId,
code: data.code, code,
}, },
}) })).body;
.then(res => res.body))
// Call the user info endpoint // Call the user info endpoint
.then(accessToken => networkSvc.request({ const user = (await networkSvc.request({
method: 'GET', method: 'GET',
url: 'https://api.github.com/user', url: 'https://api.github.com/user',
params: { params: {
access_token: accessToken, access_token: accessToken,
}, },
}) })).body;
.then((res) => {
// Check the returned sub consistency // Check the returned sub consistency
if (sub && `${res.body.id}` !== sub) { if (sub && `${user.id}` !== sub) {
throw new Error('GitHub account ID not expected.'); throw new Error('GitHub account ID not expected.');
} }
// Build token object including scopes and sub // Build token object including scopes and sub
const token = { const token = {
scopes, scopes,
accessToken, accessToken,
name: res.body.login, name: user.login,
sub: `${res.body.id}`, sub: `${user.id}`,
repoFullAccess: scopes.indexOf('repo') !== -1, repoFullAccess: scopes.indexOf('repo') !== -1,
}; };
// Add token to githubTokens // Add token to githubTokens
store.dispatch('data/setGithubToken', token); store.dispatch('data/setGithubToken', token);
return token; return token;
}));
}, },
addAccount(repoFullAccess = false) { async addAccount(repoFullAccess = false) {
return this.startOauth2(getScopes({ repoFullAccess })); return this.startOauth2(getScopes({ repoFullAccess }));
}, },
getUser(userId) {
return networkSvc.request({ /**
* Getting a user from its userId is not feasible with API v3.
* Using an undocumented endpoint...
*/
async getUser(userId) {
const user = (await networkSvc.request({
url: `https://api.github.com/user/${userId}`, url: `https://api.github.com/user/${userId}`,
params: { params: {
t: Date.now(), // Prevent from caching t: Date.now(), // Prevent from caching
}, },
}) })).body;
.then((res) => {
store.commit('userInfo/addItem', { store.commit('userInfo/addItem', {
id: `gh:${res.body.id}`, id: `gh:${user.id}`,
name: res.body.login, name: user.login,
imageUrl: res.body.avatar_url || '', imageUrl: user.avatar_url || '',
});
return res.body;
}); });
return user;
}, },
getTree(token, owner, repo, sha) {
return repoRequest(token, owner, repo, { /**
url: `git/trees/${encodeURIComponent(sha)}?recursive=1`, * https://developer.github.com/v3/repos/commits/#get-a-single-commit
}) * https://developer.github.com/v3/git/trees/#get-a-tree
.then((res) => { */
if (res.body.truncated) { async getTree({
token,
owner,
repo,
branch,
}) {
const { commit } = (await repoRequest(token, owner, repo, {
url: `commits/${encodeURIComponent(branch)}`,
})).body;
const { tree, truncated } = (await repoRequest(token, owner, repo, {
url: `git/trees/${encodeURIComponent(commit.tree.sha)}?recursive=1`,
})).body;
if (truncated) {
throw new Error('Git tree too big. Please remove some files in the repository.'); throw new Error('Git tree too big. Please remove some files in the repository.');
} }
return res.body.tree; return tree;
});
}, },
getHeadTree(token, owner, repo, branch) {
return repoRequest(token, owner, repo, { /**
url: `commits/${encodeURIComponent(branch)}`, * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
}) */
.then(res => this.getTree(token, owner, repo, res.body.commit.tree.sha)); async getCommits({
}, token,
getCommits(token, owner, repo, sha, path) { owner,
repo,
sha,
path,
}) {
return repoRequest(token, owner, repo, { return repoRequest(token, owner, repo, {
url: 'commits', url: 'commits',
params: { sha, path }, params: { sha, path },
}) });
.then(res => res.body);
}, },
uploadFile(token, owner, repo, branch, path, content, sha) {
/**
* https://developer.github.com/v3/repos/contents/#create-a-file
* https://developer.github.com/v3/repos/contents/#update-a-file
*/
async uploadFile({
token,
owner,
repo,
branch,
path,
content,
sha,
}) {
return repoRequest(token, owner, repo, { return repoRequest(token, owner, repo, {
method: 'PUT', method: 'PUT',
url: `contents/${encodeURIComponent(path)}`, url: `contents/${encodeURIComponent(path)}`,
@ -126,10 +163,20 @@ export default {
sha, sha,
branch, branch,
}, },
}) });
.then(res => res.body);
}, },
removeFile(token, owner, repo, branch, path, sha) {
/**
* https://developer.github.com/v3/repos/contents/#delete-a-file
*/
async removeFile({
token,
owner,
repo,
branch,
path,
sha,
}) {
return repoRequest(token, owner, repo, { return repoRequest(token, owner, repo, {
method: 'DELETE', method: 'DELETE',
url: `contents/${encodeURIComponent(path)}`, url: `contents/${encodeURIComponent(path)}`,
@ -138,21 +185,42 @@ export default {
sha, sha,
branch, branch,
}, },
}) });
.then(res => res.body);
}, },
downloadFile(token, owner, repo, branch, path) {
return repoRequest(token, owner, repo, { /**
* https://developer.github.com/v3/repos/contents/#get-contents
*/
async downloadFile({
token,
owner,
repo,
branch,
path,
}) {
const body = await repoRequest(token, owner, repo, {
url: `contents/${encodeURIComponent(path)}`, url: `contents/${encodeURIComponent(path)}`,
params: { ref: branch }, params: { ref: branch },
}) });
.then(res => ({ return {
sha: res.body.sha, sha: body.sha,
content: utils.decodeBase64(res.body.content), content: utils.decodeBase64(body.content),
})); };
}, },
uploadGist(token, description, filename, content, isPublic, gistId) {
return request(token, gistId ? { /**
* https://developer.github.com/v3/gists/#create-a-gist
* https://developer.github.com/v3/gists/#edit-a-gist
*/
async uploadGist({
token,
description,
filename,
content,
isPublic,
gistId,
}) {
const { body } = await request(token, gistId ? {
method: 'PATCH', method: 'PATCH',
url: `https://api.github.com/gists/${gistId}`, url: `https://api.github.com/gists/${gistId}`,
body: { body: {
@ -175,19 +243,24 @@ export default {
}, },
public: isPublic, public: isPublic,
}, },
}) });
.then(res => res.body); return body;
}, },
downloadGist(token, gistId, filename) {
return request(token, { /**
* https://developer.github.com/v3/gists/#get-a-single-gist
*/
async downloadGist({
token,
gistId,
filename,
}) {
const result = (await request(token, {
url: `https://api.github.com/gists/${gistId}`, url: `https://api.github.com/gists/${gistId}`,
}) })).body.files[filename];
.then((res) => {
const result = res.body.files[filename];
if (!result) { if (!result) {
throw new Error('Gist file not found.'); throw new Error('Gist file not found.');
} }
return result.content; return result.content;
});
}, },
}; };

View File

@ -6,8 +6,6 @@ const clientId = GOOGLE_CLIENT_ID;
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk'; const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
const appsDomain = null; const appsDomain = null;
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h) const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
let gapi;
let google;
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata']; const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
const getDriveScopes = token => [token.driveFullAccess const getDriveScopes = token => [token.driveFullAccess
@ -17,8 +15,6 @@ const getDriveScopes = token => [token.driveFullAccess
const bloggerScopes = ['https://www.googleapis.com/auth/blogger']; const bloggerScopes = ['https://www.googleapis.com/auth/blogger'];
const photosScopes = ['https://www.googleapis.com/auth/photos']; const photosScopes = ['https://www.googleapis.com/auth/photos'];
const libraries = ['picker'];
const checkIdToken = (idToken) => { const checkIdToken = (idToken) => {
try { try {
const token = idToken.split('.'); const token = idToken.split('.');
@ -43,15 +39,16 @@ export default {
driveState, driveState,
driveActionFolder: null, driveActionFolder: null,
driveActionFiles: [], driveActionFiles: [],
request(token, options) { async $request(token, options) {
return networkSvc.request({ try {
return (await networkSvc.request({
...options, ...options,
headers: { headers: {
...options.headers || {}, ...options.headers || {},
Authorization: `Bearer ${token.accessToken}`, Authorization: `Bearer ${token.accessToken}`,
}, },
}, true) }, true)).body;
.catch((err) => { } catch (err) {
const { reason } = (((err.body || {}).error || {}).errors || [])[0] || {}; const { reason } = (((err.body || {}).error || {}).errors || [])[0] || {};
if (reason === 'authError') { if (reason === 'authError') {
// Mark the token as revoked and get a new one // Mark the token as revoked and get a new one
@ -60,13 +57,172 @@ export default {
expiresOn: 0, expiresOn: 0,
}); });
// Refresh token and retry // Refresh token and retry
return this.refreshToken(token, token.scopes) const refreshedToken = await this.refreshToken(token, token.scopes);
.then(refreshedToken => this.request(refreshedToken, options)); return this.$request(refreshedToken, options);
} }
throw err; throw err;
}); }
}, },
uploadFileInternal(
/**
* https://developers.google.com/identity/protocols/OpenIDConnect
*/
async startOauth2(scopes, sub = null, silent = false) {
const { accessToken, expiresIn, idToken } = await networkSvc.startOauth2(
'https://accounts.google.com/o/oauth2/v2/auth',
{
client_id: clientId,
response_type: 'token id_token',
scope: ['openid', ...scopes].join(' '),
hd: appsDomain,
login_hint: sub,
prompt: silent ? 'none' : null,
nonce: utils.uid(),
},
silent,
);
// Call the token info endpoint
const { body } = await networkSvc.request({
method: 'POST',
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
params: {
access_token: accessToken,
},
}, true);
// Check the returned client ID consistency
if (body.aud !== clientId) {
throw new Error('Client ID inconsistent.');
}
// Check the returned sub consistency
if (sub && `${body.sub}` !== sub) {
throw new Error('Google account ID not expected.');
}
// Build token object including scopes and sub
const existingToken = store.getters['data/googleTokens'][body.sub];
const token = {
scopes,
accessToken,
expiresOn: Date.now() + (expiresIn * 1000),
idToken,
sub: body.sub,
name: (existingToken || {}).name || 'Unknown',
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
isSponsor: false,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
};
try {
// Call the user info endpoint
token.name = (await this.getUser(token.sub)).displayName;
} catch (err) {
if (err.status === 404) {
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo.');
} else {
throw err;
}
}
if (existingToken) {
// We probably retrieved a new token with restricted scopes.
// That's no problem, token will be refreshed later with merged scopes.
// Restore flags
Object.assign(token, {
isLogin: existingToken.isLogin || token.isLogin,
isSponsor: existingToken.isSponsor,
isDrive: existingToken.isDrive || token.isDrive,
isBlogger: existingToken.isBlogger || token.isBlogger,
isPhotos: existingToken.isPhotos || token.isPhotos,
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
});
}
if (token.isLogin) {
try {
token.isSponsor = (await networkSvc.request({
method: 'GET',
url: 'userInfo',
params: {
idToken: token.idToken,
},
})).body.sponsorUntil > Date.now();
} catch (err) {
// Ignore
}
}
// Add token to googleTokens
store.dispatch('data/setGoogleToken', token);
return token;
},
async refreshToken(token, scopes = []) {
const { sub } = token;
const lastToken = store.getters['data/googleTokens'][sub];
const mergedScopes = [...new Set([
...scopes,
...lastToken.scopes,
])];
if (
// If we already have permissions for the requested scopes
mergedScopes.length === lastToken.scopes.length &&
// And lastToken is not expired
lastToken.expiresOn > Date.now() + tokenExpirationMargin &&
// And in case of a login token, ID token is still valid
(!lastToken.isLogin || checkIdToken(lastToken.idToken))
) {
return lastToken;
}
// New scopes are requested or existing token is about to expire.
// Try to get a new token in background
try {
return await this.startOauth2(mergedScopes, sub, true);
} catch (err) {
// If it fails try to popup a window
if (store.state.offline) {
throw err;
}
await store.dispatch('modal/providerRedirection', { providerName: 'Google' });
return this.startOauth2(mergedScopes, sub);
}
},
signin() {
return this.startOauth2(driveAppDataScopes);
},
addDriveAccount(fullAccess = false, sub = null) {
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
},
addBloggerAccount() {
return this.startOauth2(bloggerScopes);
},
addPhotosAccount() {
return this.startOauth2(photosScopes);
},
async getSponsorship(token) {
const refreshedToken = await this.refreshToken(token);
return networkSvc.request({
method: 'GET',
url: 'userInfo',
params: {
idToken: refreshedToken.idToken,
},
}, true);
},
/**
* https://developers.google.com/drive/v3/reference/files/create
* https://developers.google.com/drive/v3/reference/files/update
* https://developers.google.com/drive/v3/web/simple-upload
*/
async $uploadFile({
refreshedToken, refreshedToken,
name, name,
parents, parents,
@ -76,10 +232,9 @@ export default {
fileId = null, fileId = null,
oldParents = null, oldParents = null,
ifNotTooLate = cb => res => cb(res), ifNotTooLate = cb => res => cb(res),
) { }) {
return Promise.resolve()
// Refreshing a token can take a while if an oauth window pops up, make sure it's not too late // Refreshing a token can take a while if an oauth window pops up, make sure it's not too late
.then(ifNotTooLate(() => { return ifNotTooLate(() => {
const options = { const options = {
method: 'POST', method: 'POST',
url: 'https://www.googleapis.com/drive/v3/files', url: 'https://www.googleapis.com/drive/v3/files',
@ -118,7 +273,7 @@ export default {
'https://www.googleapis.com/', 'https://www.googleapis.com/',
'https://www.googleapis.com/upload/', 'https://www.googleapis.com/upload/',
); );
return this.request(refreshedToken, { return this.$request(refreshedToken, {
...options, ...options,
params: { params: {
...params, ...params,
@ -128,41 +283,123 @@ export default {
'Content-Type': `multipart/mixed; boundary="${boundary}"`, 'Content-Type': `multipart/mixed; boundary="${boundary}"`,
}, },
body: multipartRequestBody, body: multipartRequestBody,
}).then(res => res.body); });
} }
if (mediaType) { if (mediaType) {
metadata.mimeType = mediaType; metadata.mimeType = mediaType;
} }
return this.request(refreshedToken, { return this.$request(refreshedToken, {
...options, ...options,
body: metadata, body: metadata,
params, params,
}).then(res => res.body); });
})); });
}, },
downloadFileInternal(refreshedToken, id) { async uploadFile({
return this.request(refreshedToken, { token,
name,
parents,
appProperties,
media,
mediaType,
fileId,
oldParents,
ifNotTooLate,
}) {
const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
return this.$uploadFile({
refreshedToken,
name,
parents,
appProperties,
media,
mediaType,
fileId,
oldParents,
ifNotTooLate,
});
},
async uploadAppDataFile({
token,
name,
media,
fileId,
ifNotTooLate,
}) {
const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
return this.$uploadFile({
refreshedToken,
name,
parents: ['appDataFolder'],
media,
fileId,
ifNotTooLate,
});
},
/**
* https://developers.google.com/drive/v3/reference/files/get
*/
async getFile(token, id) {
const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
return this.$request(refreshedToken, {
method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${id}`,
params: {
fields: 'id,name,mimeType,appProperties,teamDriveId',
supportsTeamDrives: true,
},
});
},
/**
* https://developers.google.com/drive/v3/web/manage-downloads
*/
async $downloadFile(refreshedToken, id) {
return this.$request(refreshedToken, {
method: 'GET', method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`, url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
raw: true, raw: true,
}).then(res => res.body); });
}, },
removeFileInternal(refreshedToken, id, ifNotTooLate = cb => res => cb(res)) { async downloadFile(token, id) {
return Promise.resolve() const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
return this.$downloadFile(refreshedToken, id);
},
async downloadAppDataFile(token, id) {
const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
return this.$downloadFile(refreshedToken, id);
},
/**
* https://developers.google.com/drive/v3/reference/files/delete
*/
async $removeFile(refreshedToken, id, ifNotTooLate = cb => res => cb(res)) {
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late // Refreshing a token can take a while if an oauth window pops up, so check if it's too late
.then(ifNotTooLate(() => this.request(refreshedToken, { return ifNotTooLate(() => this.$request(refreshedToken, {
method: 'DELETE', method: 'DELETE',
url: `https://www.googleapis.com/drive/v3/files/${id}`, url: `https://www.googleapis.com/drive/v3/files/${id}`,
params: { params: {
supportsTeamDrives: true, supportsTeamDrives: true,
}, },
}))); }));
}, },
getFileRevisionsInternal(refreshedToken, id) { async removeFile(token, id, ifNotTooLate) {
return Promise.resolve() const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
.then(() => { return this.$removeFile(refreshedToken, id, ifNotTooLate);
const revisions = []; },
const getPage = pageToken => this.request(refreshedToken, { async removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
return this.$removeFile(refreshedToken, id, ifNotTooLate);
},
/**
* https://developers.google.com/drive/v3/reference/revisions/list
*/
async $getFileRevisions(refreshedToken, id) {
const allRevisions = [];
const getPage = async (pageToken) => {
const { revisions, nextPageToken } = await this.$request(refreshedToken, {
method: 'GET', method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${id}/revisions`, url: `https://www.googleapis.com/drive/v3/files/${id}/revisions`,
params: { params: {
@ -170,214 +407,70 @@ export default {
pageSize: 1000, pageSize: 1000,
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)', fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
}, },
}) });
.then((res) => { revisions.forEach((revision) => {
res.body.revisions.forEach((revision) => {
store.commit('userInfo/addItem', { store.commit('userInfo/addItem', {
id: revision.lastModifyingUser.permissionId, id: revision.lastModifyingUser.permissionId,
name: revision.lastModifyingUser.displayName, name: revision.lastModifyingUser.displayName,
imageUrl: revision.lastModifyingUser.photoLink, imageUrl: revision.lastModifyingUser.photoLink,
}); });
revisions.push(revision); allRevisions.push(revision);
}); });
if (res.body.nextPageToken) { if (nextPageToken) {
return getPage(res.body.nextPageToken); return getPage(nextPageToken);
} }
return revisions; return allRevisions;
}); };
return getPage(); return getPage();
},
async getFileRevisions(token, id) {
const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
return this.$getFileRevisions(refreshedToken, id);
},
async getAppDataFileRevisions(token, id) {
const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
return this.$getFileRevisions(refreshedToken, id);
},
/**
* https://developers.google.com/drive/v3/reference/revisions/get
*/
async $downloadFileRevision(refreshedToken, id, revisionId) {
return this.$request(refreshedToken, {
method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${id}/revisions/${revisionId}?alt=media`,
raw: true,
}); });
}, },
downloadFileRevisionInternal(refreshedToken, fileId, revisionId) { async downloadFileRevision(token, fileId, revisionId) {
return Promise.resolve() const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
.then(() => this.request(refreshedToken, { return this.$downloadFileRevision(refreshedToken, fileId, revisionId);
method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`,
raw: true,
}).then(res => res.body));
}, },
getUser(userId) { async downloadAppDataFileRevision(token, fileId, revisionId) {
return networkSvc.request({ const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
return this.$downloadFileRevision(refreshedToken, fileId, revisionId);
},
/**
* https://developers.google.com/+/web/api/rest/latest/people/get
*/
async getUser(userId) {
const { body } = await networkSvc.request({
method: 'GET', method: 'GET',
url: `https://www.googleapis.com/plus/v1/people/${userId}?key=${apiKey}`, url: `https://www.googleapis.com/plus/v1/people/${userId}?key=${apiKey}`,
}, true) }, true);
.then((res) => {
store.commit('userInfo/addItem', { store.commit('userInfo/addItem', {
id: `go:${res.body.id}`, id: `go:${body.id}`,
name: res.body.displayName, name: body.displayName,
imageUrl: (res.body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'), imageUrl: (body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
});
return res.body;
}); });
return body;
}, },
startOauth2(scopes, sub = null, silent = false) {
return networkSvc.startOauth2(
'https://accounts.google.com/o/oauth2/v2/auth',
{
client_id: clientId,
response_type: 'token id_token',
scope: ['openid', ...scopes].join(' '),
hd: appsDomain,
login_hint: sub,
prompt: silent ? 'none' : null,
nonce: utils.uid(),
},
silent,
)
// Call the token info endpoint
.then(data => networkSvc.request({
method: 'POST',
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
params: {
access_token: data.accessToken,
},
}, true).then((res) => {
// Check the returned client ID consistency
if (res.body.aud !== clientId) {
throw new Error('Client ID inconsistent.');
}
// Check the returned sub consistency
if (sub && `${res.body.sub}` !== sub) {
throw new Error('Google account ID not expected.');
}
// Build token object including scopes and sub
return {
scopes,
accessToken: data.accessToken,
expiresOn: Date.now() + (data.expiresIn * 1000),
idToken: data.idToken,
sub: `${res.body.sub}`,
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
isSponsor: false,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
};
}))
// Call the user info endpoint
.then(token => this.getUser(token.sub)
.catch((err) => {
if (err.status === 404) {
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo!');
} else {
throw err;
}
})
.then((user = {}) => {
const existingToken = store.getters['data/googleTokens'][token.sub];
// Add name to token
token.name = user.displayName || (existingToken && existingToken.name) || 'Unknown';
if (existingToken) {
// We probably retrieved a new token with restricted scopes.
// That's no problem, token will be refreshed later with merged scopes.
// Restore flags
Object.assign(token, {
isLogin: existingToken.isLogin || token.isLogin,
isSponsor: existingToken.isSponsor,
isDrive: existingToken.isDrive || token.isDrive,
isBlogger: existingToken.isBlogger || token.isBlogger,
isPhotos: existingToken.isPhotos || token.isPhotos,
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
});
}
return token.isLogin && networkSvc.request({
method: 'GET',
url: 'userInfo',
params: {
idToken: token.idToken,
},
})
.then((res) => {
token.isSponsor = res.body.sponsorUntil > Date.now();
}, () => {
// Ignore error
});
})
.then(() => {
// Add token to googleTokens
store.dispatch('data/setGoogleToken', token);
return token;
}));
},
refreshToken(token, scopes = []) {
const { sub } = token;
const lastToken = store.getters['data/googleTokens'][sub];
const mergedScopes = [...new Set([
...scopes,
...lastToken.scopes,
])];
return Promise.resolve() /**
.then(() => { * https://developers.google.com/drive/v3/reference/changes/list
if ( */
// If we already have permissions for the requested scopes async getChanges(token, startPageToken, isAppData, teamDriveId = null) {
mergedScopes.length === lastToken.scopes.length &&
// And lastToken is not expired
lastToken.expiresOn > Date.now() + tokenExpirationMargin &&
// And in case of a login token, ID token is still valid
(!lastToken.isLogin || checkIdToken(lastToken.idToken))
) {
return lastToken;
}
// New scopes are requested or existing token is going to expire.
// Try to get a new token in background
return this.startOauth2(mergedScopes, sub, true)
// If it fails try to popup a window
.catch((err) => {
if (store.state.offline) {
throw err;
}
return store.dispatch('modal/providerRedirection', {
providerName: 'Google',
onResolve: () => this.startOauth2(mergedScopes, sub),
});
});
});
},
loadClientScript() {
if (gapi) {
return Promise.resolve();
}
return networkSvc.loadScript('https://apis.google.com/js/api.js')
.then(() => Promise.all(libraries
.map(library => new Promise((resolve, reject) => window.gapi.load(library, {
callback: resolve,
onerror: reject,
timeout: 30000,
ontimeout: reject,
})))))
.then(() => {
({ gapi } = window);
({ google } = window);
});
},
getSponsorship(token) {
return this.refreshToken(token)
.then(refreshedToken => networkSvc.request({
method: 'GET',
url: 'userInfo',
params: {
idToken: refreshedToken.idToken,
},
}, true));
},
signin() {
return this.startOauth2(driveAppDataScopes);
},
addDriveAccount(fullAccess = false, sub = null) {
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
},
addBloggerAccount() {
return this.startOauth2(bloggerScopes);
},
addPhotosAccount() {
return this.startOauth2(photosScopes);
},
getChanges(token, startPageToken, isAppData, teamDriveId = null) {
const result = { const result = {
changes: [], changes: [],
}; };
@ -385,9 +478,13 @@ export default {
if (!isAppData) { if (!isAppData) {
fileFields += ',file/parents,file/mimeType,file/appProperties'; fileFields += ',file/parents,file/mimeType,file/appProperties';
} }
return this.refreshToken(token, isAppData ? driveAppDataScopes : getDriveScopes(token)) const refreshedToken = await this.refreshToken(
.then((refreshedToken) => { token,
const getPage = (pageToken = '1') => this.request(refreshedToken, { isAppData ? driveAppDataScopes : getDriveScopes(token),
);
const getPage = async (pageToken = '1') => {
const { changes, nextPageToken, newStartPageToken } = await this.$request(refreshedToken, {
method: 'GET', method: 'GET',
url: 'https://www.googleapis.com/drive/v3/changes', url: 'https://www.googleapis.com/drive/v3/changes',
params: { params: {
@ -399,104 +496,23 @@ export default {
includeTeamDriveItems: !!teamDriveId, includeTeamDriveItems: !!teamDriveId,
teamDriveId, teamDriveId,
}, },
}) });
.then((res) => { result.changes = [...result.changes, ...changes.filter(item => item.fileId)];
result.changes = result.changes.concat(res.body.changes.filter(item => item.fileId)); if (nextPageToken) {
if (res.body.nextPageToken) { return getPage(nextPageToken);
return getPage(res.body.nextPageToken);
} }
result.startPageToken = res.body.newStartPageToken; result.startPageToken = newStartPageToken;
return result; return result;
}); };
return getPage(startPageToken); return getPage(startPageToken);
});
}, },
uploadFile(
token, /**
name, * https://developers.google.com/blogger/docs/3.0/reference/blogs/getByUrl
parents, * https://developers.google.com/blogger/docs/3.0/reference/posts/insert
appProperties, * https://developers.google.com/blogger/docs/3.0/reference/posts/update
media, */
mediaType, async uploadBlogger({
fileId,
oldParents,
ifNotTooLate,
) {
return this.refreshToken(token, getDriveScopes(token))
.then(refreshedToken => this.uploadFileInternal(
refreshedToken,
name,
parents,
appProperties,
media,
mediaType,
fileId,
oldParents,
ifNotTooLate,
));
},
uploadAppDataFile(token, name, media, fileId, ifNotTooLate) {
return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this.uploadFileInternal(
refreshedToken,
name,
['appDataFolder'],
undefined,
media,
undefined,
fileId,
undefined,
ifNotTooLate,
));
},
getFile(token, id) {
return this.refreshToken(token, getDriveScopes(token))
.then(refreshedToken => this.request(refreshedToken, {
method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${id}`,
params: {
fields: 'id,name,mimeType,appProperties,teamDriveId',
supportsTeamDrives: true,
},
}))
.then(res => res.body);
},
downloadFile(token, id) {
return this.refreshToken(token, getDriveScopes(token))
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
},
downloadAppDataFile(token, id) {
return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
},
removeFile(token, id, ifNotTooLate) {
return this.refreshToken(token, getDriveScopes(token))
.then(refreshedToken => this.removeFileInternal(refreshedToken, id, ifNotTooLate));
},
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this.removeFileInternal(refreshedToken, id, ifNotTooLate));
},
getFileRevisions(token, id) {
return this.refreshToken(token, getDriveScopes(token))
.then(refreshedToken => this.getFileRevisionsInternal(refreshedToken, id));
},
getAppDataFileRevisions(token, id) {
return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this.getFileRevisionsInternal(refreshedToken, id));
},
downloadFileRevision(token, fileId, revisionId) {
return this.refreshToken(token, getDriveScopes(token))
.then(refreshedToken => this
.downloadFileRevisionInternal(refreshedToken, fileId, revisionId));
},
downloadAppDataFileRevision(token, fileId, revisionId) {
return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this
.downloadFileRevisionInternal(refreshedToken, fileId, revisionId));
},
uploadBlogger(
token, token,
blogUrl, blogUrl,
blogId, blogId,
@ -507,30 +523,28 @@ export default {
isDraft, isDraft,
published, published,
isPage, isPage,
) { }) {
return this.refreshToken(token, bloggerScopes) const refreshedToken = await this.refreshToken(token, bloggerScopes);
.then(refreshedToken => Promise.resolve()
.then(() => { // Get the blog ID
if (blogId) { const blog = { id: blogId };
return blogId; if (!blog.id) {
} blog.id = (await this.$request(refreshedToken, {
return this.request(refreshedToken, {
url: 'https://www.googleapis.com/blogger/v3/blogs/byurl', url: 'https://www.googleapis.com/blogger/v3/blogs/byurl',
params: { params: {
url: blogUrl, url: blogUrl,
}, },
}).then(res => res.body.id); })).id;
}) }
.then((resolvedBlogId) => {
// Create/update the post/page
const path = isPage ? 'pages' : 'posts'; const path = isPage ? 'pages' : 'posts';
const options = { let options = {
method: 'POST', method: 'POST',
url: `https://www.googleapis.com/blogger/v3/blogs/${resolvedBlogId}/${path}/`, url: `https://www.googleapis.com/blogger/v3/blogs/${blog.id}/${path}/`,
body: { body: {
kind: isPage ? 'blogger#page' : 'blogger#post', kind: isPage ? 'blogger#page' : 'blogger#post',
blog: { blog,
id: resolvedBlogId,
},
title, title,
content, content,
}, },
@ -547,14 +561,13 @@ export default {
options.url += postId; options.url += postId;
options.body.id = postId; options.body.id = postId;
} }
return this.request(refreshedToken, options) const post = await this.$request(refreshedToken, options);
.then(res => res.body);
})
.then((post) => {
if (isPage) { if (isPage) {
return post; return post;
} }
const options = {
// Revert/publish post
options = {
method: 'POST', method: 'POST',
url: `https://www.googleapis.com/blogger/v3/blogs/${post.blog.id}/posts/${post.id}/`, url: `https://www.googleapis.com/blogger/v3/blogs/${post.blog.id}/posts/${post.id}/`,
params: {}, params: {},
@ -567,15 +580,26 @@ export default {
options.params.publishDate = published.toISOString(); options.params.publishDate = published.toISOString();
} }
} }
return this.request(refreshedToken, options) return this.$request(refreshedToken, options);
.then(res => res.body);
}));
}, },
openPicker(token, type = 'doc') {
/**
* https://developers.google.com/picker/docs/
*/
async openPicker(token, type = 'doc') {
const scopes = type === 'img' ? photosScopes : getDriveScopes(token); const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
return this.loadClientScript() if (!window.google) {
.then(() => this.refreshToken(token, scopes)) await networkSvc.loadScript('https://apis.google.com/js/api.js');
.then(refreshedToken => new Promise((resolve) => { await new Promise((resolve, reject) => window.gapi.load('picker', {
callback: resolve,
onerror: reject,
timeout: 30000,
ontimeout: reject,
}));
}
const refreshedToken = await this.refreshToken(token, scopes);
const { google } = window;
return Promise((resolve) => {
let picker; let picker;
const pickerBuilder = new google.picker.PickerBuilder() const pickerBuilder = new google.picker.PickerBuilder()
.setOAuthToken(refreshedToken.accessToken) .setOAuthToken(refreshedToken.accessToken)
@ -636,6 +660,6 @@ export default {
} }
picker = pickerBuilder.build(); picker = pickerBuilder.build();
picker.setVisible(true); picker.setVisible(true);
})); });
}, },
}; };

View File

@ -10,11 +10,15 @@ const request = (token, options) => networkSvc.request({
...options.headers || {}, ...options.headers || {},
Authorization: `Bearer ${token.accessToken}`, Authorization: `Bearer ${token.accessToken}`,
}, },
}); })
.then(res => res.body);
export default { export default {
startOauth2(sub = null, silent = false) { /**
return networkSvc.startOauth2( * https://developer.wordpress.com/docs/oauth2/
*/
async startOauth2(sub = null, silent = false) {
const { accessToken, expiresIn } = await networkSvc.startOauth2(
'https://public-api.wordpress.com/oauth2/authorize', 'https://public-api.wordpress.com/oauth2/authorize',
{ {
client_id: clientId, client_id: clientId,
@ -22,49 +26,49 @@ export default {
scope: 'global', scope: 'global',
}, },
silent, silent,
) );
// Call the user info endpoint // Call the user info endpoint
.then(data => request({ accessToken: data.accessToken }, { const body = await request({ accessToken }, {
url: 'https://public-api.wordpress.com/rest/v1.1/me', url: 'https://public-api.wordpress.com/rest/v1.1/me',
}) });
.then((res) => {
// Check the returned sub consistency // Check the returned sub consistency
if (sub && `${res.body.ID}` !== sub) { if (sub && `${body.ID}` !== sub) {
throw new Error('WordPress account ID not expected.'); throw new Error('WordPress account ID not expected.');
} }
// Build token object including scopes and sub // Build token object including scopes and sub
const token = { const token = {
accessToken: data.accessToken, accessToken,
expiresOn: Date.now() + (data.expiresIn * 1000), expiresOn: Date.now() + (expiresIn * 1000),
name: res.body.display_name, name: body.display_name,
sub: `${res.body.ID}`, sub: `${body.ID}`,
}; };
// Add token to wordpressTokens // Add token to wordpressTokens
store.dispatch('data/setWordpressToken', token); store.dispatch('data/setWordpressToken', token);
return token; return token;
}));
}, },
refreshToken(token) { async refreshToken(token) {
const { sub } = token; const { sub } = token;
const lastToken = store.getters['data/wordpressTokens'][sub]; const lastToken = store.getters['data/wordpressTokens'][sub];
return Promise.resolve()
.then(() => {
if (lastToken.expiresOn > Date.now() + tokenExpirationMargin) { if (lastToken.expiresOn > Date.now() + tokenExpirationMargin) {
return lastToken; return lastToken;
} }
// Existing token is going to expire. // Existing token is going to expire.
// Try to get a new token in background // Try to get a new token in background
return store.dispatch('modal/providerRedirection', { await store.dispatch('modal/providerRedirection', { providerName: 'WordPress' });
providerName: 'WordPress', return this.startOauth2(sub);
onResolve: () => this.startOauth2(sub),
});
});
}, },
addAccount(fullAccess = false) { addAccount(fullAccess = false) {
return this.startOauth2(fullAccess); return this.startOauth2(fullAccess);
}, },
uploadPost(
/**
* https://developer.wordpress.com/docs/api/1.2/post/sites/%24site/posts/new/
* https://developer.wordpress.com/docs/api/1.2/post/sites/%24site/posts/%24post_ID/
*/
async uploadPost({
token, token,
domain, domain,
siteId, siteId,
@ -78,9 +82,9 @@ export default {
featuredImage, featuredImage,
status, status,
date, date,
) { }) {
return this.refreshToken(token) const refreshedToken = await this.refreshToken(token);
.then(refreshedToken => request(refreshedToken, { await request(refreshedToken, {
method: 'POST', method: 'POST',
url: `https://public-api.wordpress.com/rest/v1.2/sites/${siteId || domain}/posts/${postId || 'new'}`, url: `https://public-api.wordpress.com/rest/v1.2/sites/${siteId || domain}/posts/${postId || 'new'}`,
body: { body: {
@ -94,7 +98,6 @@ export default {
status, status,
date: date && date.toISOString(), date: date && date.toISOString(),
}, },
}) });
.then(res => res.body));
}, },
}; };

View File

@ -7,11 +7,16 @@ const request = (token, options) => networkSvc.request({
...options.headers || {}, ...options.headers || {},
Authorization: `Bearer ${token.accessToken}`, Authorization: `Bearer ${token.accessToken}`,
}, },
}); })
.then(res => res.body);
export default { export default {
startOauth2(subdomain, clientId, sub = null, silent = false) { /**
return networkSvc.startOauth2( * https://support.zendesk.com/hc/en-us/articles/203663836-Using-OAuth-authentication-with-your-application
*/
async startOauth2(subdomain, clientId, sub = null, silent = false) {
const { accessToken } = await networkSvc.startOauth2(
`https://${subdomain}.zendesk.com/oauth/authorizations/new`, `https://${subdomain}.zendesk.com/oauth/authorizations/new`,
{ {
client_id: clientId, client_id: clientId,
@ -19,33 +24,39 @@ export default {
scope: 'read hc:write', scope: 'read hc:write',
}, },
silent, silent,
) );
// Call the user info endpoint // Call the user info endpoint
.then(({ accessToken }) => request({ accessToken }, { const { user } = await request({ accessToken }, {
url: `https://${subdomain}.zendesk.com/api/v2/users/me.json`, url: `https://${subdomain}.zendesk.com/api/v2/users/me.json`,
}) });
.then((res) => { const uniqueSub = `${subdomain}/${user.id}`;
const uniqueSub = `${subdomain}/${res.body.user.id}`;
// Check the returned sub consistency // Check the returned sub consistency
if (sub && uniqueSub !== sub) { if (sub && uniqueSub !== sub) {
throw new Error('Zendesk account ID not expected.'); throw new Error('Zendesk account ID not expected.');
} }
// Build token object including scopes and sub // Build token object including scopes and sub
const token = { const token = {
accessToken, accessToken,
name: res.body.user.name, name: user.name,
subdomain, subdomain,
sub: uniqueSub, sub: uniqueSub,
}; };
// Add token to zendeskTokens // Add token to zendeskTokens
store.dispatch('data/setZendeskToken', token); store.dispatch('data/setZendeskToken', token);
return token; return token;
}));
}, },
addAccount(subdomain, clientId) { addAccount(subdomain, clientId) {
return this.startOauth2(subdomain, clientId); return this.startOauth2(subdomain, clientId);
}, },
uploadArticle(
/**
* https://developer.zendesk.com/rest_api/docs/help_center/articles
*/
async uploadArticle({
token, token,
sectionId, sectionId,
articleId, articleId,
@ -54,20 +65,25 @@ export default {
labels, labels,
locale, locale,
isDraft, isDraft,
) { }) {
const article = { const article = {
title, title,
body: content, body: content,
locale, locale,
draft: isDraft, draft: isDraft,
}; };
if (articleId) { if (articleId) {
return request(token, { // Update article
await request(token, {
method: 'PUT', method: 'PUT',
url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/articles/${articleId}/translations/${locale}.json`, url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/articles/${articleId}/translations/${locale}.json`,
body: { translation: article }, body: { translation: article },
}) });
.then(() => labels && request(token, {
// Add labels
if (labels) {
await request(token, {
method: 'PUT', method: 'PUT',
url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/articles/${articleId}.json`, url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/articles/${articleId}.json`,
body: { body: {
@ -75,17 +91,20 @@ export default {
label_names: labels, label_names: labels,
}, },
}, },
})) });
.then(() => articleId);
} }
return articleId;
}
// Create new article
if (labels) { if (labels) {
article.label_names = labels; article.label_names = labels;
} }
return request(token, { const body = await request(token, {
method: 'POST', method: 'POST',
url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/sections/${sectionId}/articles.json`, url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/sections/${sectionId}/articles.json`,
body: { article }, body: { article },
}) });
.then(res => `${res.body.article.id}`); return `${body.article.id}`;
}, },
}; };

View File

@ -14,27 +14,18 @@ export default new Provider({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.postId}${location.domain}${token.name}`; return `${location.postId}${location.domain}${token.name}`;
}, },
publish(token, html, metadata, publishLocation) { async publish(token, html, metadata, publishLocation) {
return wordpressHelper.uploadPost( const post = await wordpressHelper.uploadPost({
...publishLocation,
...metadata,
token, token,
publishLocation.domain, content: html,
publishLocation.siteId, });
publishLocation.postId, return {
metadata.title,
html,
metadata.tags,
metadata.categories,
metadata.excerpt,
metadata.author,
metadata.featuredImage,
metadata.status,
metadata.date,
)
.then(post => ({
...publishLocation, ...publishLocation,
siteId: `${post.site_ID}`, siteId: `${post.site_ID}`,
postId: `${post.ID}`, postId: `${post.ID}`,
})); };
}, },
makeLocation(token, domain, postId) { makeLocation(token, domain, postId) {
const location = { const location = {

View File

@ -15,21 +15,19 @@ export default new Provider({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.articleId}${token.name}${token.subdomain}`; return `${location.articleId}${token.name}${token.subdomain}`;
}, },
publish(token, html, metadata, publishLocation) { async publish(token, html, metadata, publishLocation) {
return zendeskHelper.uploadArticle( const articleId = await zendeskHelper.uploadArticle({
...publishLocation,
token, token,
publishLocation.sectionId, title: metadata.title,
publishLocation.articleId, content: html,
metadata.title, labels: metadata.tags,
html, isDraft: metadata.status === 'draft',
metadata.tags, });
publishLocation.locale, return {
metadata.status === 'draft',
)
.then(articleId => ({
...publishLocation, ...publishLocation,
articleId, articleId,
})); };
}, },
makeLocation(token, sectionId, locale, articleId) { makeLocation(token, sectionId, locale, articleId) {
const location = { const location = {

View File

@ -38,12 +38,11 @@ const ensureDate = (value, defaultValue) => {
return new Date(`${value}`); return new Date(`${value}`);
}; };
function publish(publishLocation) { const publish = async (publishLocation) => {
const { fileId } = publishLocation; const { fileId } = publishLocation;
const template = store.getters['data/allTemplates'][publishLocation.templateId]; const template = store.getters['data/allTemplates'][publishLocation.templateId];
return exportSvc.applyTemplate(fileId, template) const html = await exportSvc.applyTemplate(fileId, template);
.then(html => localDbSvc.loadItem(`${fileId}/content`) const content = await localDbSvc.loadItem(`${fileId}/content`);
.then((content) => {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemMap[fileId];
const properties = utils.computeProperties(content.properties); const properties = utils.computeProperties(content.properties);
const provider = providerRegistry.providers[publishLocation.providerId]; const provider = providerRegistry.providers[publishLocation.providerId];
@ -59,25 +58,21 @@ function publish(publishLocation) {
date: ensureDate(properties.date, new Date()), date: ensureDate(properties.date, new Date()),
}; };
return provider.publish(token, html, metadata, publishLocation); return provider.publish(token, html, metadata, publishLocation);
})); };
}
function publishFile(fileId) { const publishFile = async (fileId) => {
let counter = 0; let counter = 0;
return loadContent(fileId) await loadContent(fileId);
.then(() => {
const publishLocations = [ const publishLocations = [
...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [], ...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [],
]; ];
const publishOneContentLocation = () => { try {
const publishLocation = publishLocations.shift(); await utils.awaitSequence(publishLocations, async (publishLocation) => {
if (!publishLocation) { await store.dispatch('queue/doWithLocation', {
return null;
}
return store.dispatch('queue/doWithLocation', {
location: publishLocation, location: publishLocation,
promise: publish(publishLocation) action: async () => {
.then((publishLocationToStore) => { const publishLocationToStore = await publish(publishLocation);
try {
// Replace publish location if modified // Replace publish location if modified
if (utils.serializeObject(publishLocation) !== if (utils.serializeObject(publishLocation) !==
utils.serializeObject(publishLocationToStore) utils.serializeObject(publishLocationToStore)
@ -85,33 +80,24 @@ function publishFile(fileId) {
store.commit('publishLocation/patchItem', publishLocationToStore); store.commit('publishLocation/patchItem', publishLocationToStore);
} }
counter += 1; counter += 1;
return publishOneContentLocation(); } catch (err) {
}, (err) => {
if (store.state.offline) { if (store.state.offline) {
throw err; throw err;
} }
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err); store.dispatch('notification/error', err);
return publishOneContentLocation(); }
}), },
});
}); });
};
return publishOneContentLocation();
})
.then(() => {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemMap[fileId];
store.dispatch('notification/info', `"${file.name}" was published to ${counter} location(s).`); store.dispatch('notification/info', `"${file.name}" was published to ${counter} location(s).`);
}) } finally {
.then( await localDbSvc.unloadContents();
() => localDbSvc.unloadContents(), }
err => localDbSvc.unloadContents() };
.then(() => {
throw err;
}),
);
}
function requestPublish() { const requestPublish = () => {
// No publish in light mode // No publish in light mode
if (store.state.light) { if (store.state.light) {
return; return;
@ -135,21 +121,21 @@ function requestPublish() {
intervalId = utils.setInterval(() => attempt(), 1000); intervalId = utils.setInterval(() => attempt(), 1000);
attempt(); attempt();
})); }));
} };
function createPublishLocation(publishLocation) { const createPublishLocation = (publishLocation) => {
publishLocation.id = utils.uid(); publishLocation.id = utils.uid();
const currentFile = store.getters['file/current']; const currentFile = store.getters['file/current'];
publishLocation.fileId = currentFile.id; publishLocation.fileId = currentFile.id;
store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => publish(publishLocation) async () => {
.then((publishLocationToStore) => { const publishLocationToStore = await publish(publishLocation);
store.commit('publishLocation/setItem', publishLocationToStore); store.commit('publishLocation/setItem', publishLocationToStore);
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`); store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
}), },
); );
} };
export default { export default {
requestPublish, requestPublish,

View File

@ -1,11 +1,12 @@
function SectionDimension(startOffset, endOffset) { class SectionDimension {
constructor(startOffset, endOffset) {
this.startOffset = startOffset; this.startOffset = startOffset;
this.endOffset = endOffset; this.endOffset = endOffset;
this.height = endOffset - startOffset; this.height = endOffset - startOffset;
}
} }
function dimensionNormalizer(dimensionName) { const dimensionNormalizer = dimensionName => (editorSvc) => {
return (editorSvc) => {
const dimensionList = editorSvc.previewCtx.sectionDescList const dimensionList = editorSvc.previewCtx.sectionDescList
.map(sectionDesc => sectionDesc[dimensionName]); .map(sectionDesc => sectionDesc[dimensionName]);
let dimension; let dimension;
@ -33,14 +34,13 @@ function dimensionNormalizer(dimensionName) {
} }
} }
} }
}; };
}
const normalizeEditorDimensions = dimensionNormalizer('editorDimension'); const normalizeEditorDimensions = dimensionNormalizer('editorDimension');
const normalizePreviewDimensions = dimensionNormalizer('previewDimension'); const normalizePreviewDimensions = dimensionNormalizer('previewDimension');
const normalizeTocDimensions = dimensionNormalizer('tocDimension'); const normalizeTocDimensions = dimensionNormalizer('tocDimension');
function measureSectionDimensions(editorSvc) { const measureSectionDimensions = (editorSvc) => {
let editorSectionOffset = 0; let editorSectionOffset = 0;
let previewSectionOffset = 0; let previewSectionOffset = 0;
let tocSectionOffset = 0; let tocSectionOffset = 0;
@ -106,7 +106,7 @@ function measureSectionDimensions(editorSvc) {
normalizeEditorDimensions(editorSvc); normalizeEditorDimensions(editorSvc);
normalizePreviewDimensions(editorSvc); normalizePreviewDimensions(editorSvc);
normalizeTocDimensions(editorSvc); normalizeTocDimensions(editorSvc);
} };
export default { export default {
measureSectionDimensions, measureSectionDimensions,

View File

@ -8,20 +8,19 @@ let lastCheck = 0;
const appId = 'ESTHdCYOi18iLhhO'; const appId = 'ESTHdCYOi18iLhhO';
let monetize; let monetize;
const getMonetize = () => Promise.resolve() const getMonetize = async () => {
.then(() => networkSvc.loadScript('https://cdn.monetizejs.com/api/js/latest/monetize.min.js')) await networkSvc.loadScript('https://cdn.monetizejs.com/api/js/latest/monetize.min.js');
.then(() => {
monetize = monetize || new window.MonetizeJS({ monetize = monetize || new window.MonetizeJS({
applicationID: appId, applicationID: appId,
}); });
}); };
const isGoogleSponsor = () => { const isGoogleSponsor = () => {
const sponsorToken = store.getters['workspace/sponsorToken']; const sponsorToken = store.getters['workspace/sponsorToken'];
return sponsorToken && sponsorToken.isSponsor; return sponsorToken && sponsorToken.isSponsor;
}; };
const checkPayment = () => { const checkPayment = async () => {
const currentDate = Date.now(); const currentDate = Date.now();
if (!isGoogleSponsor() if (!isGoogleSponsor()
&& networkSvc.isUserActive() && networkSvc.isUserActive()
@ -30,15 +29,15 @@ const checkPayment = () => {
&& lastCheck + checkPaymentEvery < currentDate && lastCheck + checkPaymentEvery < currentDate
) { ) {
lastCheck = currentDate; lastCheck = currentDate;
getMonetize() await getMonetize();
.then(() => monetize.getPaymentsImmediate((err, payments) => { monetize.getPaymentsImmediate((err, payments) => {
const isSponsor = payments && payments.app === appId && ( const isSponsor = payments && payments.app === appId && (
(payments.chargeOption && payments.chargeOption.alias === 'once') || (payments.chargeOption && payments.chargeOption.alias === 'once') ||
(payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly')); (payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly'));
if (isSponsor !== store.state.monetizeSponsor) { if (isSponsor !== store.state.monetizeSponsor) {
store.commit('setMonetizeSponsor', isSponsor); store.commit('setMonetizeSponsor', isSponsor);
} }
})); });
} }
}; };
@ -46,12 +45,11 @@ export default {
init: () => { init: () => {
utils.setInterval(checkPayment, 2000); utils.setInterval(checkPayment, 2000);
}, },
getToken() { async getToken() {
if (isGoogleSponsor() || store.state.offline) { if (isGoogleSponsor() || store.state.offline) {
return Promise.resolve(); return null;
} }
return getMonetize() await getMonetize();
.then(() => new Promise(resolve => return new Promise(resolve => monetize.getTokenImmediate((err, result) => resolve(result)));
monetize.getTokenImmediate((err, result) => resolve(result))));
}, },
}; };

View File

@ -50,36 +50,36 @@ const isSyncPossible = () => !store.state.offline &&
/** /**
* Return true if we are the many window, ie we have the lastSyncActivity lock. * Return true if we are the many window, ie we have the lastSyncActivity lock.
*/ */
function isSyncWindow() { const isSyncWindow = () => {
const storedLastSyncActivity = getLastStoredSyncActivity(); const storedLastSyncActivity = getLastStoredSyncActivity();
return lastSyncActivity === storedLastSyncActivity || return lastSyncActivity === storedLastSyncActivity ||
Date.now() > inactivityThreshold + storedLastSyncActivity; Date.now() > inactivityThreshold + storedLastSyncActivity;
} };
/** /**
* Return true if auto sync can start, ie if lastSyncActivity is old enough. * Return true if auto sync can start, ie if lastSyncActivity is old enough.
*/ */
function isAutoSyncReady() { const isAutoSyncReady = () => {
let { autoSyncEvery } = store.getters['data/computedSettings']; let { autoSyncEvery } = store.getters['data/computedSettings'];
if (autoSyncEvery < minAutoSyncEvery) { if (autoSyncEvery < minAutoSyncEvery) {
autoSyncEvery = minAutoSyncEvery; autoSyncEvery = minAutoSyncEvery;
} }
return Date.now() > autoSyncEvery + getLastStoredSyncActivity(); return Date.now() > autoSyncEvery + getLastStoredSyncActivity();
} };
/** /**
* Update the lastSyncActivity, assuming we have the lock. * Update the lastSyncActivity, assuming we have the lock.
*/ */
function setLastSyncActivity() { const setLastSyncActivity = () => {
const currentDate = Date.now(); const currentDate = Date.now();
lastSyncActivity = currentDate; lastSyncActivity = currentDate;
localStorage.setItem(store.getters['workspace/lastSyncActivityKey'], currentDate); localStorage.setItem(store.getters['workspace/lastSyncActivityKey'], currentDate);
} };
/** /**
* Upgrade hashes if syncedContent is from an old version * Upgrade hashes if syncedContent is from an old version
*/ */
function upgradeSyncedContent(syncedContent) { const upgradeSyncedContent = (syncedContent) => {
if (syncedContent.v) { if (syncedContent.v) {
return syncedContent; return syncedContent;
} }
@ -100,12 +100,12 @@ function upgradeSyncedContent(syncedContent) {
syncHistory, syncHistory,
v: 1, v: 1,
}; };
} };
/** /**
* Clean a syncedContent. * Clean a syncedContent.
*/ */
function cleanSyncedContent(syncedContent) { const cleanSyncedContent = (syncedContent) => {
// Clean syncHistory from removed syncLocations // Clean syncHistory from removed syncLocations
Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => { Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => {
if (syncLocationId !== 'main' && !store.state.syncLocation.itemMap[syncLocationId]) { if (syncLocationId !== 'main' && !store.state.syncLocation.itemMap[syncLocationId]) {
@ -123,12 +123,12 @@ function cleanSyncedContent(syncedContent) {
delete syncedContent.historyData[hash]; delete syncedContent.historyData[hash];
} }
}); });
} };
/** /**
* Apply changes retrieved from the main provider. Update sync data accordingly. * Apply changes retrieved from the main provider. Update sync data accordingly.
*/ */
function applyChanges(changes) { const applyChanges = (changes) => {
const storeItemMap = { ...store.getters.allItemMap }; const storeItemMap = { ...store.getters.allItemMap };
const syncData = { ...store.getters['data/syncData'] }; const syncData = { ...store.getters['data/syncData'] };
let saveSyncData = false; let saveSyncData = false;
@ -170,12 +170,12 @@ function applyChanges(changes) {
if (saveSyncData) { if (saveSyncData) {
store.dispatch('data/setSyncData', syncData); store.dispatch('data/setSyncData', syncData);
} }
} };
/** /**
* Create a sync location by uploading the current file content. * Create a sync location by uploading the current file content.
*/ */
function createSyncLocation(syncLocation) { const createSyncLocation = (syncLocation) => {
syncLocation.id = utils.uid(); syncLocation.id = utils.uid();
const currentFile = store.getters['file/current']; const currentFile = store.getters['file/current'];
const fileId = currentFile.id; const fileId = currentFile.id;
@ -184,15 +184,14 @@ function createSyncLocation(syncLocation) {
const content = utils.deepCopy(store.getters['content/current']); const content = utils.deepCopy(store.getters['content/current']);
store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => { async () => {
const provider = providerRegistry.providers[syncLocation.providerId]; const provider = providerRegistry.providers[syncLocation.providerId];
const token = provider.getToken(syncLocation); const token = provider.getToken(syncLocation);
return provider.uploadContent(token, { const syncLocationToStore = await provider.uploadContent(token, {
...content, ...content,
history: [content.hash], history: [content.hash],
}, syncLocation) }, syncLocation);
.then(syncLocationToStore => localDbSvc.loadSyncedContent(fileId) await localDbSvc.loadSyncedContent(fileId);
.then(() => {
const newSyncedContent = utils.deepCopy(upgradeSyncedContent(store.state.syncedContent.itemMap[`${fileId}/syncedContent`])); const newSyncedContent = utils.deepCopy(upgradeSyncedContent(store.state.syncedContent.itemMap[`${fileId}/syncedContent`]));
const newSyncHistoryItem = []; const newSyncHistoryItem = [];
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem; newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
@ -203,10 +202,9 @@ function createSyncLocation(syncLocation) {
store.commit('syncedContent/patchItem', newSyncedContent); store.commit('syncedContent/patchItem', newSyncedContent);
store.commit('syncLocation/setItem', syncLocationToStore); store.commit('syncLocation/setItem', syncLocationToStore);
store.dispatch('notification/info', `A new synchronized location was added to "${currentFile.name}".`); store.dispatch('notification/info', `A new synchronized location was added to "${currentFile.name}".`);
}));
}, },
); );
} };
// Prevent from sending new data too long after old data has been fetched // Prevent from sending new data too long after old data has been fetched
const tooLateChecker = (timeout) => { const tooLateChecker = (timeout) => {
@ -219,33 +217,13 @@ const tooLateChecker = (timeout) => {
}; };
}; };
class SyncContext { const isTempFile = (fileId) => {
restart = false;
attempted = {};
}
/**
* Sync one file with all its locations.
*/
function syncFile(fileId, syncContext = new SyncContext()) {
syncContext.attempted[`${fileId}/content`] = true;
return localDbSvc.loadSyncedContent(fileId)
.then(() => localDbSvc.loadItem(`${fileId}/content`)
.catch(() => {})) // Item may not exist if content has not been downloaded yet
.then(() => {
const getFile = () => store.state.file.itemMap[fileId];
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
const getSyncedContent = () => upgradeSyncedContent(store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
const isTempFile = () => {
if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) { if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) {
// If file has already been synced, it's not a temp file // If file has already been synced, it's not a temp file
return false; return false;
} }
const file = getFile(); const file = store.state.file.itemMap[fileId];
const content = getContent(); const content = store.state.content.itemMap[`${fileId}/content`];
if (!file || !content) { if (!file || !content) {
return false; return false;
} }
@ -257,7 +235,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [], ...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [],
]; ];
if (locations.length) { if (locations.length) {
// If file has explicit sync/publish locations, it's not a temp file // If file has sync/publish locations, it's not a temp file
return false; return false;
} }
// Return true if it's a welcome file that has no discussion // Return true if it's a welcome file that has no discussion
@ -265,41 +243,59 @@ function syncFile(fileId, syncContext = new SyncContext()) {
const hash = utils.hash(content.text); const hash = utils.hash(content.text);
const hasDiscussions = Object.keys(content.discussions).length; const hasDiscussions = Object.keys(content.discussions).length;
return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions; return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
}; };
if (isTempFile()) { class SyncContext {
return null; restart = false;
attempted = {};
}
/**
* Sync one file with all its locations.
*/
const syncFile = async (fileId, syncContext = new SyncContext()) => {
syncContext.attempted[`${fileId}/content`] = true;
await localDbSvc.loadSyncedContent(fileId);
try {
await localDbSvc.loadItem(`${fileId}/content`);
} catch (e) {
// Item may not exist if content has not been downloaded yet
}
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
const getSyncedContent = () => upgradeSyncedContent(store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
try {
if (isTempFile(fileId)) {
return;
} }
const attemptedLocations = {};
const syncOneContentLocation = () => {
const syncLocations = [ const syncLocations = [
...store.getters['syncLocation/filteredGroupedByFileId'][fileId] || [], ...store.getters['syncLocation/filteredGroupedByFileId'][fileId] || [],
]; ];
if (isWorkspaceSyncPossible()) { if (isWorkspaceSyncPossible()) {
syncLocations.unshift({ id: 'main', providerId: workspaceProvider.id, fileId }); syncLocations.unshift({ id: 'main', providerId: workspaceProvider.id, fileId });
} }
let result;
syncLocations.some((syncLocation) => { await utils.awaitSequence(syncLocations, async (syncLocation) => {
const provider = providerRegistry.providers[syncLocation.providerId]; const provider = providerRegistry.providers[syncLocation.providerId];
if ( if (!provider) {
// Skip if it has been attempted already return;
!attemptedLocations[syncLocation.id] && }
// Skip temp file const token = provider.getToken(syncLocation);
!isTempFile() if (!token) {
) { return;
attemptedLocations[syncLocation.id] = true; }
const token = provider && provider.getToken(syncLocation);
result = token && store.dispatch('queue/doWithLocation', { const doSyncLocation = async () => {
location: syncLocation, const serverContent = await provider.downloadContent(token, syncLocation);
promise: provider.downloadContent(token, syncLocation)
.then((serverContent = null) => {
const syncedContent = getSyncedContent(); const syncedContent = getSyncedContent();
const syncHistoryItem = getSyncHistoryItem(syncLocation.id); const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
let mergedContent = (() => { let mergedContent = (() => {
const clientContent = utils.deepCopy(getContent()); const clientContent = utils.deepCopy(getContent());
if (!clientContent) { if (!clientContent) {
return utils.deepCopy(serverContent); return utils.deepCopy(serverContent || null);
} }
if (!serverContent) { if (!serverContent) {
// Sync location has not been created yet // Sync location has not been created yet
@ -314,11 +310,10 @@ function syncFile(fileId, syncContext = new SyncContext()) {
return clientContent; return clientContent;
} }
// Perform a merge with last merged content if any, or a simple fusion otherwise // Perform a merge with last merged content if any, or a simple fusion otherwise
let lastMergedContent; let lastMergedContent = utils.someResult(
serverContent.history.some((hash) => { serverContent.history,
lastMergedContent = syncedContent.historyData[hash]; hash => syncedContent.historyData[hash],
return lastMergedContent; );
});
if (!lastMergedContent && syncHistoryItem) { if (!lastMergedContent && syncHistoryItem) {
lastMergedContent = syncedContent.historyData[syncHistoryItem[LAST_MERGED]]; lastMergedContent = syncedContent.historyData[syncHistoryItem[LAST_MERGED]];
} }
@ -326,7 +321,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
})(); })();
if (!mergedContent) { if (!mergedContent) {
return null; return;
} }
// Update or set content in store // Update or set content in store
@ -338,7 +333,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
comments: mergedContent.comments, comments: mergedContent.comments,
}); });
// Retrieve content with new `hash` and freeze it // Retrieve content with its new hash value and freeze it
mergedContent = utils.deepCopy(getContent()); mergedContent = utils.deepCopy(getContent());
// Make merged content history // Make merged content history
@ -385,15 +380,15 @@ function syncFile(fileId, syncContext = new SyncContext()) {
if (skipUpload) { if (skipUpload) {
// Server content and merged content are equal, skip content upload // Server content and merged content are equal, skip content upload
return null; return;
} }
// Upload merged content // Upload merged content
return provider.uploadContent(token, { const syncLocationToStore = await provider.uploadContent(token, {
...mergedContent, ...mergedContent,
history: mergedContentHistory.slice(0, maxContentHistory), history: mergedContentHistory.slice(0, maxContentHistory),
}, syncLocation, tooLateChecker(restartContentSyncAfter)) }, syncLocation, tooLateChecker(restartContentSyncAfter));
.then((syncLocationToStore) => {
// Replace sync location if modified // Replace sync location if modified
if (utils.serializeObject(syncLocation) !== if (utils.serializeObject(syncLocation) !==
utils.serializeObject(syncLocationToStore) utils.serializeObject(syncLocationToStore)
@ -407,57 +402,49 @@ function syncFile(fileId, syncContext = new SyncContext()) {
) { ) {
syncContext.restart = true; syncContext.restart = true;
} }
}); };
})
.catch((err) => { await store.dispatch('queue/doWithLocation', {
location: syncLocation,
action: async () => {
try {
await doSyncLocation();
} catch (err) {
if (store.state.offline || (err && err.message === 'TOO_LATE')) { if (store.state.offline || (err && err.message === 'TOO_LATE')) {
throw err; throw err;
} }
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err); store.dispatch('notification/error', err);
}),
})
.then(() => syncOneContentLocation());
} }
return result; },
}); });
return result; });
}; } catch (err) {
return syncOneContentLocation();
})
.then(
() => localDbSvc.unloadContents(),
err => localDbSvc.unloadContents()
.then(() => {
throw err;
}),
)
.catch((err) => {
if (err && err.message === 'TOO_LATE') { if (err && err.message === 'TOO_LATE') {
// Restart sync // Restart sync
return syncFile(fileId, syncContext); await syncFile(fileId, syncContext);
} }
throw err; throw err;
}); } finally {
} await localDbSvc.unloadContents();
}
};
/** /**
* Sync a data item, typically settings, workspaces and templates. * Sync a data item, typically settings, workspaces and templates.
*/ */
function syncDataItem(dataId) { const syncDataItem = async (dataId) => {
const getItem = () => store.state.data.itemMap[dataId] const getItem = () => store.state.data.itemMap[dataId]
|| store.state.data.lsItemMap[dataId]; || store.state.data.lsItemMap[dataId];
const item = getItem(); const item = getItem();
const syncData = store.getters['data/syncDataByItemId'][dataId]; const syncData = store.getters['data/syncDataByItemId'][dataId];
// Sync if item hash and syncData hash are inconsistent // Sync if item hash and syncData hash are out of sync
if (syncData && item && item.hash === syncData.hash) { if (syncData && item && item.hash === syncData.hash) {
return null; return;
} }
return workspaceProvider.downloadData(dataId) const serverItem = await workspaceProvider.downloadData(dataId);
.then((serverItem = null) => {
const dataSyncData = store.getters['data/dataSyncData'][dataId]; const dataSyncData = store.getters['data/dataSyncData'][dataId];
let mergedItem = (() => { let mergedItem = (() => {
const clientItem = utils.deepCopy(getItem()); const clientItem = utils.deepCopy(getItem());
@ -485,7 +472,7 @@ function syncDataItem(dataId) {
})(); })();
if (!mergedItem) { if (!mergedItem) {
return null; return;
} }
// Update item in store // Update item in store
@ -497,28 +484,23 @@ function syncDataItem(dataId) {
// Retrieve item with new `hash` and freeze it // Retrieve item with new `hash` and freeze it
mergedItem = utils.deepCopy(getItem()); mergedItem = utils.deepCopy(getItem());
return Promise.resolve()
.then(() => {
if (serverItem && serverItem.hash === mergedItem.hash) { if (serverItem && serverItem.hash === mergedItem.hash) {
return null; return;
} }
return workspaceProvider.uploadData(mergedItem, tooLateChecker(restartContentSyncAfter)); await workspaceProvider.uploadData(mergedItem, tooLateChecker(restartContentSyncAfter));
}) store.dispatch('data/patchDataSyncData', {
.then(() => store.dispatch('data/patchDataSyncData', {
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]), [dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
}));
}); });
} };
/** /**
* Sync the whole workspace with the main provider and the current file explicit locations. * Sync the whole workspace with the main provider and the current file explicit locations.
*/ */
function syncWorkspace() { const syncWorkspace = async () => {
try {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
const syncContext = new SyncContext(); const syncContext = new SyncContext();
return Promise.resolve()
.then(() => {
// Store the sub in the DB since it's not safely stored in the token // Store the sub in the DB since it's not safely stored in the token
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const localSettings = store.getters['data/localSettings']; const localSettings = store.getters['data/localSettings'];
@ -529,9 +511,8 @@ function syncWorkspace() {
} else if (localSettings.syncSub !== syncToken.sub) { } else if (localSettings.syncSub !== syncToken.sub) {
throw new Error('Synchronization failed due to token inconsistency.'); throw new Error('Synchronization failed due to token inconsistency.');
} }
})
.then(() => workspaceProvider.getChanges()) const changes = await workspaceProvider.getChanges();
.then((changes) => {
// Apply changes // Apply changes
applyChanges(changes); applyChanges(changes);
if (workspaceProvider.onChangesApplied) { if (workspaceProvider.onChangesApplied) {
@ -542,7 +523,7 @@ function syncWorkspace() {
const ifNotTooLate = tooLateChecker(restartSyncAfter); const ifNotTooLate = tooLateChecker(restartSyncAfter);
// Called until no item to save // Called until no item to save
const saveNextItem = ifNotTooLate(() => { const saveNextItem = ifNotTooLate(async () => {
const storeItemMap = { const storeItemMap = {
...store.state.file.itemMap, ...store.state.file.itemMap,
...store.state.folder.itemMap, ...store.state.folder.itemMap,
@ -551,8 +532,9 @@ function syncWorkspace() {
// Deal with contents and data later // Deal with contents and data later
}; };
const syncDataByItemId = store.getters['data/syncDataByItemId']; const syncDataByItemId = store.getters['data/syncDataByItemId'];
let promise; const [changedItem, syncDataToUpdate] = utils.someResult(
Object.entries(storeItemMap).some(([id, item]) => { Object.entries(storeItemMap),
([id, item]) => {
const existingSyncData = syncDataByItemId[id]; const existingSyncData = syncDataByItemId[id];
if ((!existingSyncData || existingSyncData.hash !== item.hash) if ((!existingSyncData || existingSyncData.hash !== item.hash)
// Add file/folder if parent has been added // Add file/folder if parent has been added
@ -560,25 +542,30 @@ function syncWorkspace() {
// Add file if content has been added // Add file if content has been added
&& (item.type !== 'file' || syncDataByItemId[`${id}/content`]) && (item.type !== 'file' || syncDataByItemId[`${id}/content`])
) { ) {
promise = workspaceProvider return [item, existingSyncData];
}
return null;
},
) || [];
if (changedItem) {
const resultSyncData = await workspaceProvider
.saveSimpleItem( .saveSimpleItem(
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
utils.deepCopy(item), utils.deepCopy(changedItem),
utils.deepCopy(existingSyncData), utils.deepCopy(syncDataToUpdate),
ifNotTooLate, ifNotTooLate,
) );
.then(resultSyncData => store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncData', {
[resultSyncData.id]: resultSyncData, [resultSyncData.id]: resultSyncData,
})) });
.then(() => saveNextItem()); await saveNextItem();
} }
return promise;
});
return promise;
}); });
await saveNextItem();
// Called until no item to remove // Called until no item to remove
const removeNextItem = ifNotTooLate(() => { const removeNextItem = ifNotTooLate(async () => {
const storeItemMap = { const storeItemMap = {
...store.state.file.itemMap, ...store.state.file.itemMap,
...store.state.folder.itemMap, ...store.state.folder.itemMap,
@ -587,37 +574,41 @@ function syncWorkspace() {
...store.state.content.itemMap, ...store.state.content.itemMap,
}; };
const syncData = store.getters['data/syncData']; const syncData = store.getters['data/syncData'];
let promise; const syncDataToRemove = utils.deepCopy(utils.someResult(
Object.entries(syncData).some(([, existingSyncData]) => { Object.values(syncData),
if (!storeItemMap[existingSyncData.itemId] && existingSyncData => !storeItemMap[existingSyncData.itemId]
// We don't want to delete data items, especially on first sync // We don't want to delete data items, especially on first sync
existingSyncData.type !== 'data' && && existingSyncData.type !== 'data'
// Remove content only if file has been removed // Remove content only if file has been removed
(existingSyncData.type !== 'content' || !storeItemMap[existingSyncData.itemId.split('/')[0]]) && (existingSyncData.type !== 'content'
) { || !storeItemMap[existingSyncData.itemId.split('/')[0]])
&& existingSyncData,
));
if (syncDataToRemove) {
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
const syncDataToRemove = utils.deepCopy(existingSyncData); await workspaceProvider.removeItem(syncDataToRemove, ifNotTooLate);
promise = workspaceProvider
.removeItem(syncDataToRemove, ifNotTooLate)
.then(() => {
const syncDataCopy = { ...store.getters['data/syncData'] }; const syncDataCopy = { ...store.getters['data/syncData'] };
delete syncDataCopy[syncDataToRemove.id]; delete syncDataCopy[syncDataToRemove.id];
store.dispatch('data/setSyncData', syncDataCopy); store.dispatch('data/setSyncData', syncDataCopy);
}) await removeNextItem();
.then(() => removeNextItem());
} }
return promise;
});
return promise;
}); });
await removeNextItem();
// Sync settings and workspaces only in the main workspace
if (workspace.id === 'main') {
await syncDataItem('settings');
await syncDataItem('workspaces');
}
await syncDataItem('templates');
const getOneFileIdToSync = () => { const getOneFileIdToSync = () => {
const contentIds = [...new Set([ const contentIds = [...new Set([
...Object.keys(localDbSvc.hashMap.content), ...Object.keys(localDbSvc.hashMap.content),
...store.getters['file/items'].map(file => `${file.id}/content`), ...store.getters['file/items'].map(file => `${file.id}/content`),
])]; ])];
let fileId; return utils.someResult(contentIds, (contentId) => {
contentIds.some((contentId) => {
// Get content hash from itemMap or from localDbSvc if not loaded // Get content hash from itemMap or from localDbSvc if not loaded
const loadedContent = store.state.content.itemMap[contentId]; const loadedContent = store.state.content.itemMap[contentId];
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId]; const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
@ -628,88 +619,64 @@ function syncWorkspace() {
// And if syncData does not exist or if content hash and syncData hash are inconsistent // And if syncData does not exist or if content hash and syncData hash are inconsistent
(!syncData || syncData.hash !== hash) (!syncData || syncData.hash !== hash)
) { ) {
[fileId] = contentId.split('/'); const [fileId] = contentId.split('/');
return fileId;
} }
return fileId;
});
return fileId;
};
const syncNextFile = () => {
const fileId = getOneFileIdToSync();
if (!fileId) {
return null; return null;
} });
return syncFile(fileId, syncContext)
.then(() => syncNextFile());
}; };
const onSyncEnd = () => Promise.resolve(workspaceProvider.onSyncEnd const syncNextFile = async () => {
&& workspaceProvider.onSyncEnd()); const fileId = getOneFileIdToSync();
if (fileId) {
await syncFile(fileId, syncContext);
await syncNextFile();
}
};
return Promise.resolve()
.then(() => saveNextItem())
.then(() => removeNextItem())
// Sync settings only in the main workspace
.then(() => workspace.id === 'main' && syncDataItem('settings'))
// Sync workspaces only in the main workspace
.then(() => workspace.id === 'main' && syncDataItem('workspaces'))
.then(() => syncDataItem('templates'))
.then(() => {
const currentFileId = store.getters['file/current'].id; const currentFileId = store.getters['file/current'].id;
if (currentFileId) { if (currentFileId) {
// Sync current file first // Sync current file first
return syncFile(currentFileId, syncContext) await syncFile(currentFileId, syncContext);
.then(() => syncNextFile());
} }
return syncNextFile(); await syncNextFile();
})
.then(
() => onSyncEnd(),
err => onSyncEnd().then(() => {
throw err;
}, () => {
throw err;
}),
)
.then(
() => {
if (syncContext.restart) { if (syncContext.restart) {
// Restart sync // Restart sync
return syncWorkspace(); await syncWorkspace();
} }
return null; } catch (err) {
},
(err) => {
if (err && err.message === 'TOO_LATE') { if (err && err.message === 'TOO_LATE') {
// Restart sync // Restart sync
return syncWorkspace(); await syncWorkspace();
} } else {
throw err; throw err;
}, }
); } finally {
}); if (workspaceProvider.onSyncEnd) {
} workspaceProvider.onSyncEnd();
}
}
};
/** /**
* Enqueue a sync task, if possible. * Enqueue a sync task, if possible.
*/ */
function requestSync() { const requestSync = () => {
// No sync in light mode // No sync in light mode
if (store.state.light) { if (store.state.light) {
return; return;
} }
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => { store.dispatch('queue/enqueueSyncRequest', async () => {
let intervalId; let intervalId;
const attempt = () => { const attempt = async () => {
// Only start syncing when these conditions are met // Only start syncing when these conditions are met
if (networkSvc.isUserActive() && isSyncWindow()) { if (networkSvc.isUserActive() && isSyncWindow()) {
clearInterval(intervalId); clearInterval(intervalId);
if (!isSyncPossible()) { if (!isSyncPossible()) {
// Cancel sync // Cancel sync
reject(new Error('Sync not possible.')); throw new Error('Sync not possible.');
return;
} }
// Determine if we have to clean files // Determine if we have to clean files
@ -729,25 +696,17 @@ function requestSync() {
// Call setLastSyncActivity periodically // Call setLastSyncActivity periodically
intervalId = utils.setInterval(() => setLastSyncActivity(), 1000); intervalId = utils.setInterval(() => setLastSyncActivity(), 1000);
setLastSyncActivity(); setLastSyncActivity();
const cleaner = cb => (res) => {
clearInterval(intervalId);
cb(res);
};
Promise.resolve() try {
.then(() => {
if (isWorkspaceSyncPossible()) { if (isWorkspaceSyncPossible()) {
return syncWorkspace(); await syncWorkspace();
} } else if (hasCurrentFileSyncLocations()) {
if (hasCurrentFileSyncLocations()) {
// Only sync current file if workspace sync is unavailable. // Only sync current file if workspace sync is unavailable.
// We could also sync files that are out-of-sync but it would // We could sync all files that are out-of-sync but it would
// require to load all the syncedContent objects from the DB. // require to load all the syncedContent objects from the DB.
return syncFile(store.getters['file/current'].id); await syncFile(store.getters['file/current'].id);
} }
return null;
})
.then(() => {
// Clean files // Clean files
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => { Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemMap[fileId];
@ -755,44 +714,48 @@ function requestSync() {
fileSvc.deleteFile(fileId); fileSvc.deleteFile(fileId);
} }
}); });
}) } finally {
.then(cleaner(resolve), cleaner(reject)); clearInterval(intervalId);
}
} }
}; };
intervalId = utils.setInterval(() => attempt(), 1000); intervalId = utils.setInterval(() => attempt(), 1000);
attempt(); attempt();
})); });
} };
export default { export default {
init() { async init() {
return Promise.resolve()
.then(() => {
// Load workspaces and tokens from localStorage // Load workspaces and tokens from localStorage
localDbSvc.syncLocalStorage(); localDbSvc.syncLocalStorage();
// Try to find a suitable action provider // Try to find a suitable action provider
actionProvider = providerRegistry.providers[utils.queryParams.providerId]; actionProvider = providerRegistry.providers[utils.queryParams.providerId];
return actionProvider && actionProvider.initAction && actionProvider.initAction(); if (actionProvider && actionProvider.initAction) {
}) await actionProvider.initAction();
.then(() => { }
// Try to find a suitable workspace sync provider // Try to find a suitable workspace sync provider
workspaceProvider = providerRegistry.providers[utils.queryParams.providerId]; workspaceProvider = providerRegistry.providers[utils.queryParams.providerId];
if (!workspaceProvider || !workspaceProvider.initWorkspace) { if (!workspaceProvider || !workspaceProvider.initWorkspace) {
workspaceProvider = googleDriveAppDataProvider; workspaceProvider = googleDriveAppDataProvider;
} }
return workspaceProvider.initWorkspace(); const workspace = await workspaceProvider.initWorkspace();
}) store.dispatch('workspace/setCurrentWorkspaceId', workspace.id);
.then(workspace => store.dispatch('workspace/setCurrentWorkspaceId', workspace.id)) await localDbSvc.init();
.then(() => localDbSvc.init())
.then(() => {
// Try to find a suitable action provider // Try to find a suitable action provider
actionProvider = providerRegistry.providers[utils.queryParams.providerId] || actionProvider; actionProvider = providerRegistry.providers[utils.queryParams.providerId] || actionProvider;
return actionProvider && actionProvider.performAction && actionProvider.performAction() if (actionProvider && actionProvider.performAction) {
.then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation)); const newSyncLocation = await actionProvider.performAction();
}) if (newSyncLocation) {
.then(() => tempFileSvc.init()) this.createSyncLocation(newSyncLocation);
.then(() => { }
}
await tempFileSvc.init();
if (!store.state.light) { if (!store.state.light) {
// Sync periodically // Sync periodically
utils.setInterval(() => { utils.setInterval(() => {
@ -813,7 +776,6 @@ export default {
} }
}, 5000); }, 5000);
} }
});
}, },
isSyncPossible, isSyncPossible,
requestSync, requestSync,

View File

@ -25,20 +25,19 @@ export default {
} }
this.closed = true; this.closed = true;
}, },
init() { async init() {
if (!origin || !window.parent) { if (!origin || !window.parent) {
return Promise.resolve(); return;
} }
store.commit('setLight', true); store.commit('setLight', true);
return fileSvc.createFile({ const file = await fileSvc.createFile({
name: fileName || utils.getHostname(origin), name: fileName || utils.getHostname(origin),
text: contentText || '\n', text: contentText || '\n',
properties: contentProperties, properties: contentProperties,
parentId: 'temp', parentId: 'temp',
}, true) }, true);
.then((file) => {
const fileItemMap = store.state.file.itemMap; const fileItemMap = store.state.file.itemMap;
// Sanitize file creations // Sanitize file creations
@ -94,6 +93,5 @@ export default {
// Watch preview refresh and file name changes // Watch preview refresh and file name changes
editorSvc.$on('previewCtx', onChange); editorSvc.$on('previewCtx', onChange);
store.watch(() => store.getters['file/current'].name, onChange); store.watch(() => store.getters['file/current'].name, onChange);
});
}, },
}; };

View File

@ -16,7 +16,7 @@ export default {
promised[id] = true; promised[id] = true;
store.commit('userInfo/addItem', { id, name, imageUrl }); store.commit('userInfo/addItem', { id, name, imageUrl });
}, },
getInfo(userId) { async getInfo(userId) {
if (!promised[userId]) { if (!promised[userId]) {
const [type, sub] = parseUserId(userId); const [type, sub] = parseUserId(userId);
@ -33,27 +33,26 @@ export default {
if (!store.state.offline) { if (!store.state.offline) {
promised[userId] = true; promised[userId] = true;
switch (type) { switch (type) {
case 'github': { case 'github':
return githubHelper.getUser(sub) try {
.catch((err) => { await githubHelper.getUser(sub);
} catch (err) {
if (err.status !== 404) { if (err.status !== 404) {
promised[userId] = false; promised[userId] = false;
} }
});
} }
break;
case 'google': case 'google':
default: { default:
return googleHelper.getUser(sub) try {
.catch((err) => { await googleHelper.getUser(sub);
} catch (err) {
if (err.status !== 404) { if (err.status !== 404) {
promised[userId] = false; promised[userId] = false;
} }
});
} }
} }
} }
} }
return null;
}, },
}; };

View File

@ -224,6 +224,14 @@ export default {
}; };
return runWithNextValue(); return runWithNextValue();
}, },
someResult(values, func) {
let result;
values.some((value) => {
result = func(value);
return result;
});
return result;
},
parseQueryParams, parseQueryParams,
addQueryParams(url = '', params = {}, hash = false) { addQueryParams(url = '', params = {}, hash = false) {
const keys = Object.keys(params).filter(key => params[key] != null); const keys = Object.keys(params).filter(key => params[key] != null);

View File

@ -31,11 +31,11 @@ module.mutations = {
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: (state, getters, rootState, rootGetters) => { current: ({ itemMap, revisionContent }, getters, rootState, rootGetters) => {
if (state.revisionContent) { if (revisionContent) {
return state.revisionContent; return revisionContent;
} }
return state.itemMap[`${rootGetters['file/current'].id}/content`] || empty(); return itemMap[`${rootGetters['file/current'].id}/content`] || empty();
}, },
currentChangeTrigger: (state, getters) => { currentChangeTrigger: (state, getters) => {
const { current } = getters; const { current } = getters;
@ -45,11 +45,9 @@ module.getters = {
current.hash, current.hash,
]); ]);
}, },
currentProperties: (state, getters) => utils.computeProperties(getters.current.properties), currentProperties: (state, { current }) => utils.computeProperties(current.properties),
isCurrentEditable: (state, getters, rootState, rootGetters) => isCurrentEditable: ({ revisionContent }, { current }, rootState, rootGetters) =>
!state.revisionContent && !revisionContent && current.id && rootGetters['layout/styles'].showEditor,
getters.current.id &&
rootGetters['layout/styles'].showEditor,
}; };
module.actions = { module.actions = {
@ -76,7 +74,7 @@ module.actions = {
}); });
} }
}, },
restoreRevision({ async restoreRevision({
state, state,
getters, getters,
commit, commit,
@ -84,8 +82,7 @@ module.actions = {
}) { }) {
const { revisionContent } = state; const { revisionContent } = state;
if (revisionContent) { if (revisionContent) {
dispatch('modal/fileRestoration', null, { root: true }) await dispatch('modal/fileRestoration', null, { root: true });
.then(() => {
// Close revision // Close revision
commit('setRevisionContent'); commit('setRevisionContent');
const currentContent = utils.deepCopy(getters.current); const currentContent = utils.deepCopy(getters.current);
@ -108,7 +105,6 @@ module.actions = {
text: revisionContent.originalText, text: revisionContent.originalText,
}); });
} }
});
} }
}, },
}; };

View File

@ -5,8 +5,8 @@ const module = moduleTemplate(empty, true);
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: (state, getters, rootState, rootGetters) => current: ({ itemMap }, getters, rootState, rootGetters) =>
state.itemMap[`${rootGetters['file/current'].id}/contentState`] || empty(), itemMap[`${rootGetters['file/current'].id}/contentState`] || empty(),
}; };
module.actions = { module.actions = {

View File

@ -104,7 +104,7 @@ export default {
lsItemMap: {}, lsItemMap: {},
}, },
mutations: { mutations: {
setItem: (state, value) => { setItem: ({ itemMap, lsItemMap }, value) => {
// Create an empty item and override its data field // Create an empty item and override its data field
const emptyItem = empty(value.id); const emptyItem = empty(value.id);
const data = typeof value.data === 'object' const data = typeof value.data === 'object'
@ -118,19 +118,19 @@ export default {
}); });
// Store item in itemMap or lsItemMap if its stored in the localStorage // Store item in itemMap or lsItemMap if its stored in the localStorage
Vue.set(lsItemIdSet.has(item.id) ? state.lsItemMap : state.itemMap, item.id, item); Vue.set(lsItemIdSet.has(item.id) ? lsItemMap : itemMap, item.id, item);
}, },
deleteItem(state, id) { deleteItem({ itemMap }, id) {
// Only used by localDbSvc to clean itemMap from object moved to localStorage // Only used by localDbSvc to clean itemMap from object moved to localStorage
Vue.delete(state.itemMap, id); Vue.delete(itemMap, id);
}, },
}, },
getters: { getters: {
workspaces: getter('workspaces'), workspaces: getter('workspaces'),
sanitizedWorkspaces: (state, getters, rootState, rootGetters) => { sanitizedWorkspaces: (state, { workspaces }, rootState, rootGetters) => {
const sanitizedWorkspaces = {}; const sanitizedWorkspaces = {};
const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken']; const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken'];
Object.entries(getters.workspaces).forEach(([id, workspace]) => { Object.entries(workspaces).forEach(([id, workspace]) => {
const sanitizedWorkspace = { const sanitizedWorkspace = {
id, id,
providerId: mainWorkspaceToken && 'googleDriveAppData', providerId: mainWorkspaceToken && 'googleDriveAppData',
@ -146,9 +146,9 @@ export default {
return sanitizedWorkspaces; return sanitizedWorkspaces;
}, },
settings: getter('settings'), settings: getter('settings'),
computedSettings: (state, getters) => { computedSettings: (state, { settings }) => {
const customSettings = yaml.safeLoad(getters.settings); const customSettings = yaml.safeLoad(settings);
const settings = yaml.safeLoad(defaultSettings); const parsedSettings = yaml.safeLoad(defaultSettings);
const override = (obj, opt) => { const override = (obj, opt) => {
const objType = Object.prototype.toString.call(obj); const objType = Object.prototype.toString.call(obj);
const optType = Object.prototype.toString.call(opt); const optType = Object.prototype.toString.call(opt);
@ -166,44 +166,44 @@ export default {
}); });
return obj; return obj;
}; };
return override(settings, customSettings); return override(parsedSettings, customSettings);
}, },
localSettings: getter('localSettings'), localSettings: getter('localSettings'),
layoutSettings: getter('layoutSettings'), layoutSettings: getter('layoutSettings'),
templates: getter('templates'), templates: getter('templates'),
allTemplates: (state, getters) => ({ allTemplates: (state, { templates }) => ({
...getters.templates, ...templates,
...additionalTemplates, ...additionalTemplates,
}), }),
lastCreated: getter('lastCreated'), lastCreated: getter('lastCreated'),
lastOpened: getter('lastOpened'), lastOpened: getter('lastOpened'),
lastOpenedIds: (state, getters, rootState) => { lastOpenedIds: (state, { lastOpened }, rootState) => {
const lastOpened = { const result = {
...getters.lastOpened, ...lastOpened,
}; };
const currentFileId = rootState.file.currentId; const currentFileId = rootState.file.currentId;
if (currentFileId && !lastOpened[currentFileId]) { if (currentFileId && !result[currentFileId]) {
lastOpened[currentFileId] = Date.now(); result[currentFileId] = Date.now();
} }
return Object.keys(lastOpened) return Object.keys(result)
.filter(id => rootState.file.itemMap[id]) .filter(id => rootState.file.itemMap[id])
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1]) .sort((id1, id2) => result[id2] - result[id1])
.slice(0, 20); .slice(0, 20);
}, },
syncData: getter('syncData'), syncData: getter('syncData'),
syncDataByItemId: (state, getters) => { syncDataByItemId: (state, { syncData }) => {
const result = {}; const result = {};
Object.entries(getters.syncData).forEach(([, value]) => { Object.entries(syncData).forEach(([, value]) => {
result[value.itemId] = value; result[value.itemId] = value;
}); });
return result; return result;
}, },
syncDataByType: (state, getters) => { syncDataByType: (state, { syncData }) => {
const result = {}; const result = {};
utils.types.forEach((type) => { utils.types.forEach((type) => {
result[type] = {}; result[type] = {};
}); });
Object.entries(getters.syncData).forEach(([, item]) => { Object.entries(syncData).forEach(([, item]) => {
if (result[item.type]) { if (result[item.type]) {
result[item.type][item.itemId] = item; result[item.type][item.itemId] = item;
} }
@ -212,12 +212,12 @@ export default {
}, },
dataSyncData: getter('dataSyncData'), dataSyncData: getter('dataSyncData'),
tokens: getter('tokens'), tokens: getter('tokens'),
googleTokens: (state, getters) => getters.tokens.google || {}, googleTokens: (state, { tokens }) => tokens.google || {},
couchdbTokens: (state, getters) => getters.tokens.couchdb || {}, couchdbTokens: (state, { tokens }) => tokens.couchdb || {},
dropboxTokens: (state, getters) => getters.tokens.dropbox || {}, dropboxTokens: (state, { tokens }) => tokens.dropbox || {},
githubTokens: (state, getters) => getters.tokens.github || {}, githubTokens: (state, { tokens }) => tokens.github || {},
wordpressTokens: (state, getters) => getters.tokens.wordpress || {}, wordpressTokens: (state, { tokens }) => tokens.wordpress || {},
zendeskTokens: (state, getters) => getters.tokens.zendesk || {}, zendeskTokens: (state, { tokens }) => tokens.zendesk || {},
}, },
actions: { actions: {
setWorkspaces: setter('workspaces'), setWorkspaces: setter('workspaces'),

View File

@ -59,8 +59,8 @@ export default {
}, },
}, },
getters: { getters: {
newDiscussion: state => newDiscussion: ({ currentDiscussionId, newDiscussionId, newDiscussion }) =>
state.currentDiscussionId === state.newDiscussionId && state.newDiscussion, currentDiscussionId === newDiscussionId && newDiscussion,
currentFileDiscussionLastComments: (state, getters, rootState, rootGetters) => { currentFileDiscussionLastComments: (state, getters, rootState, rootGetters) => {
const { discussions, comments } = rootGetters['content/current']; const { discussions, comments } = rootGetters['content/current'];
const discussionLastComments = {}; const discussionLastComments = {};
@ -74,14 +74,18 @@ export default {
}); });
return discussionLastComments; return discussionLastComments;
}, },
currentFileDiscussions: (state, getters, rootState, rootGetters) => { currentFileDiscussions: (
{ newDiscussionId },
{ newDiscussion, currentFileDiscussionLastComments },
rootState,
rootGetters,
) => {
const currentFileDiscussions = {}; const currentFileDiscussions = {};
const { newDiscussion } = getters;
if (newDiscussion) { if (newDiscussion) {
currentFileDiscussions[state.newDiscussionId] = newDiscussion; currentFileDiscussions[newDiscussionId] = newDiscussion;
} }
const { discussions } = rootGetters['content/current']; const { discussions } = rootGetters['content/current'];
Object.entries(getters.currentFileDiscussionLastComments) Object.entries(currentFileDiscussionLastComments)
.sort(([, lastComment1], [, lastComment2]) => .sort(([, lastComment1], [, lastComment2]) =>
lastComment1.created - lastComment2.created) lastComment1.created - lastComment2.created)
.forEach(([discussionId]) => { .forEach(([discussionId]) => {
@ -89,17 +93,22 @@ export default {
}); });
return currentFileDiscussions; return currentFileDiscussions;
}, },
currentDiscussion: (state, getters) => currentDiscussion: ({ currentDiscussionId }, { currentFileDiscussions }) =>
getters.currentFileDiscussions[state.currentDiscussionId], currentFileDiscussions[currentDiscussionId],
previousDiscussionId: idShifter(-1), previousDiscussionId: idShifter(-1),
nextDiscussionId: idShifter(1), nextDiscussionId: idShifter(1),
currentDiscussionComments: (state, getters, rootState, rootGetters) => { currentDiscussionComments: (
{ currentDiscussionId },
{ currentDiscussion },
rootState,
rootGetters,
) => {
const comments = {}; const comments = {};
if (getters.currentDiscussion) { if (currentDiscussion) {
const contentComments = rootGetters['content/current'].comments; const contentComments = rootGetters['content/current'].comments;
Object.entries(contentComments) Object.entries(contentComments)
.filter(([, comment]) => .filter(([, comment]) =>
comment.discussionId === state.currentDiscussionId) comment.discussionId === currentDiscussionId)
.sort(([, comment1], [, comment2]) => .sort(([, comment1], [, comment2]) =>
comment1.created - comment2.created) comment1.created - comment2.created)
.forEach(([commentId, comment]) => { .forEach(([commentId, comment]) => {
@ -108,10 +117,12 @@ export default {
} }
return comments; return comments;
}, },
currentDiscussionLastCommentId: (state, getters) => currentDiscussionLastCommentId: (state, { currentDiscussionComments }) =>
Object.keys(getters.currentDiscussionComments).pop(), Object.keys(currentDiscussionComments).pop(),
currentDiscussionLastComment: (state, getters) => currentDiscussionLastComment: (
getters.currentDiscussionComments[getters.currentDiscussionLastCommentId], state,
{ currentDiscussionComments, currentDiscussionLastCommentId },
) => currentDiscussionComments[currentDiscussionLastCommentId],
}, },
actions: { actions: {
cancelNewComment({ commit, getters }) { cancelNewComment({ commit, getters }) {
@ -120,15 +131,15 @@ export default {
commit('setCurrentDiscussionId', getters.nextDiscussionId); commit('setCurrentDiscussionId', getters.nextDiscussionId);
} }
}, },
createNewDiscussion({ commit, dispatch, rootGetters }, selection) { async createNewDiscussion({ commit, dispatch, rootGetters }, selection) {
const loginToken = rootGetters['workspace/loginToken']; const loginToken = rootGetters['workspace/loginToken'];
if (!loginToken) { if (!loginToken) {
dispatch('modal/signInForComment', { try {
onResolve: () => googleHelper.signin() await dispatch('modal/signInForComment', null, { root: true });
.then(() => syncSvc.requestSync()) await googleHelper.signin();
.then(() => dispatch('createNewDiscussion', selection)), syncSvc.requestSync();
}, { root: true }) await dispatch('createNewDiscussion', selection);
.catch(() => { /* Cancel */ }); } catch (e) { /* cancel */ }
} else if (selection) { } else if (selection) {
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim(); let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
const maxLength = 80; const maxLength = 80;

View File

@ -44,11 +44,11 @@ const fakeFileNode = new Node(emptyFile());
fakeFileNode.item.id = 'fake'; fakeFileNode.item.id = 'fake';
fakeFileNode.noDrag = true; fakeFileNode.noDrag = true;
function getParent(node, getters) { function getParent({ item, isNil }, { nodeMap, rootNode }) {
if (node.isNil) { if (isNil) {
return nilFileNode; return nilFileNode;
} }
return getters.nodeMap[node.item.parentId] || getters.rootNode; return nodeMap[item.parentId] || rootNode;
} }
function getFolder(node, getters) { function getFolder(node, getters) {
@ -67,6 +67,21 @@ export default {
newChildNode: nilFileNode, newChildNode: nilFileNode,
openNodes: {}, openNodes: {},
}, },
mutations: {
setSelectedId: setter('selectedId'),
setEditingId: setter('editingId'),
setDragSourceId: setter('dragSourceId'),
setDragTargetId: setter('dragTargetId'),
setNewItem(state, item) {
state.newChildNode = item ? new Node(item, [], item.type === 'folder') : nilFileNode;
},
setNewItemName(state, name) {
state.newChildNode.item.name = name;
},
toggleOpenNode(state, id) {
Vue.set(state.openNodes, id, !state.openNodes[id]);
},
},
getters: { getters: {
nodeStructure: (state, getters, rootState, rootGetters) => { nodeStructure: (state, getters, rootState, rootGetters) => {
const rootNode = new Node(emptyFolder(), [], true, true); const rootNode = new Node(emptyFolder(), [], true, true);
@ -138,41 +153,26 @@ export default {
rootNode, rootNode,
}; };
}, },
nodeMap: (state, getters) => getters.nodeStructure.nodeMap, nodeMap: (state, { nodeStructure }) => nodeStructure.nodeMap,
rootNode: (state, getters) => getters.nodeStructure.rootNode, rootNode: (state, { nodeStructure }) => nodeStructure.rootNode,
newChildNodeParent: (state, getters) => getParent(state.newChildNode, getters), newChildNodeParent: (state, getters) => getParent(state.newChildNode, getters),
selectedNode: (state, getters) => getters.nodeMap[state.selectedId] || nilFileNode, selectedNode: ({ selectedId }, { nodeMap }) => nodeMap[selectedId] || nilFileNode,
selectedNodeFolder: (state, getters) => getFolder(getters.selectedNode, getters), selectedNodeFolder: (state, getters) => getFolder(getters.selectedNode, getters),
editingNode: (state, getters) => getters.nodeMap[state.editingId] || nilFileNode, editingNode: ({ editingId }, { nodeMap }) => nodeMap[editingId] || nilFileNode,
dragSourceNode: (state, getters) => getters.nodeMap[state.dragSourceId] || nilFileNode, dragSourceNode: ({ dragSourceId }, { nodeMap }) => nodeMap[dragSourceId] || nilFileNode,
dragTargetNode: (state, getters) => { dragTargetNode: ({ dragTargetId }, { nodeMap }) => {
if (state.dragTargetId === 'fake') { if (dragTargetId === 'fake') {
return fakeFileNode; return fakeFileNode;
} }
return getters.nodeMap[state.dragTargetId] || nilFileNode; return nodeMap[dragTargetId] || nilFileNode;
}, },
dragTargetNodeFolder: (state, getters) => { dragTargetNodeFolder: ({ dragTargetId }, getters) => {
if (state.dragTargetId === 'fake') { if (dragTargetId === 'fake') {
return getters.rootNode; return getters.rootNode;
} }
return getFolder(getters.dragTargetNode, getters); return getFolder(getters.dragTargetNode, getters);
}, },
}, },
mutations: {
setSelectedId: setter('selectedId'),
setEditingId: setter('editingId'),
setDragSourceId: setter('dragSourceId'),
setDragTargetId: setter('dragTargetId'),
setNewItem(state, item) {
state.newChildNode = item ? new Node(item, [], item.type === 'folder') : nilFileNode;
},
setNewItemName(state, name) {
state.newChildNode.item.name = name;
},
toggleOpenNode(state, id) {
Vue.set(state.openNodes, id, !state.openNodes[id]);
},
},
actions: { actions: {
openNode({ openNode({
state, state,

View File

@ -10,10 +10,10 @@ module.state = {
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: state => state.itemMap[state.currentId] || empty(), current: ({ itemMap, currentId }) => itemMap[currentId] || empty(),
isCurrentTemp: (state, getters) => getters.current.parentId === 'temp', isCurrentTemp: (state, { current }) => current.parentId === 'temp',
lastOpened: (state, getters, rootState, rootGetters) => lastOpened: ({ itemMap }, { items }, rootState, rootGetters) =>
state.itemMap[rootGetters['data/lastOpenedIds'][0]] || getters.items[0] || empty(), itemMap[rootGetters['data/lastOpenedIds'][0]] || items[0] || empty(),
}; };
module.mutations = { module.mutations = {

View File

@ -54,13 +54,33 @@ const store = new Vuex.Store({
minuteCounter: 0, minuteCounter: 0,
monetizeSponsor: false, monetizeSponsor: false,
}, },
mutations: {
setLight: (state, value) => {
state.light = value;
},
setOffline: (state, value) => {
state.offline = value;
},
updateLastOfflineCheck: (state) => {
state.lastOfflineCheck = Date.now();
},
updateMinuteCounter: (state) => {
state.minuteCounter += 1;
},
setMonetizeSponsor: (state, value) => {
state.monetizeSponsor = value;
},
setGoogleSponsor: (state, value) => {
state.googleSponsor = value;
},
},
getters: { getters: {
allItemMap: (state) => { allItemMap: (state) => {
const result = {}; const result = {};
utils.types.forEach(type => Object.assign(result, state[type].itemMap)); utils.types.forEach(type => Object.assign(result, state[type].itemMap));
return result; return result;
}, },
itemPaths: (state) => { itemPaths: (state, getters) => {
const result = {}; const result = {};
const folderMap = state.folder.itemMap; const folderMap = state.folder.itemMap;
const getPath = (item) => { const getPath = (item) => {
@ -84,8 +104,10 @@ const store = new Vuex.Store({
result[item.id] = itemPath; result[item.id] = itemPath;
return itemPath; return itemPath;
}; };
[
[...state.folder.items, ...state.file.items].forEach(item => getPath(item)); ...getters['folder/items'],
...getters['file/items'],
].forEach(item => getPath(item));
return result; return result;
}, },
pathItems: (state, { allItemMap, itemPaths }) => { pathItems: (state, { allItemMap, itemPaths }) => {
@ -97,29 +119,9 @@ const store = new Vuex.Store({
}); });
return result; return result;
}, },
isSponsor: (state, getters) => { isSponsor: ({ light, monetizeSponsor }, getters) => {
const sponsorToken = getters['workspace/sponsorToken']; const sponsorToken = getters['workspace/sponsorToken'];
return state.light || state.monetizeSponsor || (sponsorToken && sponsorToken.isSponsor); return light || monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);
},
},
mutations: {
setLight: (state, value) => {
state.light = value;
},
setOffline: (state, value) => {
state.offline = value;
},
updateLastOfflineCheck: (state) => {
state.lastOfflineCheck = Date.now();
},
updateMinuteCounter: (state) => {
state.minuteCounter += 1;
},
setMonetizeSponsor: (state, value) => {
state.monetizeSponsor = value;
},
setGoogleSponsor: (state, value) => {
state.googleSponsor = value;
}, },
}, },
actions: { actions: {

View File

@ -12,22 +12,22 @@ export default (empty) => {
module.getters = { module.getters = {
...module.getters, ...module.getters,
groupedByFileId: (state, getters) => { groupedByFileId: (state, { items }) => {
const groups = {}; const groups = {};
getters.items.forEach(item => addToGroup(groups, item)); items.forEach(item => addToGroup(groups, item));
return groups; return groups;
}, },
filteredGroupedByFileId: (state, getters) => { filteredGroupedByFileId: (state, { items }) => {
const groups = {}; const groups = {};
getters.items.filter((item) => { items.filter((item) => {
// Filter items that we can't use // Filter items that we can't use
const provider = providerRegistry.providers[item.providerId]; const provider = providerRegistry.providers[item.providerId];
return provider && provider.getToken(item); return provider && provider.getToken(item);
}).forEach(item => addToGroup(groups, item)); }).forEach(item => addToGroup(groups, item));
return groups; return groups;
}, },
current: (state, getters, rootState, rootGetters) => { current: (state, { filteredGroupedByFileId }, rootState, rootGetters) => {
const locations = getters.filteredGroupedByFileId[rootGetters['file/current'].id] || []; const locations = filteredGroupedByFileId[rootGetters['file/current'].id] || [];
return locations.map((location) => { return locations.map((location) => {
const provider = providerRegistry.providers[location.providerId]; const provider = providerRegistry.providers[location.providerId];
return { return {

View File

@ -13,39 +13,28 @@ export default {
}, },
}, },
getters: { getters: {
config: state => !state.hidden && state.stack[0], config: ({ hidden, stack }) => !hidden && stack[0],
}, },
actions: { actions: {
open({ commit, state }, param) { async open({ commit, state }, param) {
return new Promise((resolve, reject) => {
const config = typeof param === 'object' ? { ...param } : { type: param }; const config = typeof param === 'object' ? { ...param } : { type: param };
const clean = () => commit('setStack', state.stack.filter((otherConfig => otherConfig !== config))); try {
config.resolve = (result) => { return await new Promise((resolve, reject) => {
clean(); config.resolve = resolve;
if (config.onResolve) { config.reject = reject;
// Call onResolve immediately (mostly to prevent browsers from blocking popup windows)
config.onResolve(result)
.then(res => resolve(res));
} else {
resolve(result);
}
};
config.reject = (error) => {
clean();
reject(error);
};
commit('setStack', [config, ...state.stack]); commit('setStack', [config, ...state.stack]);
}); });
} finally {
commit('setStack', state.stack.filter((otherConfig => otherConfig !== config)));
}
}, },
hideUntil({ commit }, promise) { async hideUntil({ commit }, promise) {
try {
commit('setHidden', true); commit('setHidden', true);
return promise.then((res) => { return await promise;
} finally {
commit('setHidden', false); commit('setHidden', false);
return res; }
}, (err) => {
commit('setHidden', false);
throw err;
});
}, },
folderDeletion: ({ dispatch }, item) => dispatch('open', { folderDeletion: ({ dispatch }, item) => dispatch('open', {
content: `<p>You are about to delete the folder <b>${item.name}</b>. Its files will be moved to Trash. Are you sure?</p>`, content: `<p>You are about to delete the folder <b>${item.name}</b>. Its files will be moved to Trash. Are you sure?</p>`,
@ -105,39 +94,34 @@ export default {
resolveText: 'Yes, clean', resolveText: 'Yes, clean',
rejectText: 'No', rejectText: 'No',
}), }),
providerRedirection: ({ dispatch }, { providerName, onResolve }) => dispatch('open', { providerRedirection: ({ dispatch }, { providerName }) => dispatch('open', {
content: `<p>You are about to navigate to the <b>${providerName}</b> authorization page.</p>`, content: `<p>You are about to navigate to the <b>${providerName}</b> authorization page.</p>`,
resolveText: 'Ok, go on', resolveText: 'Ok, go on',
rejectText: 'Cancel', rejectText: 'Cancel',
onResolve,
}), }),
workspaceGoogleRedirection: ({ dispatch }, { onResolve }) => dispatch('open', { workspaceGoogleRedirection: ({ dispatch }) => dispatch('open', {
content: '<p>StackEdit needs full Google Drive access to open this workspace.</p>', content: '<p>StackEdit needs full Google Drive access to open this workspace.</p>',
resolveText: 'Ok, grant', resolveText: 'Ok, grant',
rejectText: 'Cancel', rejectText: 'Cancel',
onResolve,
}), }),
signInForSponsorship: ({ dispatch }, { onResolve }) => dispatch('open', { signInForSponsorship: ({ dispatch }) => dispatch('open', {
type: 'signInForSponsorship', type: 'signInForSponsorship',
content: `<p>You have to sign in with Google to sponsor.</p> content: `<p>You have to sign in with Google to sponsor.</p>
<div class="modal__info"><b>Note:</b> This will sync your main workspace.</div>`, <div class="modal__info"><b>Note:</b> This will sync your main workspace.</div>`,
resolveText: 'Ok, sign in', resolveText: 'Ok, sign in',
rejectText: 'Cancel', rejectText: 'Cancel',
onResolve,
}), }),
signInForComment: ({ dispatch }, { onResolve }) => dispatch('open', { signInForComment: ({ dispatch }) => dispatch('open', {
content: `<p>You have to sign in with Google to start commenting.</p> content: `<p>You have to sign in with Google to start commenting.</p>
<div class="modal__info"><b>Note:</b> This will sync your main workspace.</div>`, <div class="modal__info"><b>Note:</b> This will sync your main workspace.</div>`,
resolveText: 'Ok, sign in', resolveText: 'Ok, sign in',
rejectText: 'Cancel', rejectText: 'Cancel',
onResolve,
}), }),
signInForHistory: ({ dispatch }, { onResolve }) => dispatch('open', { signInForHistory: ({ dispatch }) => dispatch('open', {
content: `<p>You have to sign in with Google to enable revision history.</p> content: `<p>You have to sign in with Google to enable revision history.</p>
<div class="modal__info"><b>Note:</b> This will sync your main workspace.</div>`, <div class="modal__info"><b>Note:</b> This will sync your main workspace.</div>`,
resolveText: 'Ok, sign in', resolveText: 'Ok, sign in',
rejectText: 'Cancel', rejectText: 'Cancel',
onResolve,
}), }),
sponsorOnly: ({ dispatch }) => dispatch('open', { sponsorOnly: ({ dispatch }) => dispatch('open', {
content: '<p>This feature is restricted to sponsors as it relies on server resources.</p>', content: '<p>This feature is restricted to sponsors as it relies on server resources.</p>',

View File

@ -11,7 +11,7 @@ export default (empty, simpleHash = false) => {
itemMap: {}, itemMap: {},
}, },
getters: { getters: {
items: state => Object.values(state.itemMap), items: ({ itemMap }) => Object.values(itemMap),
}, },
mutations: { mutations: {
setItem(state, value) { setItem(state, value) {

View File

@ -71,16 +71,13 @@ export default {
})); }));
} }
}, },
doWithLocation({ commit }, { location, promise }) { async doWithLocation({ commit }, { location, action }) {
try {
commit('setCurrentLocation', location); commit('setCurrentLocation', location);
return promise return await action();
.then((res) => { } finally {
commit('setCurrentLocation', {}); commit('setCurrentLocation', {});
return res; }
}, (err) => {
commit('setCurrentLocation', {});
throw err;
});
}, },
}, },
}; };

View File

@ -5,8 +5,8 @@ const module = moduleTemplate(empty, true);
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: (state, getters, rootState, rootGetters) => current: ({ itemMap }, getters, rootState, rootGetters) =>
state.itemMap[`${rootGetters['file/current'].id}/syncedContent`] || empty(), itemMap[`${rootGetters['file/current'].id}/syncedContent`] || empty(),
}; };
export default module; export default module;

View File

@ -6,8 +6,8 @@ export default {
itemMap: {}, itemMap: {},
}, },
mutations: { mutations: {
addItem: (state, item) => { addItem: ({ itemMap }, item) => {
Vue.set(state.itemMap, item.id, item); Vue.set(itemMap, item.id, item);
}, },
}, },
}; };

View File

@ -19,54 +19,50 @@ export default {
const workspaces = rootGetters['data/sanitizedWorkspaces']; const workspaces = rootGetters['data/sanitizedWorkspaces'];
return workspaces.main; return workspaces.main;
}, },
currentWorkspace: (state, getters, rootState, rootGetters) => { currentWorkspace: ({ currentWorkspaceId }, { mainWorkspace }, rootState, rootGetters) => {
const workspaces = rootGetters['data/sanitizedWorkspaces']; const workspaces = rootGetters['data/sanitizedWorkspaces'];
return workspaces[state.currentWorkspaceId] || getters.mainWorkspace; return workspaces[currentWorkspaceId] || mainWorkspace;
}, },
hasUniquePaths: (state, getters) => { hasUniquePaths: (state, { currentWorkspace }) =>
const workspace = getters.currentWorkspace; currentWorkspace.providerId === 'githubWorkspace',
return workspace.providerId === 'githubWorkspace'; lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`,
}, lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`,
lastSyncActivityKey: (state, getters) => `${getters.currentWorkspace.id}/lastSyncActivity`,
lastFocusKey: (state, getters) => `${getters.currentWorkspace.id}/lastWindowFocus`,
mainWorkspaceToken: (state, getters, rootState, rootGetters) => { mainWorkspaceToken: (state, getters, rootState, rootGetters) => {
const googleTokens = rootGetters['data/googleTokens']; const googleTokens = rootGetters['data/googleTokens'];
const loginSubs = Object.keys(googleTokens) const loginSubs = Object.keys(googleTokens)
.filter(sub => googleTokens[sub].isLogin); .filter(sub => googleTokens[sub].isLogin);
return googleTokens[loginSubs[0]]; return googleTokens[loginSubs[0]];
}, },
syncToken: (state, getters, rootState, rootGetters) => { syncToken: (state, { currentWorkspace, mainWorkspaceToken }, rootState, rootGetters) => {
const workspace = getters.currentWorkspace; switch (currentWorkspace.providerId) {
switch (workspace.providerId) {
case 'googleDriveWorkspace': { case 'googleDriveWorkspace': {
const googleTokens = rootGetters['data/googleTokens']; const googleTokens = rootGetters['data/googleTokens'];
return googleTokens[workspace.sub]; return googleTokens[currentWorkspace.sub];
} }
case 'githubWorkspace': { case 'githubWorkspace': {
const githubTokens = rootGetters['data/githubTokens']; const githubTokens = rootGetters['data/githubTokens'];
return githubTokens[workspace.sub]; return githubTokens[currentWorkspace.sub];
} }
case 'couchdbWorkspace': { case 'couchdbWorkspace': {
const couchdbTokens = rootGetters['data/couchdbTokens']; const couchdbTokens = rootGetters['data/couchdbTokens'];
return couchdbTokens[workspace.id]; return couchdbTokens[currentWorkspace.id];
} }
default: default:
return getters.mainWorkspaceToken; return mainWorkspaceToken;
} }
}, },
loginToken: (state, getters, rootState, rootGetters) => { loginToken: (state, { currentWorkspace, mainWorkspaceToken }, rootState, rootGetters) => {
const workspace = getters.currentWorkspace; switch (currentWorkspace.providerId) {
switch (workspace.providerId) {
case 'googleDriveWorkspace': { case 'googleDriveWorkspace': {
const googleTokens = rootGetters['data/googleTokens']; const googleTokens = rootGetters['data/googleTokens'];
return googleTokens[workspace.sub]; return googleTokens[currentWorkspace.sub];
} }
case 'githubWorkspace': { case 'githubWorkspace': {
const githubTokens = rootGetters['data/githubTokens']; const githubTokens = rootGetters['data/githubTokens'];
return githubTokens[workspace.sub]; return githubTokens[currentWorkspace.sub];
} }
default: default:
return getters.mainWorkspaceToken; return mainWorkspaceToken;
} }
}, },
userId: (state, { loginToken }, rootState, rootGetters) => { userId: (state, { loginToken }, rootState, rootGetters) => {
@ -82,7 +78,7 @@ export default {
}); });
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub; return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
}, },
sponsorToken: (state, getters) => getters.mainWorkspaceToken, sponsorToken: (state, { mainWorkspaceToken }) => mainWorkspaceToken,
}, },
actions: { actions: {
setCurrentWorkspaceId: ({ commit, getters }, value) => { setCurrentWorkspaceId: ({ commit, getters }, value) => {