Support for google drive actions

This commit is contained in:
Benoit Schweblin 2018-01-04 20:19:10 +00:00
parent 2f4781bc5d
commit d0ea2f7850
36 changed files with 388 additions and 166 deletions

View File

@ -16,7 +16,9 @@ module.exports = {
], ],
globals: { globals: {
"NODE_ENV": false, "NODE_ENV": false,
"VERSION": false "VERSION": false,
"GOOGLE_CLIENT_ID": false,
"GITHUB_CLIENT_ID": false
}, },
// check if imports actually resolve // check if imports actually resolve
'settings': { 'settings': {

View File

@ -4,6 +4,9 @@ var config = require('../config')
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
} }
if (!process.env.GOOGLE_CLIENT_ID) {
process.env.GOOGLE_CLIENT_ID = JSON.parse(config.dev.env.GOOGLE_CLIENT_ID)
}
var opn = require('opn') var opn = require('opn')
var path = require('path') var path = require('path')

View File

@ -19,7 +19,9 @@ module.exports = merge(baseWebpackConfig, {
devtool: 'source-map', devtool: 'source-map',
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
NODE_ENV: config.dev.env.NODE_ENV NODE_ENV: config.dev.env.NODE_ENV,
GOOGLE_CLIENT_ID: config.dev.env.GOOGLE_CLIENT_ID,
GITHUB_CLIENT_ID: config.dev.env.GITHUB_CLIENT_ID
}), }),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),

View File

@ -28,7 +28,9 @@ var webpackConfig = merge(baseWebpackConfig, {
plugins: [ plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html // http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({ new webpack.DefinePlugin({
NODE_ENV: env.NODE_ENV NODE_ENV: env.NODE_ENV,
GOOGLE_CLIENT_ID: env.GOOGLE_CLIENT_ID,
GITHUB_CLIENT_ID: env.GITHUB_CLIENT_ID
}), }),
new webpack.optimize.UglifyJsPlugin({ new webpack.optimize.UglifyJsPlugin({
compress: { compress: {

View File

@ -2,5 +2,7 @@ var merge = require('webpack-merge')
var prodEnv = require('./prod.env') var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, { module.exports = merge(prodEnv, {
NODE_ENV: '"development"' NODE_ENV: '"development"',
GOOGLE_CLIENT_ID: '"241271498917-c3loeet001r90q6u79q484bsh5clg4fr.apps.googleusercontent.com"',
GITHUB_CLIENT_ID: '"cbf0cf25cfd026be23e1"'
}) })

View File

@ -33,6 +33,7 @@ module.exports = {
// (https://github.com/webpack/css-loader#sourcemaps) // (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected, // In our experience, they generally work as expected,
// just be aware of this issue when enabling this option. // just be aware of this issue when enabling this option.
cssSourceMap: false // cssSourceMap: false
cssSourceMap: true
} }
} }

View File

@ -1,3 +1,5 @@
module.exports = { module.exports = {
NODE_ENV: '"production"' NODE_ENV: '"production"',
GOOGLE_CLIENT_ID: '"241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com"',
GITHUB_CLIENT_ID: '"30c1491057c9ad4dbd56"'
} }

View File

@ -1,4 +1,11 @@
process.env.NODE_ENV = 'production'; const env = require('./config/prod.env');
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(env.NODE_ENV);
}
if (!process.env.GOOGLE_CLIENT_ID) {
process.env.GOOGLE_CLIENT_ID = JSON.parse(env.GOOGLE_CLIENT_ID);
}
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');

View File

@ -44,8 +44,11 @@ module.exports = (app, serveV4) => {
/* eslint-enable global-require, import/no-unresolved */ /* eslint-enable global-require, import/no-unresolved */
} }
// Serve callback.html in /app // Serve callback.html
app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html'))); app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html')));
// Google Drive action receiver
app.get('/googleDriveAction', (req, res) =>
res.redirect(`./app#providerId=googleDrive&state=${encodeURIComponent(req.query.state)}`));
// Serve static resources // Serve static resources
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {

View File

@ -5,7 +5,7 @@ const verifier = require('google-id-token-verifier');
const BUCKET_NAME = process.env.USER_BUCKET_NAME || 'stackedit-users'; const BUCKET_NAME = process.env.USER_BUCKET_NAME || 'stackedit-users';
const PAYPAL_URI = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr'; const PAYPAL_URI = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr';
const PAYPAL_RECEIVER_EMAIL = process.env.PAYPAL_RECEIVER_EMAIL || 'stackedit.project@gmail.com'; const PAYPAL_RECEIVER_EMAIL = process.env.PAYPAL_RECEIVER_EMAIL || 'stackedit.project@gmail.com';
const GOOGLE_CLIENT_ID = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com'; const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const s3Client = new AWS.S3(); const s3Client = new AWS.S3();
const cb = (resolve, reject) => (err, res) => { const cb = (resolve, reject) => (err, res) => {

View File

@ -158,7 +158,7 @@ export default {
.side-bar__info { .side-bar__info {
padding: 10px; padding: 10px;
margin: 0 -10px; margin: -10px -10px 0;
background-color: $info-bg; background-color: $info-bg;
p { p {

View File

@ -88,6 +88,7 @@ export default {
<style lang="scss"> <style lang="scss">
.toc__inner { .toc__inner {
position: relative;
color: rgba(0, 0, 0, 0.75); color: rgba(0, 0, 0, 0.75);
cursor: pointer; cursor: pointer;
font-size: 9px; font-size: 9px;
@ -147,7 +148,7 @@ export default {
left: 0; left: 0;
width: 100%; width: 100%;
height: 35px; height: 35px;
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(255, 255, 255, 0.2);
pointer-events: none; pointer-events: none;
} }
</style> </style>

View File

@ -230,9 +230,9 @@ textarea {
color: rgba(0, 0, 0, 0.33); color: rgba(0, 0, 0, 0.33);
position: absolute; position: absolute;
left: 0; left: 0;
padding: 1px; padding: 2px 3px 2px 0;
width: 20px; width: 20px;
height: 20px; height: 21px;
line-height: 1; line-height: 1;
&:active, &:active,

View File

@ -101,16 +101,18 @@ export default {
start, end, start, end,
})); }));
const isSticky = this.$el.parentNode.classList.contains('sticky-comment');
const isVisible = () => isSticky || this.$store.state.discussion.stickyComment === null;
this.$watch( this.$watch(
() => this.$store.state.discussion.currentDiscussionId, () => this.$store.state.discussion.currentDiscussionId,
() => this.$nextTick(() => { () => this.$nextTick(() => {
if (this.$store.state.discussion.newCommentFocus) { if (isVisible() && this.$store.state.discussion.newCommentFocus) {
clEditor.focus(); clEditor.focus();
} }
}), }),
{ immediate: true }); { immediate: true });
const isSticky = this.$el.parentNode.classList.contains('sticky-comment');
if (isSticky) { if (isSticky) {
let scrollerMirrorElt; let scrollerMirrorElt;
const getScrollerMirrorElt = () => { const getScrollerMirrorElt = () => {
@ -128,10 +130,10 @@ export default {
} else { } else {
// Maintain the state with the sticky comment // Maintain the state with the sticky comment
this.$watch( this.$watch(
() => this.$store.state.discussion.stickyComment === null, () => isVisible(),
(isVisible) => { (visible) => {
clEditor.toggleEditable(isVisible); clEditor.toggleEditable(visible);
if (isVisible) { if (visible) {
const text = this.$store.state.discussion.newCommentText; const text = this.$store.state.discussion.newCommentText;
clEditor.setContent(text); clEditor.setContent(text);
const selection = this.$store.state.discussion.newCommentSelection; const selection = this.$store.state.discussion.newCommentSelection;

View File

@ -1,19 +1,14 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<div v-if="!loginToken"> <div class="menu-info-entries" v-if="!loginToken">
<div class="menu-entry menu-entry--info flex flex--row flex--align-center"> <div class="menu-entry menu-entry--info flex flex--row flex--align-center">
<div class="menu-entry__icon menu-entry__icon--disabled"> <div class="menu-entry__icon menu-entry__icon--disabled">
<icon-sync-off></icon-sync-off> <icon-sync-off></icon-sync-off>
</div> </div>
<span><b>{{currentWorkspace.name}}</b> not synced.</span> <span><b>{{currentWorkspace.name}}</b> not synced.</span>
</div> </div>
<menu-entry @click.native="signin">
<icon-login slot="icon"></icon-login>
<div>Sign in with Google</div>
<span>Back up and sync all your files, folders and settings.</span>
</menu-entry>
</div> </div>
<div v-else> <div class="menu-info-entries" v-else>
<div class="menu-entry menu-entry--info flex flex--row flex--align-center"> <div class="menu-entry menu-entry--info flex flex--row flex--align-center">
<div class="menu-entry__icon menu-entry__icon--image"> <div class="menu-entry__icon menu-entry__icon--image">
<user-image :user-id="loginToken.sub"></user-image> <user-image :user-id="loginToken.sub"></user-image>
@ -27,6 +22,11 @@
<span><b>{{currentWorkspace.name}}</b> synced.</span> <span><b>{{currentWorkspace.name}}</b> synced.</span>
</div> </div>
</div> </div>
<menu-entry v-if="!loginToken" @click.native="signin">
<icon-login slot="icon"></icon-login>
<div>Sign in with Google</div>
<span>Back up and sync your main workspace.</span>
</menu-entry>
<menu-entry @click.native="setPanel('workspaces')"> <menu-entry @click.native="setPanel('workspaces')">
<icon-database slot="icon"></icon-database> <icon-database slot="icon"></icon-database>
<div>Workspaces</div> <div>Workspaces</div>

View File

@ -30,18 +30,14 @@
</div> </div>
</label> </label>
<hr> <hr>
<menu-entry @click.native="about">
<icon-help-circle slot="icon"></icon-help-circle>
<span>About StackEdit</span>
</menu-entry>
<menu-entry @click.native="welcomeFile">
<icon-file slot="icon"></icon-file>
<span>Welcome file</span>
</menu-entry>
<menu-entry href="editor" target="_blank"> <menu-entry href="editor" target="_blank">
<icon-open-in-new slot="icon"></icon-open-in-new> <icon-open-in-new slot="icon"></icon-open-in-new>
<span>StackEdit 4 (deprecated)</span> <span>StackEdit 4 (deprecated)</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="about">
<icon-help-circle slot="icon"></icon-help-circle>
<span>About StackEdit</span>
</menu-entry>
</div> </div>
</template> </template>
@ -49,7 +45,6 @@
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import backupSvc from '../../services/backupSvc'; import backupSvc from '../../services/backupSvc';
import utils from '../../services/utils'; import utils from '../../services/utils';
import welcomeFile from '../../data/welcomeFile.md';
export default { export default {
components: { components: {
@ -104,13 +99,6 @@ export default {
location.reload(); location.reload();
}); });
}, },
welcomeFile() {
return this.$store.dispatch('createFile', {
name: 'Welcome file',
text: welcomeFile,
})
.then(createdFile => this.$store.commit('file/setCurrentId', createdFile.id));
},
about() { about() {
return this.$store.dispatch('modal/open', 'about'); return this.$store.dispatch('modal/open', 'about');
}, },

View File

@ -42,10 +42,15 @@
} }
} }
.menu-info-entries {
padding: 10px;
margin: -10px -10px 10px;
background-color: rgba(255, 255, 255, 0.2);
}
.menu-entry--info { .menu-entry--info {
padding-top: 0; padding-top: 3px;
padding-bottom: 0; padding-bottom: 3px;
margin: 10px 0;
} }
.menu-entry__icon { .menu-entry__icon {

View File

@ -36,8 +36,8 @@ export default modalTemplate({
}), }),
computed: { computed: {
googlePhotosTokens() { googlePhotosTokens() {
const googleToken = this.$store.getters['data/googleTokens']; const googleTokens = this.$store.getters['data/googleTokens'];
return Object.entries(googleToken) return Object.entries(googleTokens)
.filter(([, token]) => token.isPhotos) .filter(([, token]) => token.isPhotos)
.sort(([, token1], [, token2]) => token1.name.localeCompare(token2.name)); .sort(([, token1], [, token2]) => token1.name.localeCompare(token2.name));
}, },

View File

@ -18,7 +18,7 @@
<button class="workspace-entry__button button" @click="edit(id)"> <button class="workspace-entry__button button" @click="edit(id)">
<icon-pen></icon-pen> <icon-pen></icon-pen>
</button> </button>
<button class="workspace-entry__button button" v-if="workspace !== currentWorkspace && workspace !== mainWorkspace" @click="remove(id)"> <button class="workspace-entry__button button" v-if="id !== currentWorkspace.id && id !== mainWorkspace.id" @click="remove(id)">
<icon-delete></icon-delete> <icon-delete></icon-delete>
</button> </button>
</div> </div>

View File

@ -1,6 +1,6 @@
export default () => ({ export default () => ({
main: { main: {
name: 'Main workspace', name: 'Main workspace',
// The rest will be filled by the data/workspaces getter // The rest will be filled by the data/sanitizedWorkspaces getter
}, },
}); });

View File

@ -1,5 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20C4,21.1 4.9,22 6,22H18C19.1,22 20,21.1 20,20V8L14,2H6Z" />
</svg>
</template>

View File

@ -40,7 +40,6 @@ import Information from './Information';
import Alert from './Alert'; import Alert from './Alert';
import SignalOff from './SignalOff'; import SignalOff from './SignalOff';
import Folder from './Folder'; import Folder from './Folder';
import File from './File';
import ScrollSync from './ScrollSync'; import ScrollSync from './ScrollSync';
import Printer from './Printer'; import Printer from './Printer';
import Undo from './Undo'; import Undo from './Undo';
@ -91,7 +90,6 @@ Vue.component('iconInformation', Information);
Vue.component('iconAlert', Alert); Vue.component('iconAlert', Alert);
Vue.component('iconSignalOff', SignalOff); Vue.component('iconSignalOff', SignalOff);
Vue.component('iconFolder', Folder); Vue.component('iconFolder', Folder);
Vue.component('iconFile', File);
Vue.component('iconScrollSync', ScrollSync); Vue.component('iconScrollSync', ScrollSync);
Vue.component('iconPrinter', Printer); Vue.component('iconPrinter', Printer);
Vue.component('iconUndo', Undo); Vue.component('iconUndo', Undo);

View File

@ -158,12 +158,18 @@ function mergeContent(serverContent, clientContent, lastMergedContent = {}) {
const serverText = makePatchableText(serverContent, markerKeys, markerIdxMap); const serverText = makePatchableText(serverContent, markerKeys, markerIdxMap);
const clientText = makePatchableText(clientContent, markerKeys, markerIdxMap); const clientText = makePatchableText(clientContent, markerKeys, markerIdxMap);
const isServerTextChanges = lastMergedText !== serverText; const isServerTextChanges = lastMergedText !== serverText;
const isClientTextChanges = lastMergedText !== clientText;
const isTextSynchronized = serverText === clientText; const isTextSynchronized = serverText === clientText;
let text = clientText;
if (!isTextSynchronized && isServerTextChanges) {
text = serverText;
if (isClientTextChanges) {
text = mergeText(serverText, clientText, lastMergedText);
}
}
const result = { const result = {
text: isTextSynchronized || !isServerTextChanges text,
? clientText
: mergeText(serverText, clientText, lastMergedText),
properties: mergeValues( properties: mergeValues(
serverContent.properties, serverContent.properties,
clientContent.properties, clientContent.properties,

View File

@ -6,6 +6,7 @@ import welcomeFile from '../data/welcomeFile.md';
const dbVersion = 1; const dbVersion = 1;
const dbStoreName = 'objects'; const dbStoreName = 'objects';
const exportWorkspace = utils.queryParams.exportWorkspace; const exportWorkspace = utils.queryParams.exportWorkspace;
const resetApp = utils.queryParams.reset;
const deleteMarkerMaxAge = 1000; const deleteMarkerMaxAge = 1000;
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
@ -105,7 +106,7 @@ const localDbSvc = {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
// Reset the app if reset flag was passed // Reset the app if reset flag was passed
if (utils.queryParams.reset) { if (resetApp) {
return Promise.all( return Promise.all(
Object.keys(store.getters['data/workspaces']) Object.keys(store.getters['data/workspaces'])
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)), .map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
@ -115,7 +116,7 @@ const localDbSvc = {
localStorage.removeItem(`data/${id}`); localStorage.removeItem(`data/${id}`);
})) }))
.then(() => { .then(() => {
location.replace(utils.resolveUrl('app')); location.reload();
throw new Error('reload'); throw new Error('reload');
}); });
} }
@ -177,33 +178,29 @@ const localDbSvc = {
// Sync local DB periodically // Sync local DB periodically
utils.setInterval(() => localDbSvc.sync(), 1000); utils.setInterval(() => localDbSvc.sync(), 1000);
const ifNoId = cb => (obj) => {
if (obj.id) {
return obj;
}
return cb();
};
// watch current file changing // watch current file changing
store.watch( store.watch(
() => store.getters['file/current'].id, () => store.getters['file/current'].id,
() => Promise.resolve(store.getters['file/current']) () => {
// See if currentFile is real, ie it has an ID
const currentFile = store.getters['file/current'];
// If current file has no ID, get the most recent file // If current file has no ID, get the most recent file
.then(ifNoId(() => store.getters['file/lastOpened'])) if (!currentFile.id) {
// If still no ID, create a new file const recentFile = store.getters['file/lastOpened'];
.then(ifNoId(() => store.dispatch('createFile', { // Set it as the current file
name: 'Welcome file', if (recentFile.id) {
text: welcomeFile, store.commit('file/setCurrentId', recentFile.id);
}))) } else {
.then((currentFile) => { // If still no ID, create a new file
// Fix current file ID store.dispatch('createFile', {
if (store.getters['file/current'].id !== currentFile.id) { name: 'Welcome file',
store.commit('file/setCurrentId', currentFile.id); text: welcomeFile,
// Wait for the next watch tick })
return null; // Set it as the current file
.then(newFile => store.commit('file/setCurrentId', newFile.id));
} }
} else {
return Promise.resolve() Promise.resolve()
// Load contentState from DB // Load contentState from DB
.then(() => localDbSvc.loadContentState(currentFile.id)) .then(() => localDbSvc.loadContentState(currentFile.id))
// Load syncedContent from DB // Load syncedContent from DB
@ -226,13 +223,13 @@ const localDbSvc = {
store.commit('file/setCurrentId', lastOpenedFile.id); store.commit('file/setCurrentId', lastOpenedFile.id);
throw err; throw err;
}, },
); )
}) .catch((err) => {
.catch((err) => { console.error(err); // eslint-disable-line no-console
console.error(err); // eslint-disable-line no-console store.dispatch('notification/error', err);
store.dispatch('notification/error', err); });
}), }
{ }, {
immediate: true, immediate: true,
}); });
}); });

View File

@ -99,11 +99,14 @@ function enterKeyHandler(evt, state) {
} }
evt.preventDefault(); evt.preventDefault();
const lf = state.before.lastIndexOf('\n') + 1;
const previousLine = state.before.slice(lf); // Get the last line before the selection
const indentMatch = previousLine.match(indentRegexp) || ['']; const lastLf = state.before.lastIndexOf('\n') + 1;
const lastLine = state.before.slice(lastLf);
// See if the line is indented
const indentMatch = lastLine.match(indentRegexp) || [''];
if (clearNewline && !state.selection && state.before.length === lastSelection) { if (clearNewline && !state.selection && state.before.length === lastSelection) {
state.before = state.before.substring(0, lf); state.before = state.before.substring(0, lastLf);
state.selection = ''; state.selection = '';
clearNewline = false; clearNewline = false;
fixNumberedList(state, indentMatch[1]); fixNumberedList(state, indentMatch[1]);
@ -135,13 +138,14 @@ function tabKeyHandler(evt, state) {
evt.preventDefault(); evt.preventDefault();
const isInverse = evt.shiftKey; const isInverse = evt.shiftKey;
const lf = state.before.lastIndexOf('\n') + 1; const lastLf = state.before.lastIndexOf('\n') + 1;
const previousLine = state.before.slice(lf) + state.selection + state.after; const lastLine = state.before.slice(lastLf);
const indentMatch = previousLine.match(indentRegexp); const currentLine = lastLine + state.selection + state.after;
const indentMatch = currentLine.match(indentRegexp);
if (isInverse) { if (isInverse) {
const previousChar = state.before.slice(-1); const previousChar = state.before.slice(-1);
if (/\s/.test(state.before.charAt(lf))) { if (/\s/.test(state.before.charAt(lastLf))) {
state.before = strSplice(state.before, lf, 1); state.before = strSplice(state.before, lastLf, 1);
if (indentMatch) { if (indentMatch) {
fixNumberedList(state, indentMatch[1]); fixNumberedList(state, indentMatch[1]);
if (indentMatch[1]) { if (indentMatch[1]) {
@ -154,8 +158,13 @@ function tabKeyHandler(evt, state) {
if (previousChar) { if (previousChar) {
state.selection = state.selection.slice(1); state.selection = state.selection.slice(1);
} }
} else if (state.selection || indentMatch) { } else if (
state.before = strSplice(state.before, lf, 0, '\t'); // If selection is not empty
state.selection
// Or we are in an indented paragraph and the cursor is over the indentation characters
|| (indentMatch && indentMatch[0].length >= lastLine.length)
) {
state.before = strSplice(state.before, lastLf, 0, '\t');
state.selection = state.selection.replace(/\n(?=.)/g, '\n\t'); state.selection = state.selection.replace(/\n(?=.)/g, '\n\t');
if (indentMatch) { if (indentMatch) {
fixNumberedList(state, indentMatch[1]); fixNumberedList(state, indentMatch[1]);

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 utils from '../utils';
export default providerRegistry.register({ export default providerRegistry.register({
id: 'googleDriveAppData', id: 'googleDriveAppData',
@ -8,8 +9,14 @@ export default providerRegistry.register({
return store.getters['workspace/syncToken']; return store.getters['workspace/syncToken'];
}, },
initWorkspace() { initWorkspace() {
// Nothing to do since the main workspace isn't necessarily synchronized // Nothing much to do since the main workspace isn't necessarily synchronized
return Promise.resolve(store.getters['data/workspaces'].main); return Promise.resolve()
.then(() => {
// Remove the URL hash
utils.setQueryParams();
// Return the main workspace
return store.getters['data/workspaces'].main;
});
}, },
getChanges() { getChanges() {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];

View File

@ -17,6 +17,94 @@ export default providerRegistry.register({
const token = this.getToken(location); const token = this.getToken(location);
return `${location.driveFileId}${token.name}`; return `${location.driveFileId}${token.name}`;
}, },
initAction() {
const state = googleHelper.driveState || {};
return state.userId && Promise.resolve()
.then(() => {
// Try to find the token corresponding to the user ID
const token = store.getters['data/googleTokens'][state.userId];
// If not found or not enough permission, popup an OAuth2 window
return token && token.isDrive ? token : store.dispatch('modal/open', {
type: 'googleDriveAccount',
onResolve: () => googleHelper.addDriveAccount(
!store.getters['data/localSettings'].googleDriveRestrictedAccess,
state.userId,
),
});
})
.then((token) => {
switch (state.action) {
case 'create':
default:
// See if folder is part of a workspace we can open
return googleHelper.getFile(token, state.folderId)
.then((folder) => {
folder.appProperties = folder.appProperties || {};
googleHelper.driveActionFolder = folder;
if (folder.appProperties.folderId) {
// Change current URL to workspace URL
utils.setQueryParams({
providerId: 'googleDriveWorkspace',
folderId: folder.appProperties.folderId,
sub: state.userId,
});
}
}, (err) => {
if (!err || err.status !== 404) {
throw err;
}
// We received a 404 error meaning we have no permission to read the folder
googleHelper.driveActionFolder = { id: state.folderId };
});
case 'open': {
const getOneFile = (ids) => {
const id = ids.shift();
return id && googleHelper.getFile(token, id)
.then((file) => {
file.appProperties = file.appProperties || {};
googleHelper.driveActionFiles.push(file);
return getOneFile(ids);
});
};
return getOneFile(state.ids || [])
.then(() => {
// Check if first file is part of a workspace
const firstFile = googleHelper.driveActionFiles[0];
if (firstFile && firstFile.appProperties && firstFile.appProperties.folderId) {
// Change current URL to workspace URL
utils.setQueryParams({
providerId: 'googleDriveWorkspace',
folderId: firstFile.appProperties.folderId,
sub: state.userId,
});
}
});
}
}
});
},
performAction() {
const state = googleHelper.driveState || {};
const token = store.getters['data/googleTokens'][state.userId];
return token && Promise.resolve()
.then(() => {
switch (state.action) {
case 'create':
default:
return store.dispatch('createFile')
.then((file) => {
store.commit('file/setCurrentId', file.id);
// Return a new syncLocation
return this.makeLocation(token, null, googleHelper.driveActionFolder.id);
});
case 'open':
return store.dispatch('queue/enqueue',
() => this.openFiles(token, googleHelper.driveActionFiles));
}
});
},
downloadContent(token, syncLocation) { downloadContent(token, syncLocation) {
return googleHelper.downloadFile(token, syncLocation.driveFileId) return googleHelper.downloadFile(token, syncLocation.driveFileId)
.then(content => providerUtils.parseContent(content, syncLocation)); .then(content => providerUtils.parseContent(content, syncLocation));
@ -60,7 +148,7 @@ export default providerRegistry.register({
}, },
openFiles(token, driveFiles) { openFiles(token, driveFiles) {
const openOneFile = () => { const openOneFile = () => {
const driveFile = driveFiles.pop(); const driveFile = driveFiles.shift();
if (!driveFile) { if (!driveFile) {
return null; return null;
} }

View File

@ -4,18 +4,24 @@ import providerRegistry from './providerRegistry';
import providerUtils from './providerUtils'; import providerUtils from './providerUtils';
import utils from '../utils'; import utils from '../utils';
let fileIdToOpen;
export default providerRegistry.register({ export default providerRegistry.register({
id: 'googleDriveWorkspace', id: 'googleDriveWorkspace',
getToken() { getToken() {
return store.getters['workspace/syncToken']; return store.getters['workspace/syncToken'];
}, },
initWorkspace() { initWorkspace() {
const makeWorkspaceId = folderId => folderId && Math.abs(utils.hash(utils.serializeObject({ const makeWorkspaceIdParams = folderId => ({
providerId: this.id, providerId: this.id,
folderId, folderId,
}))).toString(36); });
const getWorkspace = folderId => store.getters['data/workspaces'][makeWorkspaceId(folderId)]; const makeWorkspaceId = folderId => folderId && utils.makeWorkspaceId(
makeWorkspaceIdParams(folderId));
const getWorkspace = folderId =>
store.getters['data/sanitizedWorkspaces'][makeWorkspaceId(folderId)];
const initFolder = (token, folder) => Promise.resolve({ const initFolder = (token, folder) => Promise.resolve({
folderId: folder.id, folderId: folder.id,
@ -78,12 +84,6 @@ export default providerRegistry.register({
.then(() => properties); .then(() => properties);
}) })
.then((properties) => { .then((properties) => {
// Fix the current url hash
const hash = `#providerId=${this.id}&folderId=${folder.id}`;
if (location.hash !== hash) {
location.hash = hash;
}
// Update workspace in the store // Update workspace in the store
const workspaceId = makeWorkspaceId(folder.id); const workspaceId = makeWorkspaceId(folder.id);
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspaces', {
@ -92,7 +92,7 @@ export default providerRegistry.register({
sub: token.sub, sub: token.sub,
name: folder.name, name: folder.name,
providerId: this.id, providerId: this.id,
url: utils.resolveUrl(hash), url: location.href,
folderId: folder.id, folderId: folder.id,
dataFolderId: properties.dataFolderId, dataFolderId: properties.dataFolderId,
trashFolderId: properties.trashFolderId, trashFolderId: properties.trashFolderId,
@ -103,9 +103,9 @@ export default providerRegistry.register({
return getWorkspace(folder.id); return getWorkspace(folder.id);
}); });
const workspace = getWorkspace(utils.queryParams.folderId);
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
const workspace = getWorkspace(utils.queryParams.folderId);
// See if we already have a token // See if we already have a token
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
@ -115,7 +115,7 @@ export default providerRegistry.register({
} }
// 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(true), onResolve: () => googleHelper.addDriveAccount(true, utils.queryParams.sub),
}); });
}) })
.then(token => Promise.resolve() .then(token => Promise.resolve()
@ -139,12 +139,70 @@ export default providerRegistry.register({
folder.appProperties = folder.appProperties || {}; folder.appProperties = folder.appProperties || {};
const folderIdProperty = folder.appProperties.folderId; const folderIdProperty = folder.appProperties.folderId;
if (folderIdProperty && folderIdProperty !== folderId) { if (folderIdProperty && folderIdProperty !== folderId) {
throw new Error(`Google Drive folder ${folderId} is part of another workspace.`); throw new Error(`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 you have the right permissions.`); throw new Error(`Folder ${folderId} is not accessible. Make sure you have the right permissions.`);
}))); }))
.then((workspace) => {
// Fix the URL hash
utils.setQueryParams(makeWorkspaceIdParams(workspace.folderId));
return workspace;
}));
},
performAction() {
return Promise.resolve()
.then(() => {
const state = googleHelper.driveState || {};
const token = this.getToken();
switch (token && state.action) {
case 'create':
return Promise.resolve()
.then(() => {
const driveFolder = googleHelper.driveActionFolder;
let syncData = store.getters['data/syncData'][driveFolder.id];
if (!syncData && driveFolder.appProperties.id) {
// Create folder if not already synced
store.commit('folder/setItem', {
id: driveFolder.appProperties.id,
name: driveFolder.name,
});
const item = store.state.folder.itemMap[driveFolder.appProperties.id];
syncData = {
id: driveFolder.id,
itemId: item.id,
type: item.type,
hash: item.hash,
};
store.dispatch('data/patchSyncData', {
[syncData.id]: syncData,
});
}
return store.dispatch('createFile', {
parentId: syncData && syncData.itemId,
})
.then((file) => {
store.commit('file/setCurrentId', file.id);
// File will be created on next workspace sync
});
});
case 'open':
return Promise.resolve()
.then(() => {
// open first file only
const firstFile = googleHelper.driveActionFiles[0];
const syncData = store.getters['data/syncData'][firstFile.id];
if (!syncData) {
fileIdToOpen = firstFile.id;
} else {
store.commit('file/setCurrentId', syncData.itemId);
}
});
default:
return null;
}
});
}, },
getChanges() { getChanges() {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
@ -178,8 +236,8 @@ export default providerRegistry.register({
let contentChange; let contentChange;
if (change.file) { if (change.file) {
// Ignore changes in files that are not in the workspace // Ignore changes in files that are not in the workspace
const properties = change.file.appProperties; const appProperties = change.file.appProperties;
if (!properties || properties.folderId !== workspace.folderId if (!appProperties || appProperties.folderId !== workspace.folderId
) { ) {
return; return;
} }
@ -198,7 +256,7 @@ export default providerRegistry.register({
? 'folder' ? 'folder'
: 'file'; : 'file';
const item = { const item = {
id: properties.id, id: appProperties.id,
type, type,
name: change.file.name, name: change.file.name,
parentId: null, parentId: null,
@ -222,14 +280,14 @@ export default providerRegistry.register({
// create a fake change as a file content change // create a fake change as a file content change
contentChange = { contentChange = {
item: { item: {
id: `${properties.id}/content`, id: `${appProperties.id}/content`,
type: 'content', type: 'content',
// Need a truthy value to force saving sync data // Need a truthy value to force saving sync data
hash: 1, hash: 1,
}, },
syncData: { syncData: {
id: `${change.fileId}/content`, id: `${change.fileId}/content`,
itemId: `${properties.id}/content`, itemId: `${appProperties.id}/content`,
type: 'content', type: 'content',
// Need a truthy value to force downloading the content // Need a truthy value to force downloading the content
hash: 1, hash: 1,
@ -341,6 +399,14 @@ export default providerRegistry.register({
}, },
}); });
} }
// Open the file requested by action if it was to synced yet
if (fileIdToOpen && fileIdToOpen === syncData.id) {
fileIdToOpen = null;
// Open the file once downloaded content has been stored
setTimeout(() => {
store.commit('file/setCurrentId', syncData.itemId);
}, 10);
}
return item; return item;
}); });
}, },

View File

@ -2,10 +2,7 @@ import utils from '../../utils';
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
let clientId = 'cbf0cf25cfd026be23e1'; const clientId = GITHUB_CLIENT_ID;
if (utils.origin === 'https://stackedit.io') {
clientId = '30c1491057c9ad4dbd56';
}
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist']; const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
const request = (token, options) => networkSvc.request({ const request = (token, options) => networkSvc.request({

View File

@ -2,7 +2,7 @@ import utils from '../../utils';
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
const clientId = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com'; const clientId = GOOGLE_CLIENT_ID;
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk'; const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
const appsDomain = null; const appsDomain = null;
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h) const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
@ -29,8 +29,20 @@ const checkIdToken = (idToken) => {
} }
}; };
let driveState;
if (utils.queryParams.providerId === 'googleDrive') {
try {
driveState = JSON.parse(utils.queryParams.state);
} catch (e) {
// Ignore
}
}
export default { export default {
folderMimeType: 'application/vnd.google-apps.folder', folderMimeType: 'application/vnd.google-apps.folder',
driveState,
driveActionFolder: null,
driveActionFiles: [],
request(token, options) { request(token, options) {
return networkSvc.request({ return networkSvc.request({
...options, ...options,
@ -336,8 +348,8 @@ export default {
signin() { signin() {
return this.startOauth2(driveAppDataScopes); return this.startOauth2(driveAppDataScopes);
}, },
addDriveAccount(fullAccess = false) { addDriveAccount(fullAccess = false, sub = null) {
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess })); return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
}, },
addBloggerAccount() { addBloggerAccount() {
return this.startOauth2(bloggerScopes); return this.startOauth2(bloggerScopes);

View File

@ -11,7 +11,8 @@ const inactivityThreshold = 3 * 1000; // 3 sec
const restartSyncAfter = 30 * 1000; // 30 sec const restartSyncAfter = 30 * 1000; // 30 sec
const minAutoSyncEvery = 60 * 1000; // 60 sec const minAutoSyncEvery = 60 * 1000; // 60 sec
let syncProvider; let actionProvider;
let workspaceProvider;
/** /**
* Use a lock in the local storage to prevent multiple windows concurrency. * Use a lock in the local storage to prevent multiple windows concurrency.
@ -221,7 +222,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
...store.getters['syncLocation/groupedByFileId'][fileId] || [], ...store.getters['syncLocation/groupedByFileId'][fileId] || [],
]; ];
if (isWorkspaceSyncPossible()) { if (isWorkspaceSyncPossible()) {
syncLocations.unshift({ id: 'main', providerId: syncProvider.id, fileId }); syncLocations.unshift({ id: 'main', providerId: workspaceProvider.id, fileId });
} }
let result; let result;
syncLocations.some((syncLocation) => { syncLocations.some((syncLocation) => {
@ -355,7 +356,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
} }
// If content was just created, restart sync to create the file as well // If content was just created, restart sync to create the file as well
if (provider === syncProvider && if (provider === workspaceProvider &&
!store.getters['data/syncDataByItemId'][fileId] !store.getters['data/syncDataByItemId'][fileId]
) { ) {
syncContext.restart = true; syncContext.restart = true;
@ -409,7 +410,7 @@ function syncDataItem(dataId) {
return null; return null;
} }
return syncProvider.downloadData(dataId) return workspaceProvider.downloadData(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 = (() => {
@ -455,7 +456,7 @@ function syncDataItem(dataId) {
if (serverItem && serverItem.hash === mergedItem.hash) { if (serverItem && serverItem.hash === mergedItem.hash) {
return null; return null;
} }
return syncProvider.uploadData(mergedItem, dataId); return workspaceProvider.uploadData(mergedItem, dataId);
}) })
.then(() => { .then(() => {
store.dispatch('data/patchDataSyncData', { store.dispatch('data/patchDataSyncData', {
@ -485,11 +486,11 @@ function syncWorkspace() {
throw new Error('Synchronization failed due to token inconsistency.'); throw new Error('Synchronization failed due to token inconsistency.');
} }
}) })
.then(() => syncProvider.getChanges()) .then(() => workspaceProvider.getChanges())
.then((changes) => { .then((changes) => {
// Apply changes // Apply changes
applyChanges(changes); applyChanges(changes);
syncProvider.setAppliedChanges(changes); workspaceProvider.setAppliedChanges(changes);
// Prevent from sending items too long after changes have been retrieved // Prevent from sending items too long after changes have been retrieved
const syncStartTime = Date.now(); const syncStartTime = Date.now();
@ -519,7 +520,7 @@ function syncWorkspace() {
// Add file if content has been added // Add file if content has been added
&& (item.type !== 'file' || syncDataByItemId[`${id}/content`]) && (item.type !== 'file' || syncDataByItemId[`${id}/content`])
) { ) {
promise = syncProvider.saveSimpleItem( promise = workspaceProvider.saveSimpleItem(
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
utils.deepCopy(item), utils.deepCopy(item),
utils.deepCopy(existingSyncData), utils.deepCopy(existingSyncData),
@ -555,7 +556,7 @@ function syncWorkspace() {
) { ) {
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
const syncDataToRemove = utils.deepCopy(existingSyncData); const syncDataToRemove = utils.deepCopy(existingSyncData);
promise = syncProvider promise = workspaceProvider
.removeItem(syncDataToRemove, ifNotTooLate) .removeItem(syncDataToRemove, ifNotTooLate)
.then(() => { .then(() => {
const syncDataCopy = { ...store.getters['data/syncData'] }; const syncDataCopy = { ...store.getters['data/syncData'] };
@ -707,25 +708,38 @@ function requestSync() {
export default { export default {
init() { init() {
// Load workspaces and tokens from localStorage return Promise.resolve()
localDbSvc.syncLocalStorage(); .then(() => {
// Load workspaces and tokens from localStorage
localDbSvc.syncLocalStorage();
// Try to find a suitable workspace sync provider // Try to find a suitable action provider
syncProvider = providerRegistry.providers[utils.queryParams.providerId]; actionProvider = providerRegistry.providers[utils.queryParams.providerId];
if (!syncProvider || !syncProvider.initWorkspace) { return actionProvider && actionProvider.initAction && actionProvider.initAction();
syncProvider = googleDriveAppDataProvider; })
} .then(() => {
// Try to find a suitable workspace sync provider
return syncProvider.initWorkspace() workspaceProvider = providerRegistry.providers[utils.queryParams.providerId];
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
workspaceProvider = googleDriveAppDataProvider;
}
return workspaceProvider.initWorkspace();
})
.then(workspace => store.dispatch('workspace/setCurrentWorkspaceId', workspace.id)) .then(workspace => store.dispatch('workspace/setCurrentWorkspaceId', workspace.id))
.then(() => localDbSvc.init()) .then(() => localDbSvc.init())
.then(() => {
// Try to find a suitable action provider
actionProvider = providerRegistry.providers[utils.queryParams.providerId] || actionProvider;
return actionProvider && actionProvider.performAction && actionProvider.performAction()
.then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation));
})
.then(() => { .then(() => {
// Sync periodically // Sync periodically
utils.setInterval(() => { utils.setInterval(() => {
if (isSyncPossible() && if (isSyncPossible()
networkSvc.isUserActive() && && networkSvc.isUserActive()
isSyncWindow() && && isSyncWindow()
isAutoSyncReady() && isAutoSyncReady()
) { ) {
requestSync(); requestSync();
} }

View File

@ -24,13 +24,22 @@ const parseQueryParams = (params) => {
}; };
// For utils.addQueryParams() // For utils.addQueryParams()
const urlParser = window.document.createElement('a'); const urlParser = document.createElement('a');
export default { export default {
origin,
queryParams: parseQueryParams(location.hash.slice(1)),
oauth2RedirectUri: `${origin}/oauth2/callback`,
cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days
origin,
oauth2RedirectUri: `${origin}/oauth2/callback`,
queryParams: parseQueryParams(location.hash.slice(1)),
setQueryParams(params = {}) {
this.queryParams = params;
const serializedParams = Object.entries(this.queryParams).map(([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
const hash = serializedParams && `#${serializedParams}`;
if (location.hash !== hash) {
location.hash = hash;
}
},
types: [ types: [
'contentState', 'contentState',
'syncedContent', 'syncedContent',
@ -96,6 +105,9 @@ export default {
})), })),
}; };
}, },
makeWorkspaceId(params) {
return Math.abs(this.hash(this.serializeObject(params))).toString(36);
},
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

@ -117,11 +117,11 @@ export default {
}, },
}, },
getters: { getters: {
workspaces: (state, getters, rootState, rootGetters) => { workspaces: getter('workspaces'),
const workspaces = (state.lsItemMap.workspaces || {}).data || empty('workspaces').data; sanitizedWorkspaces: (state, getters, rootState, rootGetters) => {
const sanitizedWorkspaces = {}; const sanitizedWorkspaces = {};
const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken']; const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken'];
Object.entries(workspaces).forEach(([id, workspace]) => { Object.entries(getters.workspaces).forEach(([id, workspace]) => {
const sanitizedWorkspace = { const sanitizedWorkspace = {
id, id,
providerId: mainWorkspaceToken && 'googleDriveAppData', providerId: mainWorkspaceToken && 'googleDriveAppData',

View File

@ -89,7 +89,7 @@ const store = new Vuex.Store({
} }
return Promise.resolve(); return Promise.resolve();
}, },
createFile({ state, getters, commit }, desc) { createFile({ state, getters, commit }, desc = {}) {
const id = utils.uid(); const id = utils.uid();
commit('content/setItem', { commit('content/setItem', {
id: `${id}/content`, id: `${id}/content`,

View File

@ -23,6 +23,7 @@ export default {
config.resolve = (result) => { config.resolve = (result) => {
clean(); clean();
if (config.onResolve) { if (config.onResolve) {
// Call onResolve immediately (mostly to prevent browsers from blocking popup windows)
config.onResolve(result) config.onResolve(result)
.then(res => resolve(res)); .then(res => resolve(res));
} else { } else {
@ -92,8 +93,8 @@ export default {
onResolve, onResolve,
}), }),
workspaceGoogleRedirection: ({ dispatch }, { onResolve }) => dispatch('open', { workspaceGoogleRedirection: ({ dispatch }, { onResolve }) => dispatch('open', {
content: '<p>You have to sign in with Google to access this workspace.</p>', content: '<p>StackEdit needs full Google Drive access to open this workspace.</p>',
resolveText: 'Ok, sign in', resolveText: 'Ok, grant',
rejectText: 'Cancel', rejectText: 'Cancel',
onResolve, onResolve,
}), }),
@ -107,14 +108,14 @@ export default {
}), }),
signInForComment: ({ dispatch }, { onResolve }) => dispatch('open', { signInForComment: ({ dispatch }, { onResolve }) => dispatch('open', {
content: `<p>You have to sign in with Google to start commenting.</p> content: `<p>You have to sign in with Google to start commenting.</p>
<div class="modal__info"><b>Note:</b> This will sync 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,
}), }),
signInForHistory: ({ dispatch }, { onResolve }) => dispatch('open', { signInForHistory: ({ dispatch }, { onResolve }) => dispatch('open', {
content: `<p>You have to sign in with Google to enable revision history.</p> content: `<p>You have to sign in with Google to enable revision history.</p>
<div class="modal__info"><b>Note:</b> This will sync 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,

View File

@ -14,11 +14,11 @@ export default {
}, },
getters: { getters: {
mainWorkspace: (state, getters, rootState, rootGetters) => { mainWorkspace: (state, getters, rootState, rootGetters) => {
const workspaces = rootGetters['data/workspaces']; const workspaces = rootGetters['data/sanitizedWorkspaces'];
return workspaces.main; return workspaces.main;
}, },
currentWorkspace: (state, getters, rootState, rootGetters) => { currentWorkspace: (state, getters, rootState, rootGetters) => {
const workspaces = rootGetters['data/workspaces']; const workspaces = rootGetters['data/sanitizedWorkspaces'];
return workspaces[state.currentWorkspaceId] || getters.mainWorkspace; return workspaces[state.currentWorkspaceId] || getters.mainWorkspace;
}, },
lastSyncActivityKey: (state, getters) => `${getters.currentWorkspace.id}/lastSyncActivity`, lastSyncActivityKey: (state, getters) => `${getters.currentWorkspace.id}/lastSyncActivity`,