Workspaces (part 3)
This commit is contained in:
parent
abbe1804e2
commit
32aa259790
@ -1,6 +1,6 @@
|
||||
<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__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">
|
||||
</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()">
|
||||
|
@ -15,6 +15,7 @@
|
||||
<sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal>
|
||||
<!-- Providers -->
|
||||
<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-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>
|
||||
@ -62,6 +63,7 @@ import SponsorModal from './modals/SponsorModal';
|
||||
|
||||
// Providers
|
||||
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
|
||||
import GoogleDriveAccountModal from './modals/providers/GoogleDriveAccountModal';
|
||||
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
|
||||
import GoogleDriveWorkspaceModal from './modals/providers/GoogleDriveWorkspaceModal';
|
||||
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
|
||||
@ -102,6 +104,7 @@ export default {
|
||||
SponsorModal,
|
||||
// Providers
|
||||
GooglePhotoModal,
|
||||
GoogleDriveAccountModal,
|
||||
GoogleDriveSaveModal,
|
||||
GoogleDriveWorkspaceModal,
|
||||
GoogleDrivePublishModal,
|
||||
|
@ -120,6 +120,12 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-bar__panel--hidden {
|
||||
@ -127,11 +133,11 @@ export default {
|
||||
}
|
||||
|
||||
.side-bar__panel--menu {
|
||||
padding: 10px 10px 50px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.side-bar__panel--help {
|
||||
padding: 0 10px 40px 20px;
|
||||
padding: 0 10px 0 20px;
|
||||
|
||||
pre {
|
||||
font-size: 0.9em;
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex';
|
||||
import googleDriveAppDataProvider from '../../services/providers/googleDriveAppDataProvider';
|
||||
import providerRegistry from '../../services/providers/providerRegistry';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import UserImage from '../UserImage';
|
||||
import UserName from '../UserName';
|
||||
@ -83,7 +83,7 @@ export default {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
this.$store.dispatch('queue/enqueue',
|
||||
() => Promise.resolve()
|
||||
.then(() => googleDriveAppDataProvider.getRevisionContent(
|
||||
.then(() => this.workspaceProvider.getRevisionContent(
|
||||
loginToken, currentFile.id, revision.id))
|
||||
.then(resolve, reject));
|
||||
});
|
||||
@ -123,6 +123,10 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Find the workspace provider
|
||||
const workspace = this.$store.getters['workspace/currentWorkspace'];
|
||||
this.workspaceProvider = providerRegistry.providers[workspace.providerId];
|
||||
|
||||
// Watch file changes
|
||||
this.$watch(
|
||||
() => this.$store.getters['file/current'].id,
|
||||
@ -138,7 +142,7 @@ export default {
|
||||
revisionsPromise = new Promise((resolve, reject) => {
|
||||
this.$store.dispatch('queue/enqueue',
|
||||
() => Promise.resolve()
|
||||
.then(() => googleDriveAppDataProvider.listRevisions(loginToken, currentFile.id))
|
||||
.then(() => this.workspaceProvider.listRevisions(loginToken, currentFile.id))
|
||||
.then((revisions) => {
|
||||
resolve(revisions.sort(
|
||||
(revision1, revision2) => revision2.created - revision1.created));
|
||||
|
@ -158,7 +158,10 @@ export default {
|
||||
return this.$store.dispatch('modal/open', 'publishManagement');
|
||||
},
|
||||
addGoogleDriveAccount() {
|
||||
return googleHelper.addDriveAccount()
|
||||
return this.$store.dispatch('modal/open', {
|
||||
type: 'googleDriveAccount',
|
||||
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
|
||||
})
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
addDropboxAccount() {
|
||||
|
@ -139,7 +139,10 @@ export default {
|
||||
return this.$store.dispatch('modal/open', 'syncManagement');
|
||||
},
|
||||
addGoogleDriveAccount() {
|
||||
return googleHelper.addDriveAccount()
|
||||
return this.$store.dispatch('modal/open', {
|
||||
type: 'googleDriveAccount',
|
||||
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
|
||||
})
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
addDropboxAccount() {
|
||||
|
@ -37,7 +37,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
addGoogleDriveWorkspace() {
|
||||
return googleHelper.addDriveAccount()
|
||||
return googleHelper.addDriveAccount(true)
|
||||
.then(token => this.$store.dispatch('modal/open', {
|
||||
type: 'googleDriveWorkspace',
|
||||
token,
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</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__checkbox">
|
||||
<label>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</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__checkbox">
|
||||
<label>
|
||||
|
34
src/components/modals/providers/GoogleDriveAccountModal.vue
Normal file
34
src/components/modals/providers/GoogleDriveAccountModal.vue
Normal 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>
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="zendesk"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keyup.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
|
@ -3,6 +3,7 @@ export default () => ({
|
||||
htmlExportTemplate: 'styledHtml',
|
||||
pdfExportTemplate: 'styledHtml',
|
||||
pandocExportFormat: 'pdf',
|
||||
googleDriveRestrictedAccess: false,
|
||||
googleDriveFolderId: '',
|
||||
googleDriveWorkspaceFolderId: '',
|
||||
googleDrivePublishFormat: 'markdown',
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Auto-sync frequency (in ms). Minimum is 60000.
|
||||
autoSyncEvery: 90000
|
||||
autoSyncEvery: 60000
|
||||
# Adjust font size in editor and preview
|
||||
fontSizeFactor: 1
|
||||
# Adjust maximum text width in editor and preview
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="icon-provider" :class="'icon-provider--' + classState">
|
||||
<icon-sync-off v-if="!classState"></icon-sync-off>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -11,9 +11,10 @@ export default providerRegistry.register({
|
||||
// Nothing to do since the main workspace isn't necessarily synchronized
|
||||
return Promise.resolve(store.getters['data/workspaces'].main);
|
||||
},
|
||||
getChanges(token) {
|
||||
getChanges() {
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
|
||||
return googleHelper.getChanges(token, startPageToken, 'appDataFolder')
|
||||
return googleHelper.getChanges(syncToken, startPageToken, true)
|
||||
.then((result) => {
|
||||
const changes = result.changes.filter((change) => {
|
||||
if (change.file) {
|
||||
@ -30,7 +31,6 @@ export default providerRegistry.register({
|
||||
type: change.item.type,
|
||||
hash: change.item.hash,
|
||||
};
|
||||
change.file = undefined;
|
||||
}
|
||||
change.syncDataId = change.fileId;
|
||||
return true;
|
||||
@ -41,19 +41,18 @@ export default providerRegistry.register({
|
||||
},
|
||||
setAppliedChanges(changes) {
|
||||
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(
|
||||
token,
|
||||
JSON.stringify(item),
|
||||
['appDataFolder'],
|
||||
undefined,
|
||||
undefined,
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
)
|
||||
syncToken,
|
||||
JSON.stringify(item),
|
||||
undefined,
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
)
|
||||
.then(file => ({
|
||||
// Build sync data
|
||||
id: file.id,
|
||||
@ -62,19 +61,20 @@ export default providerRegistry.register({
|
||||
hash: item.hash,
|
||||
}));
|
||||
},
|
||||
removeItem(token, syncData, ifNotTooLate) {
|
||||
return googleHelper.removeAppDataFile(token, syncData.id, ifNotTooLate)
|
||||
.then(() => syncData);
|
||||
removeItem(syncData, ifNotTooLate) {
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
return googleHelper.removeAppDataFile(syncToken, syncData.id, ifNotTooLate);
|
||||
},
|
||||
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];
|
||||
if (!syncData) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return googleHelper.downloadAppDataFile(token, syncData.id)
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
return googleHelper.downloadAppDataFile(syncToken, syncData.id)
|
||||
.then((content) => {
|
||||
const item = JSON.parse(content);
|
||||
if (item.hash !== syncData.hash) {
|
||||
@ -89,27 +89,26 @@ export default providerRegistry.register({
|
||||
});
|
||||
},
|
||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||
return this.uploadData(token, content, `${syncLocation.fileId}/content`, ifNotTooLate)
|
||||
return this.uploadData(content, `${syncLocation.fileId}/content`, ifNotTooLate)
|
||||
.then(() => syncLocation);
|
||||
},
|
||||
uploadData(token, item, dataId, ifNotTooLate) {
|
||||
uploadData(item, dataId, ifNotTooLate) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
||||
if (syncData && syncData.hash === item.hash) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
return googleHelper.uploadAppDataFile(
|
||||
token,
|
||||
JSON.stringify({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
}),
|
||||
['appDataFolder'],
|
||||
undefined,
|
||||
JSON.stringify(item),
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
)
|
||||
syncToken,
|
||||
JSON.stringify({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
}),
|
||||
JSON.stringify(item),
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
)
|
||||
.then(file => store.dispatch('data/patchSyncData', {
|
||||
[file.id]: {
|
||||
// Build sync data
|
||||
@ -125,7 +124,7 @@ export default providerRegistry.register({
|
||||
if (!syncData) {
|
||||
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 => ({
|
||||
id: revision.id,
|
||||
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
|
||||
@ -137,7 +136,7 @@ export default providerRegistry.register({
|
||||
if (!syncData) {
|
||||
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));
|
||||
},
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import store from '../../store';
|
||||
import googleHelper from './helpers/googleHelper';
|
||||
import providerRegistry from './providerRegistry';
|
||||
import providerUtils from './providerUtils';
|
||||
import utils from '../utils';
|
||||
|
||||
export default providerRegistry.register({
|
||||
@ -32,7 +33,7 @@ export default providerRegistry.register({
|
||||
[folder.id],
|
||||
{ folderId: folder.id },
|
||||
undefined,
|
||||
'application/vnd.google-apps.folder',
|
||||
googleHelper.folderMimeType,
|
||||
)
|
||||
.then(dataFolder => ({
|
||||
...properties,
|
||||
@ -50,7 +51,7 @@ export default providerRegistry.register({
|
||||
[folder.id],
|
||||
{ folderId: folder.id },
|
||||
undefined,
|
||||
'application/vnd.google-apps.folder',
|
||||
googleHelper.folderMimeType,
|
||||
)
|
||||
.then(trashFolder => ({
|
||||
...properties,
|
||||
@ -71,7 +72,7 @@ export default providerRegistry.register({
|
||||
undefined,
|
||||
properties,
|
||||
undefined,
|
||||
'application/vnd.google-apps.folder',
|
||||
googleHelper.folderMimeType,
|
||||
folder.id,
|
||||
)
|
||||
.then(() => properties);
|
||||
@ -109,12 +110,12 @@ export default providerRegistry.register({
|
||||
const googleTokens = store.getters['data/googleTokens'];
|
||||
// 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];
|
||||
if (token && token.isDrive) {
|
||||
if (token && token.isDrive && token.driveFullAccess) {
|
||||
return token;
|
||||
}
|
||||
// If no token has been found, popup an authorize window and get one
|
||||
return store.dispatch('modal/workspaceGoogleRedirection', {
|
||||
onResolve: () => googleHelper.addDriveAccount(),
|
||||
onResolve: () => googleHelper.addDriveAccount(true),
|
||||
});
|
||||
})
|
||||
.then(token => Promise.resolve()
|
||||
@ -125,7 +126,7 @@ export default providerRegistry.register({
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
'application/vnd.google-apps.folder',
|
||||
googleHelper.folderMimeType,
|
||||
)
|
||||
.then(folder => initFolder(token, {
|
||||
...folder,
|
||||
@ -135,27 +136,109 @@ export default providerRegistry.register({
|
||||
// If workspace does not exist, initialize one
|
||||
.then(folderId => getWorkspace(folderId) || googleHelper.getFile(token, folderId)
|
||||
.then((folder) => {
|
||||
folder.appProperties = folder.appProperties || {};
|
||||
const folderIdProperty = folder.appProperties.folderId;
|
||||
if (folderIdProperty && folderIdProperty !== folderId) {
|
||||
throw new Error(`Google Drive folder ${folderId} is part of another workspace.`);
|
||||
}
|
||||
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;
|
||||
return googleHelper.getChanges(token, startPageToken, 'appDataFolder')
|
||||
return googleHelper.getChanges(syncToken, startPageToken, false)
|
||||
.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) {
|
||||
// Parse item from file name
|
||||
try {
|
||||
change.item = JSON.parse(change.file.name);
|
||||
} catch (e) {
|
||||
return false;
|
||||
// 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 {
|
||||
change.item = JSON.parse(change.file.name);
|
||||
} 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;
|
||||
}
|
||||
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
|
||||
change.syncData = {
|
||||
id: change.fileId,
|
||||
@ -163,13 +246,237 @@ export default providerRegistry.register({
|
||||
type: change.item.type,
|
||||
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;
|
||||
return true;
|
||||
changes.push(change);
|
||||
if (contentChange) {
|
||||
changes.push(contentChange);
|
||||
}
|
||||
});
|
||||
changes.startPageToken = result.startPageToken;
|
||||
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));
|
||||
},
|
||||
});
|
||||
|
@ -30,6 +30,7 @@ const checkIdToken = (idToken) => {
|
||||
};
|
||||
|
||||
export default {
|
||||
folderMimeType: 'application/vnd.google-apps.folder',
|
||||
request(token, options) {
|
||||
return networkSvc.request({
|
||||
...options,
|
||||
@ -120,6 +121,53 @@ export default {
|
||||
raw: true,
|
||||
}).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) {
|
||||
return networkSvc.request({
|
||||
method: 'GET',
|
||||
@ -168,7 +216,7 @@ export default {
|
||||
expiresOn: Date.now() + (data.expiresIn * 1000),
|
||||
idToken: data.idToken,
|
||||
sub: `${res.body.sub}`,
|
||||
isLogin: !store.getters['workspace/loginToken'] &&
|
||||
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 ||
|
||||
@ -297,20 +345,24 @@ export default {
|
||||
addPhotosAccount() {
|
||||
return this.startOauth2(photosScopes);
|
||||
},
|
||||
getChanges(token, startPageToken, spaces) {
|
||||
getChanges(token, startPageToken, isAppData) {
|
||||
const result = {
|
||||
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) => {
|
||||
const getPage = (pageToken = '1') => this.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: 'https://www.googleapis.com/drive/v3/changes',
|
||||
params: {
|
||||
pageToken,
|
||||
spaces,
|
||||
spaces: isAppData ? 'appDataFolder' : 'drive',
|
||||
pageSize: 1000,
|
||||
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file/name,file/appProperties)',
|
||||
fields: `nextPageToken,newStartPageToken,changes(fileId,${fileFields})`,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
@ -328,12 +380,28 @@ export default {
|
||||
uploadFile(token, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate) {
|
||||
return this.refreshToken(token, getDriveScopes(token))
|
||||
.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)
|
||||
.then(refreshedToken => this.uploadFileInternal(
|
||||
refreshedToken, name, parents, appProperties, media, undefined, fileId, ifNotTooLate));
|
||||
refreshedToken,
|
||||
name,
|
||||
['appDataFolder'],
|
||||
undefined,
|
||||
media,
|
||||
undefined,
|
||||
fileId,
|
||||
ifNotTooLate,
|
||||
));
|
||||
},
|
||||
getFile(token, id) {
|
||||
return this.refreshToken(token, getDriveScopes(token))
|
||||
@ -354,52 +422,31 @@ export default {
|
||||
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)
|
||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||
.then(ifNotTooLate(refreshedToken => this.request(refreshedToken, {
|
||||
method: 'DELETE',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||
})));
|
||||
.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) => {
|
||||
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();
|
||||
});
|
||||
.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.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`,
|
||||
raw: true,
|
||||
}).then(res => res.body));
|
||||
.then(refreshedToken => this.downloadFileRevisionInternal(
|
||||
refreshedToken, fileId, revisionId));
|
||||
},
|
||||
uploadBlogger(
|
||||
token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage,
|
||||
@ -475,6 +522,7 @@ export default {
|
||||
let picker;
|
||||
const pickerBuilder = new google.picker.PickerBuilder()
|
||||
.setOAuthToken(refreshedToken.accessToken)
|
||||
.hideTitleBar()
|
||||
.setCallback((data) => {
|
||||
switch (data[google.picker.Response.ACTION]) {
|
||||
case google.picker.Action.PICKED:
|
||||
@ -488,27 +536,35 @@ export default {
|
||||
switch (type) {
|
||||
default:
|
||||
case 'doc': {
|
||||
const view = new google.picker.DocsView(google.picker.ViewId.DOCS);
|
||||
view.setParent('root');
|
||||
view.setIncludeFolders(true);
|
||||
view.setMimeTypes([
|
||||
'text/plain',
|
||||
'text/x-markdown',
|
||||
'application/octet-stream',
|
||||
].join(','));
|
||||
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
|
||||
const addView = (hasRootParent) => {
|
||||
const view = new google.picker.DocsView(google.picker.ViewId.DOCS);
|
||||
if (hasRootParent) {
|
||||
view.setParent('root');
|
||||
}
|
||||
view.setMimeTypes([
|
||||
'text/plain',
|
||||
'text/x-markdown',
|
||||
'application/octet-stream',
|
||||
].join(','));
|
||||
pickerBuilder.addView(view);
|
||||
};
|
||||
pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
|
||||
pickerBuilder.addView(view);
|
||||
addView(false);
|
||||
// addView(true);
|
||||
break;
|
||||
}
|
||||
case 'folder': {
|
||||
const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
|
||||
view.setParent('root');
|
||||
view.setIncludeFolders(true);
|
||||
view.setSelectFolderEnabled(true);
|
||||
view.setMimeTypes('application/vnd.google-apps.folder');
|
||||
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
|
||||
pickerBuilder.addView(view);
|
||||
const addView = (hasRootParent) => {
|
||||
const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
|
||||
if (hasRootParent) {
|
||||
view.setParent('root');
|
||||
}
|
||||
view.setSelectFolderEnabled(true);
|
||||
view.setMimeTypes(this.folderMimeType);
|
||||
pickerBuilder.addView(view);
|
||||
};
|
||||
addView(false);
|
||||
// addView(true);
|
||||
break;
|
||||
}
|
||||
case 'img': {
|
||||
|
@ -51,11 +51,7 @@ export default {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
result.hash = utils.hash(utils.serializeObject({
|
||||
...result,
|
||||
hash: undefined,
|
||||
}));
|
||||
return result;
|
||||
return utils.addItemHash(result);
|
||||
},
|
||||
/**
|
||||
* Find and open a file location that fits the criteria
|
||||
|
@ -92,35 +92,39 @@ function cleanSyncedContent(syncedContent) {
|
||||
function applyChanges(changes) {
|
||||
const storeItemMap = { ...store.getters.allItemMap };
|
||||
const syncData = { ...store.getters['data/syncData'] };
|
||||
let syncDataChanged = false;
|
||||
let saveSyncData = false;
|
||||
|
||||
changes.forEach((change) => {
|
||||
const existingSyncData = syncData[change.syncDataId];
|
||||
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) {
|
||||
// Remove object from the store
|
||||
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
|
||||
delete storeItemMap[existingItem.id];
|
||||
}
|
||||
delete syncData[change.syncDataId];
|
||||
syncDataChanged = true;
|
||||
} else if (!change.removed && change.item && change.item.hash) {
|
||||
if (!existingSyncData || (existingSyncData.hash !== change.item.hash && (
|
||||
!existingItem || existingItem.hash !== change.item.hash
|
||||
))) {
|
||||
// Put object in the store, except for content and data which will be merge later
|
||||
if (change.item.type !== 'content' && change.item.type !== 'data') {
|
||||
store.commit(`${change.item.type}/setItem`, change.item);
|
||||
storeItemMap[change.item.id] = change.item;
|
||||
}
|
||||
}
|
||||
} else if (change.item && change.item.hash) {
|
||||
// Item was modifed
|
||||
syncData[change.syncDataId] = change.syncData;
|
||||
syncDataChanged = true;
|
||||
saveSyncData = true;
|
||||
if (
|
||||
// If no sync data or existing one is different
|
||||
(existingSyncData || {}).hash !== change.item.hash
|
||||
// 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);
|
||||
storeItemMap[change.item.id] = change.item;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (syncDataChanged) {
|
||||
if (saveSyncData) {
|
||||
store.dispatch('data/setSyncData', syncData);
|
||||
}
|
||||
}
|
||||
@ -405,8 +409,7 @@ function syncDataItem(dataId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
return syncToken && syncProvider.downloadData(syncToken, dataId)
|
||||
return syncProvider.downloadData(dataId)
|
||||
.then((serverItem = null) => {
|
||||
const dataSyncData = store.getters['data/dataSyncData'][dataId];
|
||||
let mergedItem = (() => {
|
||||
@ -452,11 +455,7 @@ function syncDataItem(dataId) {
|
||||
if (serverItem && serverItem.hash === mergedItem.hash) {
|
||||
return null;
|
||||
}
|
||||
return syncProvider.uploadData(
|
||||
syncToken,
|
||||
mergedItem,
|
||||
dataId,
|
||||
);
|
||||
return syncProvider.uploadData(mergedItem, dataId);
|
||||
})
|
||||
.then(() => {
|
||||
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.
|
||||
*/
|
||||
function syncWorkspace(workspace, syncToken) {
|
||||
function syncWorkspace() {
|
||||
const workspace = store.getters['workspace/currentWorkspace'];
|
||||
const syncContext = new SyncContext();
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// 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'];
|
||||
if (!localSettings.syncSub) {
|
||||
store.dispatch('data/patchLocalSettings', {
|
||||
workspaceSyncSub: syncToken.sub,
|
||||
syncSub: syncToken.sub,
|
||||
});
|
||||
} else if (localSettings.syncSub !== syncToken.sub) {
|
||||
throw new Error('Synchronization failed due to token inconsistency.');
|
||||
}
|
||||
})
|
||||
.then(() => syncProvider.getChanges(syncToken))
|
||||
.then(() => syncProvider.getChanges())
|
||||
.then((changes) => {
|
||||
// Apply changes
|
||||
applyChanges(changes);
|
||||
@ -509,15 +510,16 @@ function syncWorkspace(workspace, syncToken) {
|
||||
// Deal with contents and data later
|
||||
};
|
||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||
let result;
|
||||
let promise;
|
||||
Object.entries(storeItemMap).some(([id, item]) => {
|
||||
const existingSyncData = syncDataByItemId[id];
|
||||
if ((!existingSyncData || existingSyncData.hash !== item.hash) &&
|
||||
// Add file if content has been uploaded
|
||||
(item.type !== 'file' || syncDataByItemId[`${id}/content`])
|
||||
if ((!existingSyncData || existingSyncData.hash !== item.hash)
|
||||
// Add file/folder if parent has been added
|
||||
&& (!storeItemMap[item.parentId] || syncDataByItemId[item.parentId])
|
||||
// Add file if content has been added
|
||||
&& (item.type !== 'file' || syncDataByItemId[`${id}/content`])
|
||||
) {
|
||||
result = syncProvider.saveItem(
|
||||
syncToken,
|
||||
promise = syncProvider.saveSimpleItem(
|
||||
// Use deepCopy to freeze objects
|
||||
utils.deepCopy(item),
|
||||
utils.deepCopy(existingSyncData),
|
||||
@ -528,9 +530,9 @@ function syncWorkspace(workspace, syncToken) {
|
||||
}))
|
||||
.then(() => saveNextItem());
|
||||
}
|
||||
return result;
|
||||
return promise;
|
||||
});
|
||||
return result;
|
||||
return promise;
|
||||
});
|
||||
|
||||
// Called until no item to remove
|
||||
@ -543,7 +545,7 @@ function syncWorkspace(workspace, syncToken) {
|
||||
...store.state.content.itemMap,
|
||||
};
|
||||
const syncData = store.getters['data/syncData'];
|
||||
let result;
|
||||
let promise;
|
||||
Object.entries(syncData).some(([, existingSyncData]) => {
|
||||
if (!storeItemMap[existingSyncData.itemId] &&
|
||||
// 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
|
||||
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
||||
result = syncProvider
|
||||
.removeItem(syncToken, syncDataToRemove, ifNotTooLate)
|
||||
promise = syncProvider
|
||||
.removeItem(syncDataToRemove, ifNotTooLate)
|
||||
.then(() => {
|
||||
const syncDataCopy = { ...store.getters['data/syncData'] };
|
||||
delete syncDataCopy[syncDataToRemove.id];
|
||||
@ -562,9 +564,9 @@ function syncWorkspace(workspace, syncToken) {
|
||||
})
|
||||
.then(() => removeNextItem());
|
||||
}
|
||||
return result;
|
||||
return promise;
|
||||
});
|
||||
return result;
|
||||
return promise;
|
||||
});
|
||||
|
||||
const getOneFileIdToSync = () => {
|
||||
@ -621,14 +623,14 @@ function syncWorkspace(workspace, syncToken) {
|
||||
() => {
|
||||
if (syncContext.restart) {
|
||||
// Restart sync
|
||||
return syncWorkspace(workspace, syncToken);
|
||||
return syncWorkspace();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(err) => {
|
||||
if (err && err.message === 'TOO_LATE') {
|
||||
// Restart sync
|
||||
return syncWorkspace(workspace, syncToken);
|
||||
return syncWorkspace();
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
@ -676,9 +678,7 @@ function requestSync() {
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (isWorkspaceSyncPossible()) {
|
||||
return syncWorkspace(
|
||||
store.getters['workspace/currentWorkspace'],
|
||||
store.getters['workspace/syncToken']);
|
||||
return syncWorkspace();
|
||||
}
|
||||
if (hasCurrentFileSyncLocations()) {
|
||||
// Only sync current file if workspace sync is unavailable.
|
||||
|
@ -85,6 +85,17 @@ export default {
|
||||
}
|
||||
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) {
|
||||
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
||||
(match, p1) => String.fromCharCode(`0x${p1}`)));
|
||||
|
@ -105,16 +105,12 @@ export default {
|
||||
const data = typeof value.data === 'object'
|
||||
? Object.assign(emptyItem.data, value.data)
|
||||
: value.data;
|
||||
const item = {
|
||||
|
||||
// Make item with hash
|
||||
const item = utils.addItemHash({
|
||||
...emptyItem,
|
||||
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
|
||||
Vue.set(lsItemIdSet.has(item.id) ? state.lsItemMap : state.itemMap, item.id, item);
|
||||
|
@ -76,8 +76,8 @@ export default {
|
||||
rejectText: 'No',
|
||||
}),
|
||||
removeWorkspace: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>This will clean your workspace locally. Are you sure?</p>',
|
||||
resolveText: 'Yes, clean',
|
||||
content: '<p>You are about to remove a workspace locally. Are you sure?</p>',
|
||||
resolveText: 'Yes, remove',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
reset: ({ dispatch }) => dispatch('open', {
|
||||
@ -100,7 +100,7 @@ export default {
|
||||
signInForSponsorship: ({ dispatch }, { onResolve }) => dispatch('open', {
|
||||
type: 'signInForSponsorship',
|
||||
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',
|
||||
rejectText: 'Cancel',
|
||||
onResolve,
|
||||
|
Loading…
Reference in New Issue
Block a user