Support for google drive actions
This commit is contained in:
parent
2f4781bc5d
commit
d0ea2f7850
@ -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': {
|
||||||
|
@ -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')
|
||||||
|
@ -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(),
|
||||||
|
@ -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: {
|
||||||
|
@ -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"'
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"'
|
||||||
}
|
}
|
||||||
|
9
index.js
9
index.js
@ -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');
|
||||||
|
@ -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') {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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');
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -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));
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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]);
|
||||||
|
@ -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'];
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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({
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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}`)));
|
||||||
|
@ -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',
|
||||||
|
@ -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`,
|
||||||
|
@ -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,
|
||||||
|
@ -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`,
|
||||||
|
Loading…
Reference in New Issue
Block a user