Workspaces (part 3)

This commit is contained in:
Benoit Schweblin 2017-12-23 19:25:14 +01:00
parent abbe1804e2
commit 32aa259790
22 changed files with 608 additions and 188 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop"> <div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop">
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}"> <div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName"> <input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
</div> </div>
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{paddingLeft: leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()"> <div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{paddingLeft: leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">

View File

@ -15,6 +15,7 @@
<sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal> <sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal>
<!-- Providers --> <!-- Providers -->
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal> <google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
<google-drive-account-modal v-else-if="config.type === 'googleDriveAccount'"></google-drive-account-modal>
<google-drive-save-modal v-else-if="config.type === 'googleDriveSave'"></google-drive-save-modal> <google-drive-save-modal v-else-if="config.type === 'googleDriveSave'"></google-drive-save-modal>
<google-drive-workspace-modal v-else-if="config.type === 'googleDriveWorkspace'"></google-drive-workspace-modal> <google-drive-workspace-modal v-else-if="config.type === 'googleDriveWorkspace'"></google-drive-workspace-modal>
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal> <google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
@ -62,6 +63,7 @@ import SponsorModal from './modals/SponsorModal';
// Providers // Providers
import GooglePhotoModal from './modals/providers/GooglePhotoModal'; import GooglePhotoModal from './modals/providers/GooglePhotoModal';
import GoogleDriveAccountModal from './modals/providers/GoogleDriveAccountModal';
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal'; import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
import GoogleDriveWorkspaceModal from './modals/providers/GoogleDriveWorkspaceModal'; import GoogleDriveWorkspaceModal from './modals/providers/GoogleDriveWorkspaceModal';
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal'; import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
@ -102,6 +104,7 @@ export default {
SponsorModal, SponsorModal,
// Providers // Providers
GooglePhotoModal, GooglePhotoModal,
GoogleDriveAccountModal,
GoogleDriveSaveModal, GoogleDriveSaveModal,
GoogleDriveWorkspaceModal, GoogleDriveWorkspaceModal,
GoogleDrivePublishModal, GoogleDrivePublishModal,

View File

@ -120,6 +120,12 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
&::after {
content: '';
display: block;
height: 40px;
}
} }
.side-bar__panel--hidden { .side-bar__panel--hidden {
@ -127,11 +133,11 @@ export default {
} }
.side-bar__panel--menu { .side-bar__panel--menu {
padding: 10px 10px 50px; padding: 10px;
} }
.side-bar__panel--help { .side-bar__panel--help {
padding: 0 10px 40px 20px; padding: 0 10px 0 20px;
pre { pre {
font-size: 0.9em; font-size: 0.9em;

View File

@ -21,7 +21,7 @@
<script> <script>
import { mapMutations } from 'vuex'; import { mapMutations } from 'vuex';
import googleDriveAppDataProvider from '../../services/providers/googleDriveAppDataProvider'; import providerRegistry from '../../services/providers/providerRegistry';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import UserImage from '../UserImage'; import UserImage from '../UserImage';
import UserName from '../UserName'; import UserName from '../UserName';
@ -83,7 +83,7 @@ export default {
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
this.$store.dispatch('queue/enqueue', this.$store.dispatch('queue/enqueue',
() => Promise.resolve() () => Promise.resolve()
.then(() => googleDriveAppDataProvider.getRevisionContent( .then(() => this.workspaceProvider.getRevisionContent(
loginToken, currentFile.id, revision.id)) loginToken, currentFile.id, revision.id))
.then(resolve, reject)); .then(resolve, reject));
}); });
@ -123,6 +123,10 @@ export default {
}, },
}, },
created() { created() {
// Find the workspace provider
const workspace = this.$store.getters['workspace/currentWorkspace'];
this.workspaceProvider = providerRegistry.providers[workspace.providerId];
// Watch file changes // Watch file changes
this.$watch( this.$watch(
() => this.$store.getters['file/current'].id, () => this.$store.getters['file/current'].id,
@ -138,7 +142,7 @@ export default {
revisionsPromise = new Promise((resolve, reject) => { revisionsPromise = new Promise((resolve, reject) => {
this.$store.dispatch('queue/enqueue', this.$store.dispatch('queue/enqueue',
() => Promise.resolve() () => Promise.resolve()
.then(() => googleDriveAppDataProvider.listRevisions(loginToken, currentFile.id)) .then(() => this.workspaceProvider.listRevisions(loginToken, currentFile.id))
.then((revisions) => { .then((revisions) => {
resolve(revisions.sort( resolve(revisions.sort(
(revision1, revision2) => revision2.created - revision1.created)); (revision1, revision2) => revision2.created - revision1.created));

View File

@ -158,7 +158,10 @@ export default {
return this.$store.dispatch('modal/open', 'publishManagement'); return this.$store.dispatch('modal/open', 'publishManagement');
}, },
addGoogleDriveAccount() { addGoogleDriveAccount() {
return googleHelper.addDriveAccount() return this.$store.dispatch('modal/open', {
type: 'googleDriveAccount',
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
})
.catch(() => {}); // Cancel .catch(() => {}); // Cancel
}, },
addDropboxAccount() { addDropboxAccount() {

View File

@ -139,7 +139,10 @@ export default {
return this.$store.dispatch('modal/open', 'syncManagement'); return this.$store.dispatch('modal/open', 'syncManagement');
}, },
addGoogleDriveAccount() { addGoogleDriveAccount() {
return googleHelper.addDriveAccount() return this.$store.dispatch('modal/open', {
type: 'googleDriveAccount',
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
})
.catch(() => {}); // Cancel .catch(() => {}); // Cancel
}, },
addDropboxAccount() { addDropboxAccount() {

View File

@ -37,7 +37,7 @@ export default {
}, },
methods: { methods: {
addGoogleDriveWorkspace() { addGoogleDriveWorkspace() {
return googleHelper.addDriveAccount() return googleHelper.addDriveAccount(true)
.then(token => this.$store.dispatch('modal/open', { .then(token => this.$store.dispatch('modal/open', {
type: 'googleDriveWorkspace', type: 'googleDriveWorkspace',
token, token,

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider> <icon-provider provider-id="dropbox"></icon-provider>
</div> </div>
<p>This will link your <b>Dropbox</b> account to your <b>StackEdit</b> workspace.</p> <p>This will link your <b>Dropbox</b> account to <b>StackEdit</b>.</p>
<div class="form-entry"> <div class="form-entry">
<div class="form-entry__checkbox"> <div class="form-entry__checkbox">
<label> <label>

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="github"></icon-provider> <icon-provider provider-id="github"></icon-provider>
</div> </div>
<p>This will link your <b>GitHub</b> account to your <b>StackEdit</b> workspace.</p> <p>This will link your <b>GitHub</b> account to <b>StackEdit</b>.</p>
<div class="form-entry"> <div class="form-entry">
<div class="form-entry__checkbox"> <div class="form-entry__checkbox">
<label> <label>

View File

@ -0,0 +1,34 @@
<template>
<modal-inner aria-label="Link Google Drive account">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will link your <b>Google Drive</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
<input type="checkbox" v-model="restrictedAccess"> Restrict access
</label>
<div class="form-entry__info">
If checked, access will be restricted to files that you have opened or created with <b>StackEdit</b>.
</div>
</div>
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import modalTemplate from '../common/modalTemplate';
export default modalTemplate({
computedLocalSettings: {
restrictedAccess: 'googleDriveRestrictedAccess',
},
});
</script>

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider> <icon-provider provider-id="zendesk"></icon-provider>
</div> </div>
<p>This will link your <b>Zendesk</b> account to your <b>StackEdit</b> workspace.</p> <p>This will link your <b>Zendesk</b> account to <b>StackEdit</b>.</p>
<form-entry label="Site URL" error="siteUrl"> <form-entry label="Site URL" error="siteUrl">
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keyup.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keyup.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">

View File

@ -3,6 +3,7 @@ export default () => ({
htmlExportTemplate: 'styledHtml', htmlExportTemplate: 'styledHtml',
pdfExportTemplate: 'styledHtml', pdfExportTemplate: 'styledHtml',
pandocExportFormat: 'pdf', pandocExportFormat: 'pdf',
googleDriveRestrictedAccess: false,
googleDriveFolderId: '', googleDriveFolderId: '',
googleDriveWorkspaceFolderId: '', googleDriveWorkspaceFolderId: '',
googleDrivePublishFormat: 'markdown', googleDrivePublishFormat: 'markdown',

View File

@ -1,5 +1,5 @@
# Auto-sync frequency (in ms). Minimum is 60000. # Auto-sync frequency (in ms). Minimum is 60000.
autoSyncEvery: 90000 autoSyncEvery: 60000
# Adjust font size in editor and preview # Adjust font size in editor and preview
fontSizeFactor: 1 fontSizeFactor: 1
# Adjust maximum text width in editor and preview # Adjust maximum text width in editor and preview

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="icon-provider" :class="'icon-provider--' + classState"> <div class="icon-provider" :class="'icon-provider--' + classState">
<icon-sync-off v-if="!classState"></icon-sync-off>
</div> </div>
</template> </template>

View File

@ -11,9 +11,10 @@ export default providerRegistry.register({
// Nothing to do since the main workspace isn't necessarily synchronized // Nothing to do since the main workspace isn't necessarily synchronized
return Promise.resolve(store.getters['data/workspaces'].main); return Promise.resolve(store.getters['data/workspaces'].main);
}, },
getChanges(token) { getChanges() {
const syncToken = store.getters['workspace/syncToken'];
const startPageToken = store.getters['data/localSettings'].syncStartPageToken; const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
return googleHelper.getChanges(token, startPageToken, 'appDataFolder') return googleHelper.getChanges(syncToken, startPageToken, true)
.then((result) => { .then((result) => {
const changes = result.changes.filter((change) => { const changes = result.changes.filter((change) => {
if (change.file) { if (change.file) {
@ -30,7 +31,6 @@ export default providerRegistry.register({
type: change.item.type, type: change.item.type,
hash: change.item.hash, hash: change.item.hash,
}; };
change.file = undefined;
} }
change.syncDataId = change.fileId; change.syncDataId = change.fileId;
return true; return true;
@ -41,15 +41,14 @@ export default providerRegistry.register({
}, },
setAppliedChanges(changes) { setAppliedChanges(changes) {
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
workspaceSyncStartPageToken: changes.startPageToken, syncStartPageToken: changes.startPageToken,
}); });
}, },
saveItem(token, item, syncData, ifNotTooLate) { saveSimpleItem(item, syncData, ifNotTooLate) {
const syncToken = store.getters['workspace/syncToken'];
return googleHelper.uploadAppDataFile( return googleHelper.uploadAppDataFile(
token, syncToken,
JSON.stringify(item), JSON.stringify(item),
['appDataFolder'],
undefined,
undefined, undefined,
syncData && syncData.id, syncData && syncData.id,
ifNotTooLate, ifNotTooLate,
@ -62,19 +61,20 @@ export default providerRegistry.register({
hash: item.hash, hash: item.hash,
})); }));
}, },
removeItem(token, syncData, ifNotTooLate) { removeItem(syncData, ifNotTooLate) {
return googleHelper.removeAppDataFile(token, syncData.id, ifNotTooLate) const syncToken = store.getters['workspace/syncToken'];
.then(() => syncData); return googleHelper.removeAppDataFile(syncToken, syncData.id, ifNotTooLate);
}, },
downloadContent(token, syncLocation) { downloadContent(token, syncLocation) {
return this.downloadData(token, `${syncLocation.fileId}/content`); return this.downloadData(`${syncLocation.fileId}/content`);
}, },
downloadData(token, dataId) { 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();
} }
return googleHelper.downloadAppDataFile(token, syncData.id) const syncToken = store.getters['workspace/syncToken'];
return googleHelper.downloadAppDataFile(syncToken, syncData.id)
.then((content) => { .then((content) => {
const item = JSON.parse(content); const item = JSON.parse(content);
if (item.hash !== syncData.hash) { if (item.hash !== syncData.hash) {
@ -89,23 +89,22 @@ export default providerRegistry.register({
}); });
}, },
uploadContent(token, content, syncLocation, ifNotTooLate) { uploadContent(token, content, syncLocation, ifNotTooLate) {
return this.uploadData(token, content, `${syncLocation.fileId}/content`, ifNotTooLate) return this.uploadData(content, `${syncLocation.fileId}/content`, ifNotTooLate)
.then(() => syncLocation); .then(() => syncLocation);
}, },
uploadData(token, item, dataId, ifNotTooLate) { uploadData(item, dataId, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][dataId]; const syncData = store.getters['data/syncDataByItemId'][dataId];
if (syncData && syncData.hash === item.hash) { if (syncData && syncData.hash === item.hash) {
return Promise.resolve(); return Promise.resolve();
} }
const syncToken = store.getters['workspace/syncToken'];
return googleHelper.uploadAppDataFile( return googleHelper.uploadAppDataFile(
token, syncToken,
JSON.stringify({ JSON.stringify({
id: item.id, id: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}), }),
['appDataFolder'],
undefined,
JSON.stringify(item), JSON.stringify(item),
syncData && syncData.id, syncData && syncData.id,
ifNotTooLate, ifNotTooLate,
@ -125,7 +124,7 @@ export default providerRegistry.register({
if (!syncData) { if (!syncData) {
return Promise.reject(); // No need for a proper error message. return Promise.reject(); // No need for a proper error message.
} }
return googleHelper.getFileRevisions(token, syncData.id) return googleHelper.getAppDataFileRevisions(token, syncData.id)
.then(revisions => revisions.map(revision => ({ .then(revisions => revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId, sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
@ -137,7 +136,7 @@ export default providerRegistry.register({
if (!syncData) { if (!syncData) {
return Promise.reject(); // No need for a proper error message. return Promise.reject(); // No need for a proper error message.
} }
return googleHelper.downloadFileRevision(token, syncData.id, revisionId) return googleHelper.downloadAppDataFileRevision(token, syncData.id, revisionId)
.then(content => JSON.parse(content)); .then(content => JSON.parse(content));
}, },
}); });

View File

@ -1,6 +1,7 @@
import store from '../../store'; import store from '../../store';
import googleHelper from './helpers/googleHelper'; import googleHelper from './helpers/googleHelper';
import providerRegistry from './providerRegistry'; import providerRegistry from './providerRegistry';
import providerUtils from './providerUtils';
import utils from '../utils'; import utils from '../utils';
export default providerRegistry.register({ export default providerRegistry.register({
@ -32,7 +33,7 @@ export default providerRegistry.register({
[folder.id], [folder.id],
{ folderId: folder.id }, { folderId: folder.id },
undefined, undefined,
'application/vnd.google-apps.folder', googleHelper.folderMimeType,
) )
.then(dataFolder => ({ .then(dataFolder => ({
...properties, ...properties,
@ -50,7 +51,7 @@ export default providerRegistry.register({
[folder.id], [folder.id],
{ folderId: folder.id }, { folderId: folder.id },
undefined, undefined,
'application/vnd.google-apps.folder', googleHelper.folderMimeType,
) )
.then(trashFolder => ({ .then(trashFolder => ({
...properties, ...properties,
@ -71,7 +72,7 @@ export default providerRegistry.register({
undefined, undefined,
properties, properties,
undefined, undefined,
'application/vnd.google-apps.folder', googleHelper.folderMimeType,
folder.id, folder.id,
) )
.then(() => properties); .then(() => properties);
@ -109,12 +110,12 @@ export default providerRegistry.register({
const googleTokens = store.getters['data/googleTokens']; 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 token = workspace ? googleTokens[workspace.sub] : googleTokens[utils.queryParams.sub];
if (token && token.isDrive) { if (token && token.isDrive && token.driveFullAccess) {
return token; return token;
} }
// 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', { return store.dispatch('modal/workspaceGoogleRedirection', {
onResolve: () => googleHelper.addDriveAccount(), onResolve: () => googleHelper.addDriveAccount(true),
}); });
}) })
.then(token => Promise.resolve() .then(token => Promise.resolve()
@ -125,7 +126,7 @@ export default providerRegistry.register({
[], [],
undefined, undefined,
undefined, undefined,
'application/vnd.google-apps.folder', googleHelper.folderMimeType,
) )
.then(folder => initFolder(token, { .then(folder => initFolder(token, {
...folder, ...folder,
@ -135,27 +136,109 @@ export default providerRegistry.register({
// If workspace does not exist, initialize one // If workspace does not exist, initialize one
.then(folderId => getWorkspace(folderId) || googleHelper.getFile(token, folderId) .then(folderId => getWorkspace(folderId) || googleHelper.getFile(token, folderId)
.then((folder) => { .then((folder) => {
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(`Google Drive folder ${folderId} is part of another workspace.`); throw new Error(`Google Drive folder ${folderId} is part of another workspace.`);
} }
return initFolder(token, folder); return initFolder(token, folder);
}, () => { }, () => {
throw new Error(`Folder ${folderId} is not accessible. Make sure it's a valid StackEdit workspace folder and you have the right permissions.`); throw new Error(`Folder ${folderId} is not accessible. Make sure you have the right permissions.`);
}))); })));
}, },
getChanges(token) { getChanges() {
const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken'];
const startPageToken = store.getters['data/localSettings'].syncStartPageToken; const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
return googleHelper.getChanges(token, startPageToken, 'appDataFolder') return googleHelper.getChanges(syncToken, startPageToken, false)
.then((result) => { .then((result) => {
const changes = result.changes.filter((change) => { // Collect possible parent IDs
const parentIds = {};
Object.entries(store.getters['data/syncDataByItemId']).forEach(([id, syncData]) => {
parentIds[syncData.id] = id;
});
result.changes.forEach((change) => {
const id = ((change.file || {}).appProperties || {}).id;
if (id) {
parentIds[change.fileId] = id;
}
});
// Collect changes
const changes = [];
result.changes.forEach((change) => {
// Ignore changes on StackEdit own folders
if (change.fileId === workspace.folderId
|| change.fileId === workspace.dataFolderId
|| change.fileId === workspace.trashFolderId
) {
return;
}
let contentChange;
if (change.file) { if (change.file) {
// Parse item from file name // Ignore changes in files that are not in the workspace
const properties = change.file.appProperties;
if (!properties || properties.folderId !== workspace.folderId
) {
return;
}
// If change is on a data item
if (change.file.parents[0] === workspace.dataFolderId) {
// Data item has a JSON as a filename
try { try {
change.item = JSON.parse(change.file.name); change.item = JSON.parse(change.file.name);
} catch (e) { } catch (e) {
return;
}
} else {
// Change on a file or folder
const type = change.file.mimeType === googleHelper.folderMimeType
? 'folder'
: 'file';
const item = {
id: properties.id,
type,
name: change.file.name,
parentId: null,
};
// Fill parentId
if (change.file.parents.some(parentId => parentId === workspace.trashFolderId)) {
item.parentId = 'trash';
} else {
change.file.parents.some((parentId) => {
if (!parentIds[parentId]) {
return false; return false;
} }
item.parentId = parentIds[parentId];
return true;
});
}
change.item = utils.addItemHash(item);
if (type === 'file') {
// create a fake change as a file content change
contentChange = {
item: {
id: `${properties.id}/content`,
type: 'content',
// Need a truthy value to force saving sync data
hash: 1,
},
syncData: {
id: `${change.fileId}/content`,
itemId: `${properties.id}/content`,
type: 'content',
// Need a truthy value to force downloading the content
hash: 1,
},
syncDataId: `${change.fileId}/content`,
};
}
}
// Build sync data // Build sync data
change.syncData = { change.syncData = {
id: change.fileId, id: change.fileId,
@ -163,13 +246,237 @@ export default providerRegistry.register({
type: change.item.type, type: change.item.type,
hash: change.item.hash, hash: change.item.hash,
}; };
change.file = undefined; } else {
// Item was removed
const syncData = store.getters['data/syncData'][change.fileId];
if (syncData && syncData.type === 'file') {
// create a fake change as a file content change
contentChange = {
syncDataId: `${change.fileId}/content`,
};
} }
}
// Push change
change.syncDataId = change.fileId; change.syncDataId = change.fileId;
return true; changes.push(change);
if (contentChange) {
changes.push(contentChange);
}
}); });
changes.startPageToken = result.startPageToken; changes.startPageToken = result.startPageToken;
return changes; return changes;
}); });
}, },
setAppliedChanges(changes) {
store.dispatch('data/patchLocalSettings', {
syncStartPageToken: changes.startPageToken,
});
},
saveSimpleItem(item, syncData, ifNotTooLate) {
return Promise.resolve()
.then(() => {
const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken'];
if (item.type !== 'file' && item.type !== 'folder') {
return googleHelper.uploadFile(
syncToken,
JSON.stringify(item),
[workspace.dataFolderId],
{
folderId: workspace.folderId,
},
undefined,
undefined,
syncData && syncData.id,
ifNotTooLate,
);
}
// Type `file` or `folder`
const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId];
return googleHelper.uploadFile(
syncToken,
item.name,
[parentSyncData ? parentSyncData.id : workspace.folderId],
{
id: item.id,
folderId: workspace.folderId,
},
undefined,
item.type === 'folder' ? googleHelper.folderMimeType : undefined,
syncData && syncData.id,
ifNotTooLate,
);
})
.then(file => ({
// Build sync data
id: file.id,
itemId: item.id,
type: item.type,
hash: item.hash,
}));
},
removeItem(syncData, ifNotTooLate) {
// Ignore content deletion
if (syncData.type === 'content') {
return Promise.resolve();
}
const syncToken = store.getters['workspace/syncToken'];
return googleHelper.removeFile(syncToken, syncData.id, ifNotTooLate);
},
downloadContent(token, syncLocation) {
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId];
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (!syncData || !contentSyncData) {
return Promise.resolve();
}
return googleHelper.downloadFile(token, syncData.id)
.then((content) => {
const item = providerUtils.parseContent(content, syncLocation);
if (item.hash !== contentSyncData.hash) {
store.dispatch('data/patchSyncData', {
[contentSyncData.id]: {
...contentSyncData,
hash: item.hash,
},
});
}
return item;
});
},
downloadData(dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
if (!syncData) {
return Promise.resolve();
}
const syncToken = store.getters['workspace/syncToken'];
return googleHelper.downloadFile(syncToken, syncData.id)
.then((content) => {
const item = JSON.parse(content);
if (item.hash !== syncData.hash) {
store.dispatch('data/patchSyncData', {
[syncData.id]: {
...syncData,
hash: item.hash,
},
});
}
return item;
});
},
uploadContent(token, content, syncLocation, ifNotTooLate) {
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (contentSyncData && contentSyncData.hash === content.hash) {
return Promise.resolve();
}
return Promise.resolve()
.then(() => {
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId];
if (syncData) {
// Only update file media
return googleHelper.uploadFile(
token,
undefined,
undefined,
undefined,
providerUtils.serializeContent(content),
undefined,
syncData.id,
ifNotTooLate,
);
}
// Create file with media
const workspace = store.getters['workspace/currentWorkspace'];
// Use deepCopy to freeze objects
const item = utils.deepCopy(store.state.file.itemMap[syncLocation.fileId]);
const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId];
return googleHelper.uploadFile(
token,
item.name,
[parentSyncData ? parentSyncData.id : workspace.folderId],
{
id: item.id,
folderId: workspace.folderId,
},
providerUtils.serializeContent(content),
undefined,
undefined,
ifNotTooLate,
)
.then((file) => {
store.dispatch('data/patchSyncData', {
[file.id]: {
id: file.id,
itemId: item.id,
type: item.type,
hash: item.hash,
},
});
return file;
});
})
.then(file => store.dispatch('data/patchSyncData', {
[`${file.id}/content`]: {
// Build sync data
id: `${file.id}/content`,
itemId: content.id,
type: content.type,
hash: content.hash,
},
}))
.then(() => syncLocation);
},
uploadData(item, dataId, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
if (syncData && syncData.hash === item.hash) {
return Promise.resolve();
}
const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken'];
return googleHelper.uploadFile(
syncToken,
JSON.stringify({
id: item.id,
type: item.type,
hash: item.hash,
}),
[workspace.dataFolderId],
{
folderId: workspace.folderId,
},
JSON.stringify(item),
undefined,
syncData && syncData.id,
ifNotTooLate,
)
.then(file => store.dispatch('data/patchSyncData', {
[file.id]: {
// Build sync data
id: file.id,
itemId: item.id,
type: item.type,
hash: item.hash,
},
}));
},
listRevisions(token, fileId) {
const syncData = store.getters['data/syncDataByItemId'][fileId];
if (!syncData) {
return Promise.reject(); // No need for a proper error message.
}
return googleHelper.getFileRevisions(token, syncData.id)
.then(revisions => revisions.map(revision => ({
id: revision.id,
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
created: new Date(revision.modifiedTime).getTime(),
})));
},
getRevisionContent(token, fileId, revisionId) {
const syncData = store.getters['data/syncDataByItemId'][fileId];
if (!syncData) {
return Promise.reject(); // No need for a proper error message.
}
return googleHelper.downloadFileRevision(token, syncData.id, revisionId)
.then(content => providerUtils.parseContent(content));
},
}); });

View File

@ -30,6 +30,7 @@ const checkIdToken = (idToken) => {
}; };
export default { export default {
folderMimeType: 'application/vnd.google-apps.folder',
request(token, options) { request(token, options) {
return networkSvc.request({ return networkSvc.request({
...options, ...options,
@ -120,6 +121,53 @@ export default {
raw: true, raw: true,
}).then(res => res.body); }).then(res => res.body);
}, },
removeFileInternal(refreshedToken, id, ifNotTooLate = cb => res => cb(res)) {
return Promise.resolve()
// 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, {
method: 'DELETE',
url: `https://www.googleapis.com/drive/v3/files/${id}`,
})));
},
getFileRevisionsInternal(refreshedToken, id) {
return Promise.resolve()
.then(() => {
const revisions = [];
const getPage = pageToken => this.request(refreshedToken, {
method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${id}/revisions`,
params: {
pageToken,
pageSize: 1000,
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
},
})
.then((res) => {
res.body.revisions.forEach((revision) => {
store.commit('userInfo/addItem', {
id: revision.lastModifyingUser.permissionId,
name: revision.lastModifyingUser.displayName,
imageUrl: revision.lastModifyingUser.photoLink,
});
revisions.push(revision);
});
if (res.body.nextPageToken) {
return getPage(res.body.nextPageToken);
}
return revisions;
});
return getPage();
});
},
downloadFileRevisionInternal(refreshedToken, fileId, revisionId) {
return Promise.resolve()
.then(() => this.request(refreshedToken, {
method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`,
raw: true,
}).then(res => res.body));
},
getUser(userId) { getUser(userId) {
return networkSvc.request({ return networkSvc.request({
method: 'GET', method: 'GET',
@ -168,7 +216,7 @@ export default {
expiresOn: Date.now() + (data.expiresIn * 1000), expiresOn: Date.now() + (data.expiresIn * 1000),
idToken: data.idToken, idToken: data.idToken,
sub: `${res.body.sub}`, sub: `${res.body.sub}`,
isLogin: !store.getters['workspace/loginToken'] && isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1, scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
isSponsor: false, isSponsor: false,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 || isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
@ -297,20 +345,24 @@ export default {
addPhotosAccount() { addPhotosAccount() {
return this.startOauth2(photosScopes); return this.startOauth2(photosScopes);
}, },
getChanges(token, startPageToken, spaces) { getChanges(token, startPageToken, isAppData) {
const result = { const result = {
changes: [], changes: [],
}; };
return this.refreshToken(token, driveAppDataScopes) let fileFields = 'file/name';
if (!isAppData) {
fileFields += ',file/parents,file/mimeType,file/appProperties';
}
return this.refreshToken(token, isAppData ? driveAppDataScopes : getDriveScopes(token))
.then((refreshedToken) => { .then((refreshedToken) => {
const getPage = (pageToken = '1') => this.request(refreshedToken, { const getPage = (pageToken = '1') => 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: {
pageToken, pageToken,
spaces, spaces: isAppData ? 'appDataFolder' : 'drive',
pageSize: 1000, pageSize: 1000,
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file/name,file/appProperties)', fields: `nextPageToken,newStartPageToken,changes(fileId,${fileFields})`,
}, },
}) })
.then((res) => { .then((res) => {
@ -328,12 +380,28 @@ export default {
uploadFile(token, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate) { uploadFile(token, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate) {
return this.refreshToken(token, getDriveScopes(token)) return this.refreshToken(token, getDriveScopes(token))
.then(refreshedToken => this.uploadFileInternal( .then(refreshedToken => this.uploadFileInternal(
refreshedToken, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate)); refreshedToken,
name,
parents,
appProperties,
media,
mediaType,
fileId,
ifNotTooLate,
));
}, },
uploadAppDataFile(token, name, parents, appProperties, media, fileId, ifNotTooLate) { uploadAppDataFile(token, name, media, fileId, ifNotTooLate) {
return this.refreshToken(token, driveAppDataScopes) return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this.uploadFileInternal( .then(refreshedToken => this.uploadFileInternal(
refreshedToken, name, parents, appProperties, media, undefined, fileId, ifNotTooLate)); refreshedToken,
name,
['appDataFolder'],
undefined,
media,
undefined,
fileId,
ifNotTooLate,
));
}, },
getFile(token, id) { getFile(token, id) {
return this.refreshToken(token, getDriveScopes(token)) return this.refreshToken(token, getDriveScopes(token))
@ -354,52 +422,31 @@ export default {
return this.refreshToken(token, driveAppDataScopes) return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id)); .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)) { removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
return this.refreshToken(token, driveAppDataScopes) return this.refreshToken(token, driveAppDataScopes)
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late .then(refreshedToken => this.removeFileInternal(refreshedToken, id, ifNotTooLate));
.then(ifNotTooLate(refreshedToken => this.request(refreshedToken, {
method: 'DELETE',
url: `https://www.googleapis.com/drive/v3/files/${id}`,
})));
}, },
getFileRevisions(token, id) { getFileRevisions(token, id) {
return this.refreshToken(token, driveAppDataScopes) return this.refreshToken(token, getDriveScopes(token))
.then((refreshedToken) => { .then(refreshedToken => this.getFileRevisionsInternal(refreshedToken, id));
const revisions = [];
const getPage = pageToken => this.request(refreshedToken, {
method: 'GET',
url: `https://www.googleapis.com/drive/v3/files/${id}/revisions`,
params: {
pageToken,
pageSize: 1000,
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
}, },
}) getAppDataFileRevisions(token, id) {
.then((res) => { return this.refreshToken(token, driveAppDataScopes)
res.body.revisions.forEach((revision) => { .then(refreshedToken => this.getFileRevisionsInternal(refreshedToken, id));
store.commit('userInfo/addItem', {
id: revision.lastModifyingUser.permissionId,
name: revision.lastModifyingUser.displayName,
imageUrl: revision.lastModifyingUser.photoLink,
});
revisions.push(revision);
});
if (res.body.nextPageToken) {
return getPage(res.body.nextPageToken);
}
return revisions;
});
return getPage();
});
}, },
downloadFileRevision(token, fileId, revisionId) { 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) return this.refreshToken(token, driveAppDataScopes)
.then(refreshedToken => this.request(refreshedToken, { .then(refreshedToken => this.downloadFileRevisionInternal(
method: 'GET', refreshedToken, fileId, revisionId));
url: `https://www.googleapis.com/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`,
raw: true,
}).then(res => res.body));
}, },
uploadBlogger( uploadBlogger(
token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage, token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage,
@ -475,6 +522,7 @@ export default {
let picker; let picker;
const pickerBuilder = new google.picker.PickerBuilder() const pickerBuilder = new google.picker.PickerBuilder()
.setOAuthToken(refreshedToken.accessToken) .setOAuthToken(refreshedToken.accessToken)
.hideTitleBar()
.setCallback((data) => { .setCallback((data) => {
switch (data[google.picker.Response.ACTION]) { switch (data[google.picker.Response.ACTION]) {
case google.picker.Action.PICKED: case google.picker.Action.PICKED:
@ -488,27 +536,35 @@ export default {
switch (type) { switch (type) {
default: default:
case 'doc': { case 'doc': {
const addView = (hasRootParent) => {
const view = new google.picker.DocsView(google.picker.ViewId.DOCS); const view = new google.picker.DocsView(google.picker.ViewId.DOCS);
if (hasRootParent) {
view.setParent('root'); view.setParent('root');
view.setIncludeFolders(true); }
view.setMimeTypes([ view.setMimeTypes([
'text/plain', 'text/plain',
'text/x-markdown', 'text/x-markdown',
'application/octet-stream', 'application/octet-stream',
].join(',')); ].join(','));
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
pickerBuilder.addView(view); pickerBuilder.addView(view);
};
pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
addView(false);
// addView(true);
break; break;
} }
case 'folder': { case 'folder': {
const addView = (hasRootParent) => {
const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS); const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
if (hasRootParent) {
view.setParent('root'); view.setParent('root');
view.setIncludeFolders(true); }
view.setSelectFolderEnabled(true); view.setSelectFolderEnabled(true);
view.setMimeTypes('application/vnd.google-apps.folder'); view.setMimeTypes(this.folderMimeType);
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
pickerBuilder.addView(view); pickerBuilder.addView(view);
};
addView(false);
// addView(true);
break; break;
} }
case 'img': { case 'img': {

View File

@ -51,11 +51,7 @@ export default {
// Ignore // Ignore
} }
} }
result.hash = utils.hash(utils.serializeObject({ return utils.addItemHash(result);
...result,
hash: undefined,
}));
return result;
}, },
/** /**
* Find and open a file location that fits the criteria * Find and open a file location that fits the criteria

View File

@ -92,35 +92,39 @@ function cleanSyncedContent(syncedContent) {
function applyChanges(changes) { function 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 syncDataChanged = false; let saveSyncData = false;
changes.forEach((change) => { changes.forEach((change) => {
const existingSyncData = syncData[change.syncDataId]; const existingSyncData = syncData[change.syncDataId];
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId]; const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
if (change.removed && existingSyncData) { if (!change.item && existingSyncData) {
// Item was removed
delete syncData[change.syncDataId];
saveSyncData = true;
if (existingItem) { if (existingItem) {
// Remove object from the store // Remove object from the store
store.commit(`${existingItem.type}/deleteItem`, existingItem.id); store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
delete storeItemMap[existingItem.id]; delete storeItemMap[existingItem.id];
} }
delete syncData[change.syncDataId]; } else if (change.item && change.item.hash) {
syncDataChanged = true; // Item was modifed
} else if (!change.removed && change.item && change.item.hash) { syncData[change.syncDataId] = change.syncData;
if (!existingSyncData || (existingSyncData.hash !== change.item.hash && ( saveSyncData = true;
!existingItem || existingItem.hash !== change.item.hash if (
))) { // If no sync data or existing one is different
// Put object in the store, except for content and data which will be merge later (existingSyncData || {}).hash !== change.item.hash
if (change.item.type !== 'content' && change.item.type !== 'data') { // And no existing item or existing item is different
&& (existingItem || {}).hash !== change.item.hash
// And item is not content nor data, which will be merged later
&& change.item.type !== 'content' && change.item.type !== 'data'
) {
store.commit(`${change.item.type}/setItem`, change.item); store.commit(`${change.item.type}/setItem`, change.item);
storeItemMap[change.item.id] = change.item; storeItemMap[change.item.id] = change.item;
} }
} }
syncData[change.syncDataId] = change.syncData;
syncDataChanged = true;
}
}); });
if (syncDataChanged) { if (saveSyncData) {
store.dispatch('data/setSyncData', syncData); store.dispatch('data/setSyncData', syncData);
} }
} }
@ -405,8 +409,7 @@ function syncDataItem(dataId) {
return null; return null;
} }
const syncToken = store.getters['workspace/syncToken']; return syncProvider.downloadData(dataId)
return syncToken && syncProvider.downloadData(syncToken, dataId)
.then((serverItem = null) => { .then((serverItem = null) => {
const dataSyncData = store.getters['data/dataSyncData'][dataId]; const dataSyncData = store.getters['data/dataSyncData'][dataId];
let mergedItem = (() => { let mergedItem = (() => {
@ -452,11 +455,7 @@ function syncDataItem(dataId) {
if (serverItem && serverItem.hash === mergedItem.hash) { if (serverItem && serverItem.hash === mergedItem.hash) {
return null; return null;
} }
return syncProvider.uploadData( return syncProvider.uploadData(mergedItem, dataId);
syncToken,
mergedItem,
dataId,
);
}) })
.then(() => { .then(() => {
store.dispatch('data/patchDataSyncData', { store.dispatch('data/patchDataSyncData', {
@ -469,22 +468,24 @@ function syncDataItem(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(workspace, syncToken) { function syncWorkspace() {
const workspace = store.getters['workspace/currentWorkspace'];
const syncContext = new SyncContext(); const syncContext = new SyncContext();
return Promise.resolve() return Promise.resolve()
.then(() => { .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 localSettings = store.getters['data/localSettings']; const localSettings = store.getters['data/localSettings'];
if (!localSettings.syncSub) { if (!localSettings.syncSub) {
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
workspaceSyncSub: syncToken.sub, syncSub: syncToken.sub,
}); });
} 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(() => syncProvider.getChanges(syncToken)) .then(() => syncProvider.getChanges())
.then((changes) => { .then((changes) => {
// Apply changes // Apply changes
applyChanges(changes); applyChanges(changes);
@ -509,15 +510,16 @@ function syncWorkspace(workspace, syncToken) {
// 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 result; let promise;
Object.entries(storeItemMap).some(([id, item]) => { Object.entries(storeItemMap).some(([id, item]) => {
const existingSyncData = syncDataByItemId[id]; const existingSyncData = syncDataByItemId[id];
if ((!existingSyncData || existingSyncData.hash !== item.hash) && if ((!existingSyncData || existingSyncData.hash !== item.hash)
// Add file if content has been uploaded // Add file/folder if parent has been added
(item.type !== 'file' || syncDataByItemId[`${id}/content`]) && (!storeItemMap[item.parentId] || syncDataByItemId[item.parentId])
// Add file if content has been added
&& (item.type !== 'file' || syncDataByItemId[`${id}/content`])
) { ) {
result = syncProvider.saveItem( promise = syncProvider.saveSimpleItem(
syncToken,
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
utils.deepCopy(item), utils.deepCopy(item),
utils.deepCopy(existingSyncData), utils.deepCopy(existingSyncData),
@ -528,9 +530,9 @@ function syncWorkspace(workspace, syncToken) {
})) }))
.then(() => saveNextItem()); .then(() => saveNextItem());
} }
return result; return promise;
}); });
return result; return promise;
}); });
// Called until no item to remove // Called until no item to remove
@ -543,7 +545,7 @@ function syncWorkspace(workspace, syncToken) {
...store.state.content.itemMap, ...store.state.content.itemMap,
}; };
const syncData = store.getters['data/syncData']; const syncData = store.getters['data/syncData'];
let result; let promise;
Object.entries(syncData).some(([, existingSyncData]) => { Object.entries(syncData).some(([, existingSyncData]) => {
if (!storeItemMap[existingSyncData.itemId] && if (!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
@ -553,8 +555,8 @@ function syncWorkspace(workspace, syncToken) {
) { ) {
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
const syncDataToRemove = utils.deepCopy(existingSyncData); const syncDataToRemove = utils.deepCopy(existingSyncData);
result = syncProvider promise = syncProvider
.removeItem(syncToken, syncDataToRemove, ifNotTooLate) .removeItem(syncDataToRemove, ifNotTooLate)
.then(() => { .then(() => {
const syncDataCopy = { ...store.getters['data/syncData'] }; const syncDataCopy = { ...store.getters['data/syncData'] };
delete syncDataCopy[syncDataToRemove.id]; delete syncDataCopy[syncDataToRemove.id];
@ -562,9 +564,9 @@ function syncWorkspace(workspace, syncToken) {
}) })
.then(() => removeNextItem()); .then(() => removeNextItem());
} }
return result; return promise;
}); });
return result; return promise;
}); });
const getOneFileIdToSync = () => { const getOneFileIdToSync = () => {
@ -621,14 +623,14 @@ function syncWorkspace(workspace, syncToken) {
() => { () => {
if (syncContext.restart) { if (syncContext.restart) {
// Restart sync // Restart sync
return syncWorkspace(workspace, syncToken); return syncWorkspace();
} }
return null; return null;
}, },
(err) => { (err) => {
if (err && err.message === 'TOO_LATE') { if (err && err.message === 'TOO_LATE') {
// Restart sync // Restart sync
return syncWorkspace(workspace, syncToken); return syncWorkspace();
} }
throw err; throw err;
}); });
@ -676,9 +678,7 @@ function requestSync() {
Promise.resolve() Promise.resolve()
.then(() => { .then(() => {
if (isWorkspaceSyncPossible()) { if (isWorkspaceSyncPossible()) {
return syncWorkspace( return syncWorkspace();
store.getters['workspace/currentWorkspace'],
store.getters['workspace/syncToken']);
} }
if (hasCurrentFileSyncLocations()) { if (hasCurrentFileSyncLocations()) {
// Only sync current file if workspace sync is unavailable. // Only sync current file if workspace sync is unavailable.

View File

@ -85,6 +85,17 @@ export default {
} }
return hash; return hash;
}, },
addItemHash(item) {
return {
...item,
hash: this.hash(this.serializeObject({
...item,
// These properties must not be part of the hash
history: undefined,
hash: undefined,
})),
};
},
encodeBase64(str) { encodeBase64(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(`0x${p1}`))); (match, p1) => String.fromCharCode(`0x${p1}`)));

View File

@ -105,16 +105,12 @@ export default {
const data = typeof value.data === 'object' const data = typeof value.data === 'object'
? Object.assign(emptyItem.data, value.data) ? Object.assign(emptyItem.data, value.data)
: value.data; : value.data;
const item = {
// Make item with hash
const item = utils.addItemHash({
...emptyItem, ...emptyItem,
data, data,
}; });
// Calculate item hash
item.hash = utils.hash(utils.serializeObject({
...item,
hash: undefined,
}));
// 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) ? state.lsItemMap : state.itemMap, item.id, item);

View File

@ -76,8 +76,8 @@ export default {
rejectText: 'No', rejectText: 'No',
}), }),
removeWorkspace: ({ dispatch }) => dispatch('open', { removeWorkspace: ({ dispatch }) => dispatch('open', {
content: '<p>This will clean your workspace locally. Are you sure?</p>', content: '<p>You are about to remove a workspace locally. Are you sure?</p>',
resolveText: 'Yes, clean', resolveText: 'Yes, remove',
rejectText: 'No', rejectText: 'No',
}), }),
reset: ({ dispatch }) => dispatch('open', { reset: ({ dispatch }) => dispatch('open', {
@ -100,7 +100,7 @@ export default {
signInForSponsorship: ({ dispatch }, { onResolve }) => dispatch('open', { signInForSponsorship: ({ dispatch }, { onResolve }) => dispatch('open', {
type: 'signInForSponsorship', type: 'signInForSponsorship',
content: `<p>You have to sign in with Google to enable your sponsorship.</p> content: `<p>You have to sign in with Google to enable your sponsorship.</p>
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</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, onResolve,