Workspaces (part 2)
This commit is contained in:
parent
8263e14bcc
commit
abbe1804e2
@ -82,6 +82,12 @@ export default {
|
||||
networkSvc.init();
|
||||
sponsorSvc.init();
|
||||
this.ready = true;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.message !== 'reload') {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
this.$store.dispatch('notification/error', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -8,12 +8,12 @@
|
||||
<button class="side-title__button button" @click="newItem(true)" v-title="'New folder'">
|
||||
<icon-folder-plus></icon-folder-plus>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="editItem()" v-title="'Rename'">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="deleteItem()" v-title="'Remove'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="editItem()" v-title="'Rename'">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
</div>
|
||||
<button class="side-title__button button" @click="toggleExplorer(false)" v-title="'Close explorer'">
|
||||
<icon-close></icon-close>
|
||||
|
@ -141,7 +141,7 @@ export default {
|
||||
}
|
||||
|
||||
.layout__panel--navigation-bar {
|
||||
background-color: #2c2c2c;
|
||||
background-color: $navbar-bg;
|
||||
}
|
||||
|
||||
.layout__panel--status-bar {
|
||||
|
@ -92,7 +92,7 @@ export default {
|
||||
publishLocations: 'current',
|
||||
}),
|
||||
isSyncPossible() {
|
||||
return this.$store.getters['data/loginToken'] ||
|
||||
return this.$store.getters['workspace/syncToken'] ||
|
||||
this.$store.getters['syncLocation/current'].length;
|
||||
},
|
||||
showSpinner() {
|
||||
|
@ -22,7 +22,7 @@ import { mapGetters, mapActions } from 'vuex';
|
||||
import CommentList from './gutters/CommentList';
|
||||
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
|
||||
|
||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
||||
const appUri = `${location.protocol}//${location.host}`;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
22
src/components/ProviderName.vue
Normal file
22
src/components/ProviderName.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<span class="provider-name">{{name}}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userSvc from '../services/userSvc';
|
||||
|
||||
export default {
|
||||
props: ['providerId'],
|
||||
computed: {
|
||||
name() {
|
||||
switch (this.userId) {
|
||||
default:
|
||||
return 'Google Drive';
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
userSvc.getInfo(this.userId);
|
||||
},
|
||||
};
|
||||
</script>
|
@ -13,7 +13,8 @@ $link-color: #0c93e4;
|
||||
$error-color: #f31;
|
||||
$border-radius-base: 2px;
|
||||
$hr-color: rgba(128, 128, 128, 0.2);
|
||||
$navbar-color: rgba(255, 255, 255, 0.67);
|
||||
$navbar-bg: #2c2c2c;
|
||||
$navbar-color: mix($navbar-bg, #fff, 33%);
|
||||
$navbar-hover-color: #fff;
|
||||
$navbar-hover-background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
|
@ -33,7 +33,7 @@ export default {
|
||||
components: {
|
||||
UserImage,
|
||||
},
|
||||
computed: mapGetters('data', [
|
||||
computed: mapGetters('workspace', [
|
||||
'loginToken',
|
||||
]),
|
||||
methods: {
|
||||
|
@ -41,5 +41,9 @@ export default {
|
||||
font-size: 15px;
|
||||
padding-top: 10px;
|
||||
border-bottom: 2px solid;
|
||||
|
||||
.current-discussion & {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -12,12 +12,12 @@
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="exportPdf">
|
||||
<icon-download slot="icon"></icon-download>
|
||||
<div><div class="menu-entry__sponsor">sponsor</div> Export as PDF</div>
|
||||
<div><div class="menu-entry__label">sponsor</div> Export as PDF</div>
|
||||
<span>Produce a PDF from an HTML template.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="exportPandoc">
|
||||
<icon-download slot="icon"></icon-download>
|
||||
<div><div class="menu-entry__sponsor">sponsor</div> Export with Pandoc</div>
|
||||
<div><div class="menu-entry__label">sponsor</div> Export with Pandoc</div>
|
||||
<span>Convert to PDF, Word, EPUB...</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="history__spacer history__spacer--last"></div>
|
||||
<div class="history__spacer history__spacer--last" v-if="revisions.length"></div>
|
||||
<div class="flex flex--row flex--end" v-if="showMoreButton">
|
||||
<button class="history__button button" @click="showMore">More</button>
|
||||
</div>
|
||||
@ -79,7 +79,7 @@ export default {
|
||||
let revisionContentPromise = revisionContentPromises[revision.id];
|
||||
if (!revisionContentPromise) {
|
||||
revisionContentPromise = new Promise((resolve, reject) => {
|
||||
const loginToken = this.$store.getters['data/loginToken'];
|
||||
const loginToken = this.$store.getters['workspace/loginToken'];
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
this.$store.dispatch('queue/enqueue',
|
||||
() => Promise.resolve()
|
||||
@ -133,7 +133,7 @@ export default {
|
||||
this.setRevisionContent();
|
||||
cachedFileId = id;
|
||||
revisionContentPromises = {};
|
||||
const loginToken = this.$store.getters['data/loginToken'];
|
||||
const loginToken = this.$store.getters['workspace/loginToken'];
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
revisionsPromise = new Promise((resolve, reject) => {
|
||||
this.$store.dispatch('queue/enqueue',
|
||||
|
@ -1,15 +1,31 @@
|
||||
<template>
|
||||
<div class="side-bar__panel side-bar__panel--menu">
|
||||
<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 all your files, folders and settings.</span>
|
||||
</menu-entry>
|
||||
<div v-else class="menu-entry flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon menu-entry__icon--image">
|
||||
<user-image :user-id="loginToken.sub"></user-image>
|
||||
<div v-if="!loginToken">
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon menu-entry__icon--disabled">
|
||||
<icon-sync-off></icon-sync-off>
|
||||
</div>
|
||||
<span><b>{{currentWorkspace.name}}</b> not synced.</span>
|
||||
</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 v-else>
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon menu-entry__icon--image">
|
||||
<user-image :user-id="loginToken.sub"></user-image>
|
||||
</div>
|
||||
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
||||
</div>
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon menu-entry__icon--image">
|
||||
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
|
||||
</div>
|
||||
<span><b>{{currentWorkspace.name}}</b> synced.</span>
|
||||
</div>
|
||||
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
||||
</div>
|
||||
<menu-entry @click.native="setPanel('workspaces')">
|
||||
<icon-database slot="icon"></icon-database>
|
||||
@ -46,9 +62,10 @@
|
||||
<icon-help-circle slot="icon"></icon-help-circle>
|
||||
Markdown cheat sheet
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="print">
|
||||
<icon-printer slot="icon"></icon-printer>
|
||||
Print
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('export')">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Export to disk
|
||||
</menu-entry>
|
||||
<input class="hidden-file" id="import-disk-file-input" type="file" @change="onImportFile">
|
||||
<label class="menu-entry button flex flex--row flex--align-center" for="import-disk-file-input">
|
||||
@ -59,9 +76,9 @@
|
||||
Import from disk
|
||||
</div>
|
||||
</label>
|
||||
<menu-entry @click.native="setPanel('export')">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Export to disk
|
||||
<menu-entry @click.native="print">
|
||||
<icon-printer slot="icon"></icon-printer>
|
||||
Print
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('more')">
|
||||
@ -84,7 +101,8 @@ export default {
|
||||
UserImage,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('data', [
|
||||
...mapGetters('workspace', [
|
||||
'currentWorkspace',
|
||||
'loginToken',
|
||||
]),
|
||||
},
|
||||
@ -123,8 +141,7 @@ export default {
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
history() {
|
||||
const loginToken = this.$store.getters['data/loginToken'];
|
||||
if (!loginToken) {
|
||||
if (!this.loginToken) {
|
||||
this.$store.dispatch('modal/signInForHistory', {
|
||||
onResolve: () => googleHelper.signin()
|
||||
.then(() => syncSvc.requestSync()),
|
||||
|
@ -13,22 +13,22 @@
|
||||
<menu-entry @click.native="reset">
|
||||
<icon-logout slot="icon"></icon-logout>
|
||||
<div>Reset application</div>
|
||||
<span>Sign out and clean local data.</span>
|
||||
<span>Sign out and clean all workspaces.</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="exportWorkspace">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Export workspace backup
|
||||
</menu-entry>
|
||||
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
||||
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
||||
<div class="menu-entry__icon flex flex--column flex--center">
|
||||
<icon-content-save></icon-content-save>
|
||||
</div>
|
||||
<div class="flex flex--column">
|
||||
Import backup
|
||||
Import workspace backup
|
||||
</div>
|
||||
</label>
|
||||
<menu-entry href="#exportBackup=true" target="_blank">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Export backup
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="about">
|
||||
<icon-help-circle slot="icon"></icon-help-circle>
|
||||
@ -47,8 +47,8 @@
|
||||
|
||||
<script>
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import localDbSvc from '../../services/localDbSvc';
|
||||
import backupSvc from '../../services/backupSvc';
|
||||
import utils from '../../services/utils';
|
||||
import welcomeFile from '../../data/welcomeFile.md';
|
||||
|
||||
export default {
|
||||
@ -72,6 +72,17 @@ export default {
|
||||
reader.readAsText(blob);
|
||||
}
|
||||
},
|
||||
exportWorkspace() {
|
||||
const url = utils.addQueryParams('app', {
|
||||
...utils.queryParams,
|
||||
exportWorkspace: true,
|
||||
}, true);
|
||||
const iframeElt = utils.createHiddenIframe(url);
|
||||
document.body.appendChild(iframeElt);
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframeElt);
|
||||
}, 60000);
|
||||
},
|
||||
settings() {
|
||||
return this.$store.dispatch('modal/open', 'settings')
|
||||
.then(
|
||||
@ -88,10 +99,10 @@ export default {
|
||||
},
|
||||
reset() {
|
||||
return this.$store.dispatch('modal/reset')
|
||||
.then(
|
||||
() => localDbSvc.removeDb(),
|
||||
() => {}, // Cancel
|
||||
);
|
||||
.then(() => {
|
||||
location.href = '#reset=true';
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
welcomeFile() {
|
||||
return this.$store.dispatch('createFile', {
|
||||
|
@ -106,8 +106,8 @@ export default {
|
||||
...mapState('queue', [
|
||||
'isSyncRequested',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'loginToken',
|
||||
...mapGetters('workspace', [
|
||||
'syncToken',
|
||||
]),
|
||||
...mapGetters('syncLocation', {
|
||||
syncLocations: 'current',
|
||||
@ -116,7 +116,7 @@ export default {
|
||||
return this.$store.getters['file/current'].name;
|
||||
},
|
||||
isSyncPossible() {
|
||||
return this.$store.getters['data/loginToken'] ||
|
||||
return this.syncToken ||
|
||||
this.$store.getters['syncLocation/current'].length;
|
||||
},
|
||||
googleDriveTokens() {
|
||||
|
@ -2,9 +2,8 @@
|
||||
<div class="side-bar__panel side-bar__panel--menu">
|
||||
<div class="workspace" v-for="(workspace, id) in workspaces" :key="id">
|
||||
<menu-entry :href="workspace.url" target="_blank">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<div class="workspace__name">{{workspace.name}}</div>
|
||||
<span>{{workspace.url}}</span>
|
||||
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
||||
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
@ -32,6 +31,9 @@ export default {
|
||||
...mapGetters('data', [
|
||||
'workspaces',
|
||||
]),
|
||||
...mapGetters('workspace', [
|
||||
'currentWorkspace',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
addGoogleDriveWorkspace() {
|
||||
@ -50,8 +52,16 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../common/variables.scss';
|
||||
|
||||
.workspace .menu-entry {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.workspace__name {
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
|
||||
.menu-entry div & {
|
||||
text-decoration: none;
|
||||
|
@ -19,12 +19,13 @@
|
||||
font-size: 17px;
|
||||
line-height: 1.5;
|
||||
text-transform: none;
|
||||
white-space: normal;
|
||||
|
||||
div div {
|
||||
text-decoration: underline;
|
||||
text-decoration-skip: ink;
|
||||
|
||||
.menu-entry__sponsor {
|
||||
.menu-entry__label {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@ -33,10 +34,20 @@
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.5;
|
||||
white-space: normal;
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-entry--info {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.menu-entry__icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
@ -44,6 +55,10 @@
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.menu-entry__icon--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menu-entry__icon--image {
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
@ -54,7 +69,7 @@
|
||||
top: -999px;
|
||||
}
|
||||
|
||||
.menu-entry__sponsor {
|
||||
.menu-entry__label {
|
||||
float: right;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
@ -66,5 +81,6 @@
|
||||
|
||||
.menu-entry__text {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
@ -45,17 +45,17 @@ export default modalTemplate({
|
||||
const selectedFormat = this.selectedFormat;
|
||||
this.$store.dispatch('queue/enqueue', () => Promise.all([
|
||||
Promise.resolve().then(() => {
|
||||
const loginToken = this.$store.getters['data/loginToken'];
|
||||
return loginToken && googleHelper.refreshToken(loginToken);
|
||||
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
|
||||
return sponsorToken && googleHelper.refreshToken(sponsorToken);
|
||||
}),
|
||||
sponsorSvc.getToken(),
|
||||
])
|
||||
.then(([loginToken, token]) => networkSvc.request({
|
||||
.then(([sponsorToken, token]) => networkSvc.request({
|
||||
method: 'POST',
|
||||
url: 'pandocExport',
|
||||
params: {
|
||||
token,
|
||||
idToken: loginToken && loginToken.idToken,
|
||||
idToken: sponsorToken && sponsorToken.idToken,
|
||||
format: selectedFormat,
|
||||
options: JSON.stringify(this.$store.getters['data/computedSettings'].pandoc),
|
||||
metadata: JSON.stringify(currentContent.properties),
|
||||
|
@ -38,19 +38,19 @@ export default modalTemplate({
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
this.$store.dispatch('queue/enqueue', () => Promise.all([
|
||||
Promise.resolve().then(() => {
|
||||
const loginToken = this.$store.getters['data/loginToken'];
|
||||
return loginToken && googleHelper.refreshToken(loginToken);
|
||||
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
|
||||
return sponsorToken && googleHelper.refreshToken(sponsorToken);
|
||||
}),
|
||||
sponsorSvc.getToken(),
|
||||
exportSvc.applyTemplate(
|
||||
currentFile.id, this.allTemplates[this.selectedTemplate], true),
|
||||
])
|
||||
.then(([loginToken, token, html]) => networkSvc.request({
|
||||
.then(([sponsorToken, token, html]) => networkSvc.request({
|
||||
method: 'POST',
|
||||
url: 'pdfExport',
|
||||
params: {
|
||||
token,
|
||||
idToken: loginToken && loginToken.idToken,
|
||||
idToken: sponsorToken && sponsorToken.idToken,
|
||||
options: JSON.stringify(this.$store.getters['data/computedSettings'].wkhtmltopdf),
|
||||
},
|
||||
body: html,
|
||||
|
@ -25,12 +25,12 @@ export default {
|
||||
ModalInner,
|
||||
},
|
||||
data() {
|
||||
const loginToken = this.$store.getters['data/loginToken'];
|
||||
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
|
||||
const makeButton = (id, price, description, offer) => {
|
||||
const params = {
|
||||
cmd: '_s-xclick',
|
||||
hosted_button_id: id,
|
||||
custom: loginToken.sub,
|
||||
custom: sponsorToken.sub,
|
||||
};
|
||||
return {
|
||||
id,
|
||||
@ -42,7 +42,7 @@ export default {
|
||||
};
|
||||
|
||||
return {
|
||||
buttons: loginToken ? [
|
||||
buttons: sponsorToken ? [
|
||||
makeButton('TDAPH47B3J2JW', '$5', '3 months sponsorship'),
|
||||
makeButton('6CTKPKF8868UA', '$15', '1 year sponsorship', '-25%'),
|
||||
makeButton('A5ZDYW6SYDLBE', '$25', '2 years sponsorship', '-37%'),
|
||||
|
@ -5,18 +5,20 @@
|
||||
<div class="workspace-entry__icon flex flex--column flex--center">
|
||||
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
||||
</div>
|
||||
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc.stop="submitEdit(true)" v-model="editingName">
|
||||
<div class="workspace-entry__name" v-else>
|
||||
{{workspace.name}}
|
||||
<div class="workspace-entry__description flex flex--column">
|
||||
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc.stop="submitEdit(true)" v-model="editingName">
|
||||
<div class="workspace-entry__name" v-else>
|
||||
{{workspace.name}}
|
||||
</div>
|
||||
<div class="workspace-entry__url">
|
||||
{{workspace.url}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-entry__buttons flex flex--row flex--center">
|
||||
<button class="workspace-entry__button button" @click="edit(id)">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
<a class="workspace-entry__button button" :href="workspace.url" target="_blank">
|
||||
<icon-open-in-new></icon-open-in-new>
|
||||
</a>
|
||||
<button class="workspace-entry__button button" @click="remove(id)">
|
||||
<button class="workspace-entry__button button" v-if="workspace !== currentWorkspace && workspace !== mainWorkspace" @click="remove(id)">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
@ -31,6 +33,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import localDbSvc from '../../services/localDbSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -47,6 +50,10 @@ export default {
|
||||
...mapGetters('data', [
|
||||
'workspaces',
|
||||
]),
|
||||
...mapGetters('workspace', [
|
||||
'mainWorkspace',
|
||||
'currentWorkspace',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
edit(id) {
|
||||
@ -68,11 +75,11 @@ export default {
|
||||
this.editedId = null;
|
||||
},
|
||||
remove(id) {
|
||||
const workspaces = {
|
||||
...this.workspaces,
|
||||
};
|
||||
delete workspaces[id];
|
||||
this.$store.dispatch('data/setWorkspaces', workspaces);
|
||||
return this.$store.dispatch('modal/removeWorkspace')
|
||||
.then(
|
||||
() => localDbSvc.removeWorkspace(id),
|
||||
() => {}, // Cancel
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -97,6 +104,11 @@ export default {
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-entry__icon {
|
||||
@ -106,12 +118,22 @@ export default {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.workspace-entry__name {
|
||||
.workspace-entry__description {
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workspace-entry__name {
|
||||
overflow: hidden;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.workspace-entry__url {
|
||||
opacity: 0.5;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.workspace-entry__buttons {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
@ -31,18 +31,18 @@ export default {
|
||||
methods: {
|
||||
sponsor() {
|
||||
Promise.resolve()
|
||||
.then(() => !this.$store.getters['data/loginToken'] &&
|
||||
.then(() => !this.$store.getters['workspace/sponsorToken'] &&
|
||||
// If user has to sign in
|
||||
this.$store.dispatch('modal/signInForSponsorship', {
|
||||
onResolve: () => googleHelper.signin()
|
||||
.then(() => syncSvc.requestSync()),
|
||||
})
|
||||
}))
|
||||
.then(() => {
|
||||
if (!this.$store.getters.isSponsor) {
|
||||
this.$store.dispatch('modal/open', 'sponsor');
|
||||
}
|
||||
})
|
||||
.catch(() => { }); // Cancel
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -28,11 +28,8 @@ import modalTemplate from '../common/modalTemplate';
|
||||
import utils from '../../../services/utils';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
fileId: '',
|
||||
}),
|
||||
computedLocalSettings: {
|
||||
folderId: 'googleDriveFolderId',
|
||||
folderId: 'googleDriveWorkspaceFolderId',
|
||||
},
|
||||
methods: {
|
||||
openFolder() {
|
||||
@ -41,7 +38,7 @@ export default modalTemplate({
|
||||
googleHelper.openPicker(this.config.token, 'folder')
|
||||
.then((folders) => {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveFolderId: folders[0].id,
|
||||
googleDriveWorkspaceFolderId: folders[0].id,
|
||||
});
|
||||
}));
|
||||
},
|
||||
@ -50,7 +47,7 @@ export default modalTemplate({
|
||||
providerId: 'googleDriveWorkspace',
|
||||
folderId: this.folderId,
|
||||
sub: this.config.token.sub,
|
||||
});
|
||||
}, true);
|
||||
this.config.resolve();
|
||||
window.open(url);
|
||||
},
|
||||
|
@ -4,6 +4,7 @@ export default () => ({
|
||||
pdfExportTemplate: 'styledHtml',
|
||||
pandocExportFormat: 'pdf',
|
||||
googleDriveFolderId: '',
|
||||
googleDriveWorkspaceFolderId: '',
|
||||
googleDrivePublishFormat: 'markdown',
|
||||
googleDrivePublishTemplate: 'styledHtml',
|
||||
bloggerBlogUrl: '',
|
||||
|
@ -1,3 +1,5 @@
|
||||
# Auto-sync frequency (in ms). Minimum is 60000.
|
||||
autoSyncEvery: 90000
|
||||
# Adjust font size in editor and preview
|
||||
fontSizeFactor: 1
|
||||
# Adjust maximum text width in editor and preview
|
||||
|
@ -1,5 +1,6 @@
|
||||
export default () => ({
|
||||
main: {
|
||||
name: 'Main workspace',
|
||||
// The rest will be filled by the data/workspaces getter
|
||||
},
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ export default {
|
||||
classState() {
|
||||
switch (this.providerId) {
|
||||
case 'googleDrive':
|
||||
case 'googleDriveAppData':
|
||||
case 'googleDriveWorkspace':
|
||||
return 'google-drive';
|
||||
case 'googlePhotos':
|
||||
|
@ -24,7 +24,7 @@ if (NODE_ENV === 'production') {
|
||||
.then(() => {
|
||||
localStorage.updated = true;
|
||||
// Reload the webpage to load into the new version
|
||||
window.location.reload();
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -5,24 +5,25 @@ import welcomeFile from '../data/welcomeFile.md';
|
||||
|
||||
const dbVersion = 1;
|
||||
const dbStoreName = 'objects';
|
||||
const exportBackup = utils.queryParams.exportBackup;
|
||||
if (exportBackup) {
|
||||
location.hash = '';
|
||||
}
|
||||
|
||||
const exportWorkspace = utils.queryParams.exportWorkspace;
|
||||
const deleteMarkerMaxAge = 1000;
|
||||
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||
|
||||
const getDbName = (workspaceId) => {
|
||||
let dbName = 'stackedit-db';
|
||||
if (workspaceId !== 'main') {
|
||||
dbName += `-${workspaceId}`;
|
||||
}
|
||||
return dbName;
|
||||
};
|
||||
|
||||
class Connection {
|
||||
constructor() {
|
||||
this.getTxCbs = [];
|
||||
|
||||
// Make the DB name
|
||||
const workspaceId = store.getters['workspace/currentWorkspace'].id;
|
||||
this.dbName = 'stackedit-db';
|
||||
if (workspaceId !== 'main') {
|
||||
this.dbName += `-${workspaceId}`;
|
||||
}
|
||||
this.dbName = getDbName(workspaceId);
|
||||
|
||||
// Init connection
|
||||
const request = indexedDB.open(this.dbName, dbVersion);
|
||||
@ -33,7 +34,7 @@ class Connection {
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
this.db = event.target.result;
|
||||
this.db.onversionchange = () => window.location.reload();
|
||||
this.db.onversionchange = () => location.reload();
|
||||
|
||||
this.getTxCbs.forEach(({ onTx, onError }) => this.createTx(onTx, onError));
|
||||
this.getTxCbs = null;
|
||||
@ -101,14 +102,33 @@ const localDbSvc = {
|
||||
* Create the connection and start syncing.
|
||||
*/
|
||||
init() {
|
||||
// Create the connection
|
||||
this.connection = new Connection();
|
||||
|
||||
// Load the DB
|
||||
return localDbSvc.sync()
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// If exportBackup parameter was provided
|
||||
if (exportBackup) {
|
||||
// Reset the app if reset flag was passed
|
||||
if (utils.queryParams.reset) {
|
||||
return Promise.all(
|
||||
Object.keys(store.getters['data/workspaces'])
|
||||
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
|
||||
)
|
||||
.then(() => utils.localStorageDataIds.forEach((id) => {
|
||||
// Clean data stored in localStorage
|
||||
localStorage.removeItem(`data/${id}`);
|
||||
}))
|
||||
.then(() => {
|
||||
location.replace(utils.resolveUrl('app'));
|
||||
throw new Error('reload');
|
||||
});
|
||||
}
|
||||
|
||||
// Create the connection
|
||||
this.connection = new Connection();
|
||||
|
||||
// Load the DB
|
||||
return localDbSvc.sync();
|
||||
})
|
||||
.then(() => {
|
||||
// If exportWorkspace parameter was provided
|
||||
if (exportWorkspace) {
|
||||
const backup = JSON.stringify(store.getters.allItemMap);
|
||||
const blob = new Blob([backup], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
@ -130,8 +150,8 @@ const localDbSvc = {
|
||||
}
|
||||
|
||||
// If app was last opened 7 days ago and synchronization is off
|
||||
if (!store.getters['data/loginToken'] &&
|
||||
(store.getters['workspace/lastFocus'] + utils.cleanTrashAfter < Date.now())
|
||||
if (!store.getters['workspace/syncToken'] &&
|
||||
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
|
||||
) {
|
||||
// Clean files
|
||||
store.getters['file/items']
|
||||
@ -141,14 +161,14 @@ const localDbSvc = {
|
||||
|
||||
// Enable sponsorship
|
||||
if (utils.queryParams.paymentSuccess) {
|
||||
location.hash = '';
|
||||
location.hash = ''; // PaymentSuccess param is always on its own
|
||||
store.dispatch('modal/paymentSuccess');
|
||||
const loginToken = store.getters['data/loginToken'];
|
||||
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||
// Force check sponsorship after a few seconds
|
||||
const currentDate = Date.now();
|
||||
if (loginToken && loginToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||
store.dispatch('data/setGoogleToken', {
|
||||
...loginToken,
|
||||
...sponsorToken,
|
||||
expiresOn: currentDate - checkSponsorshipAfter,
|
||||
});
|
||||
}
|
||||
@ -288,7 +308,7 @@ const localDbSvc = {
|
||||
lastTx = item.tx;
|
||||
if (this.lastTx && item.tx - this.lastTx > deleteMarkerMaxAge) {
|
||||
// We may have missed some delete markers
|
||||
window.location.reload();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -376,7 +396,7 @@ const localDbSvc = {
|
||||
// DB item is different from the corresponding store item
|
||||
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
||||
// Update content only if it exists in the store
|
||||
if (existingStoreItem || !contentTypes[dbItem.type] || exportBackup) {
|
||||
if (existingStoreItem || !contentTypes[dbItem.type] || exportWorkspace) {
|
||||
// Put item in the store
|
||||
dbItem.tx = undefined;
|
||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||
@ -438,17 +458,25 @@ const localDbSvc = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Drop the database
|
||||
* Drop the database and clean the localStorage for the specified workspaceId.
|
||||
*/
|
||||
removeDb() {
|
||||
removeWorkspace(id) {
|
||||
const workspaces = {
|
||||
...this.workspaces,
|
||||
};
|
||||
delete workspaces[id];
|
||||
store.dispatch('data/setWorkspaces', workspaces);
|
||||
this.syncLocalStorage();
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('stackedit-db');
|
||||
const dbName = getDbName(id);
|
||||
const request = indexedDB.deleteDatabase(dbName);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = resolve;
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
}, () => store.dispatch('notification/error', 'Could not delete local database.'));
|
||||
.then(() => {
|
||||
localStorage.removeItem(`${id}/lastSyncActivity`);
|
||||
localStorage.removeItem(`${id}/lastWindowFocus`);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,6 @@ const networkTimeout = 30 * 1000; // 30 sec
|
||||
let isConnectionDown = false;
|
||||
const userInactiveAfter = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
|
||||
export default {
|
||||
init() {
|
||||
// Keep track of the last user activity
|
||||
@ -82,7 +81,9 @@ export default {
|
||||
window.addEventListener('offline', checkOffline);
|
||||
},
|
||||
isWindowFocused() {
|
||||
return parseInt(localStorage.getItem(this.lastFocusKey), 10) === this.lastFocus;
|
||||
// We don't use state.workspace.lastFocus as it's not reactive
|
||||
const storedLastFocus = localStorage.getItem(store.getters['workspace/lastFocusKey']);
|
||||
return parseInt(storedLastFocus, 10) === this.lastFocus;
|
||||
},
|
||||
isUserActive() {
|
||||
return this.lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused();
|
||||
@ -113,10 +114,7 @@ export default {
|
||||
let wnd;
|
||||
if (silent) {
|
||||
// Use an iframe as wnd for silent mode
|
||||
iframeElt = document.createElement('iframe');
|
||||
iframeElt.style.position = 'absolute';
|
||||
iframeElt.style.left = '-9999px';
|
||||
iframeElt.src = authorizeUrl;
|
||||
iframeElt = utils.createHiddenIframe(authorizeUrl);
|
||||
document.body.appendChild(iframeElt);
|
||||
wnd = iframeElt.contentWindow;
|
||||
} else {
|
||||
|
@ -5,17 +5,19 @@ import providerRegistry from './providerRegistry';
|
||||
export default providerRegistry.register({
|
||||
id: 'googleDriveAppData',
|
||||
getToken() {
|
||||
return store.getters['data/loginToken'];
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
initWorkspace() {
|
||||
// Nothing to do since the main workspace isn't necessarily synchronized
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(store.getters['data/workspaces'].main);
|
||||
},
|
||||
getChanges(token) {
|
||||
return googleHelper.getChanges(token)
|
||||
const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
|
||||
return googleHelper.getChanges(token, startPageToken, 'appDataFolder')
|
||||
.then((result) => {
|
||||
const changes = result.changes.filter((change) => {
|
||||
if (change.file) {
|
||||
// Parse item from file name
|
||||
try {
|
||||
change.item = JSON.parse(change.file.name);
|
||||
} catch (e) {
|
||||
@ -30,20 +32,17 @@ export default providerRegistry.register({
|
||||
};
|
||||
change.file = undefined;
|
||||
}
|
||||
change.syncDataId = change.fileId;
|
||||
return true;
|
||||
});
|
||||
changes.nextPageToken = result.nextPageToken;
|
||||
changes.startPageToken = result.startPageToken;
|
||||
return changes;
|
||||
});
|
||||
},
|
||||
setAppliedChanges(token, changes) {
|
||||
const lastToken = store.getters['data/googleTokens'][token.sub];
|
||||
if (changes.nextPageToken !== lastToken.nextPageToken) {
|
||||
store.dispatch('data/setGoogleToken', {
|
||||
...lastToken,
|
||||
nextPageToken: changes.nextPageToken,
|
||||
});
|
||||
}
|
||||
setAppliedChanges(changes) {
|
||||
store.dispatch('data/patchLocalSettings', {
|
||||
workspaceSyncStartPageToken: changes.startPageToken,
|
||||
});
|
||||
},
|
||||
saveItem(token, item, syncData, ifNotTooLate) {
|
||||
return googleHelper.uploadAppDataFile(
|
||||
|
@ -3,20 +3,21 @@ import googleHelper from './helpers/googleHelper';
|
||||
import providerRegistry from './providerRegistry';
|
||||
import utils from '../utils';
|
||||
|
||||
let workspaceFolderId;
|
||||
|
||||
const makeWorkspaceId = () => {
|
||||
|
||||
};
|
||||
|
||||
export default providerRegistry.register({
|
||||
id: 'googleDriveWorkspace',
|
||||
getToken() {
|
||||
return store.getters['data/loginToken'];
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
initWorkspace() {
|
||||
const makeWorkspaceId = folderId => folderId && Math.abs(utils.hash(utils.serializeObject({
|
||||
providerId: this.id,
|
||||
folderId,
|
||||
}))).toString(36);
|
||||
|
||||
const getWorkspace = folderId => store.getters['data/workspaces'][makeWorkspaceId(folderId)];
|
||||
|
||||
const initFolder = (token, folder) => Promise.resolve({
|
||||
workspaceId: this.makeWorkspaceId(folder.id),
|
||||
folderId: folder.id,
|
||||
dataFolderId: folder.appProperties.dataFolderId,
|
||||
trashFolderId: folder.appProperties.trashFolderId,
|
||||
})
|
||||
@ -29,7 +30,7 @@ export default providerRegistry.register({
|
||||
token,
|
||||
'.stackedit-data',
|
||||
[folder.id],
|
||||
{ workspaceId: properties.workspaceId },
|
||||
{ folderId: folder.id },
|
||||
undefined,
|
||||
'application/vnd.google-apps.folder',
|
||||
)
|
||||
@ -47,7 +48,7 @@ export default providerRegistry.register({
|
||||
token,
|
||||
'.stackedit-trash',
|
||||
[folder.id],
|
||||
{ workspaceId: properties.workspaceId },
|
||||
{ folderId: folder.id },
|
||||
undefined,
|
||||
'application/vnd.google-apps.folder',
|
||||
)
|
||||
@ -58,7 +59,7 @@ export default providerRegistry.register({
|
||||
})
|
||||
.then((properties) => {
|
||||
// Update workspace if some properties are missing
|
||||
if (properties.workspaceId === folder.appProperties.workspaceId
|
||||
if (properties.folderId === folder.appProperties.folderId
|
||||
&& properties.dataFolderId === folder.appProperties.dataFolderId
|
||||
&& properties.trashFolderId === folder.appProperties.trashFolderId
|
||||
) {
|
||||
@ -76,26 +77,48 @@ export default providerRegistry.register({
|
||||
.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
|
||||
const workspaceId = makeWorkspaceId(folder.id);
|
||||
store.dispatch('data/patchWorkspaces', {
|
||||
[properties.workspaceId]: {
|
||||
id: properties.workspaceId,
|
||||
[workspaceId]: {
|
||||
id: workspaceId,
|
||||
sub: token.sub,
|
||||
name: folder.name,
|
||||
providerId: this.id,
|
||||
url: utils.resolveUrl(hash),
|
||||
folderId: folder.id,
|
||||
dataFolderId: properties.dataFolderId,
|
||||
trashFolderId: properties.trashFolderId,
|
||||
},
|
||||
});
|
||||
return store.getters['data/workspaces'][properties.workspaceId];
|
||||
|
||||
// Return the workspace
|
||||
return getWorkspace(folder.id);
|
||||
});
|
||||
|
||||
return Promise.resolve(store.getters['data/googleTokens'][utils.queryParams.sub])
|
||||
.then(token => token || this.$store.dispatch('modal/workspaceGoogleRedirection', {
|
||||
onResolve: () => googleHelper.addDriveAccount(),
|
||||
}))
|
||||
const workspace = getWorkspace(utils.queryParams.folderId);
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// See if we already have a token
|
||||
const googleTokens = store.getters['data/googleTokens'];
|
||||
// Token sub is in the workspace or in the url if workspace is about to be created
|
||||
const token = workspace ? googleTokens[workspace.sub] : googleTokens[utils.queryParams.sub];
|
||||
if (token && token.isDrive) {
|
||||
return token;
|
||||
}
|
||||
// If no token has been found, popup an authorize window and get one
|
||||
return store.dispatch('modal/workspaceGoogleRedirection', {
|
||||
onResolve: () => googleHelper.addDriveAccount(),
|
||||
});
|
||||
})
|
||||
.then(token => Promise.resolve()
|
||||
// If no folderId is provided, create one
|
||||
.then(() => utils.queryParams.folderId || googleHelper.uploadFile(
|
||||
token,
|
||||
'StackEdit workspace',
|
||||
@ -103,25 +126,31 @@ export default providerRegistry.register({
|
||||
undefined,
|
||||
undefined,
|
||||
'application/vnd.google-apps.folder',
|
||||
).then(folder => initFolder(token, folder).then(() => folder.id)))
|
||||
.then((folderId) => {
|
||||
const workspaceId = this.makeWorkspaceId(folderId);
|
||||
const workspace = store.getters['data/workspaces'][workspaceId];
|
||||
return workspace || googleHelper.getFile(token, folderId)
|
||||
.then((folder) => {
|
||||
const folderWorkspaceId = folder.appProperties.workspaceId;
|
||||
if (folderWorkspaceId && folderWorkspaceId !== workspaceId) {
|
||||
throw new Error(`Google Drive folder ${folderId} is part of another workspace.`);
|
||||
}
|
||||
return initFolder(token, folder);
|
||||
});
|
||||
}));
|
||||
)
|
||||
.then(folder => initFolder(token, {
|
||||
...folder,
|
||||
appProperties: {},
|
||||
})
|
||||
.then(() => folder.id)))
|
||||
// If workspace does not exist, initialize one
|
||||
.then(folderId => getWorkspace(folderId) || googleHelper.getFile(token, folderId)
|
||||
.then((folder) => {
|
||||
const folderIdProperty = folder.appProperties.folderId;
|
||||
if (folderIdProperty && folderIdProperty !== folderId) {
|
||||
throw new Error(`Google Drive folder ${folderId} is part of another workspace.`);
|
||||
}
|
||||
return initFolder(token, folder);
|
||||
}, () => {
|
||||
throw new Error(`Folder ${folderId} is not accessible. Make sure it's a valid StackEdit workspace folder and you have the right permissions.`);
|
||||
})));
|
||||
},
|
||||
getChanges(token) {
|
||||
return googleHelper.getChanges(token)
|
||||
const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
|
||||
return googleHelper.getChanges(token, startPageToken, 'appDataFolder')
|
||||
.then((result) => {
|
||||
const changes = result.changes.filter((change) => {
|
||||
if (change.file) {
|
||||
// Parse item from file name
|
||||
try {
|
||||
change.item = JSON.parse(change.file.name);
|
||||
} catch (e) {
|
||||
@ -136,121 +165,11 @@ export default providerRegistry.register({
|
||||
};
|
||||
change.file = undefined;
|
||||
}
|
||||
change.syncDataId = change.fileId;
|
||||
return true;
|
||||
});
|
||||
changes.nextPageToken = result.nextPageToken;
|
||||
changes.startPageToken = result.startPageToken;
|
||||
return changes;
|
||||
});
|
||||
},
|
||||
setAppliedChanges(token, changes) {
|
||||
const lastToken = store.getters['data/googleTokens'][token.sub];
|
||||
if (changes.nextPageToken !== lastToken.nextPageToken) {
|
||||
store.dispatch('data/setGoogleToken', {
|
||||
...lastToken,
|
||||
nextPageToken: changes.nextPageToken,
|
||||
});
|
||||
}
|
||||
},
|
||||
saveItem(token, item, syncData, ifNotTooLate) {
|
||||
return googleHelper.uploadAppDataFile(
|
||||
token,
|
||||
JSON.stringify(item),
|
||||
['appDataFolder'],
|
||||
undefined,
|
||||
undefined,
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
)
|
||||
.then(file => ({
|
||||
// Build sync data
|
||||
id: file.id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
}));
|
||||
},
|
||||
removeItem(token, syncData, ifNotTooLate) {
|
||||
return googleHelper.removeAppDataFile(token, syncData.id, ifNotTooLate)
|
||||
.then(() => syncData);
|
||||
},
|
||||
downloadContent(token, syncLocation) {
|
||||
return this.downloadData(token, `${syncLocation.fileId}/content`);
|
||||
},
|
||||
downloadData(token, dataId) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
||||
if (!syncData) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return googleHelper.downloadAppDataFile(token, syncData.id)
|
||||
.then((content) => {
|
||||
const item = JSON.parse(content);
|
||||
if (item.hash !== syncData.hash) {
|
||||
store.dispatch('data/patchSyncData', {
|
||||
[syncData.id]: {
|
||||
...syncData,
|
||||
hash: item.hash,
|
||||
},
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||
return this.uploadData(token, content, `${syncLocation.fileId}/content`, ifNotTooLate)
|
||||
.then(() => syncLocation);
|
||||
},
|
||||
uploadData(token, item, dataId, ifNotTooLate) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
||||
if (syncData && syncData.hash === item.hash) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return googleHelper.uploadAppDataFile(
|
||||
token,
|
||||
JSON.stringify({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
}),
|
||||
['appDataFolder'],
|
||||
undefined,
|
||||
JSON.stringify(item),
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
)
|
||||
.then(file => store.dispatch('data/patchSyncData', {
|
||||
[file.id]: {
|
||||
// Build sync data
|
||||
id: file.id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
}));
|
||||
},
|
||||
listRevisions(token, fileId) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
||||
if (!syncData) {
|
||||
return Promise.reject(); // No need for a proper error message.
|
||||
}
|
||||
return googleHelper.getFileRevisions(token, syncData.id)
|
||||
.then(revisions => revisions.map(revision => ({
|
||||
id: revision.id,
|
||||
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
|
||||
created: new Date(revision.modifiedTime).getTime(),
|
||||
})));
|
||||
},
|
||||
getRevisionContent(token, fileId, revisionId) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
||||
if (!syncData) {
|
||||
return Promise.reject(); // No need for a proper error message.
|
||||
}
|
||||
return googleHelper.downloadFileRevision(token, syncData.id, revisionId)
|
||||
.then(content => JSON.parse(content));
|
||||
},
|
||||
makeWorkspaceId(folderId) {
|
||||
return Math.abs(utils.hash(utils.serializeObject({
|
||||
providerId: this.id,
|
||||
folderId: folderId,
|
||||
}))).toString(36);
|
||||
},
|
||||
});
|
||||
|
@ -168,7 +168,7 @@ export default {
|
||||
expiresOn: Date.now() + (data.expiresIn * 1000),
|
||||
idToken: data.idToken,
|
||||
sub: `${res.body.sub}`,
|
||||
isLogin: !store.getters['data/loginToken'] &&
|
||||
isLogin: !store.getters['workspace/loginToken'] &&
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||
isSponsor: false,
|
||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||
@ -195,13 +195,14 @@ export default {
|
||||
// We probably retrieved a new token with restricted scopes.
|
||||
// That's no problem, token will be refreshed later with merged scopes.
|
||||
// Restore flags
|
||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||
token.isSponsor = existingToken.isSponsor;
|
||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||
token.nextPageToken = existingToken.nextPageToken;
|
||||
Object.assign(token, {
|
||||
isLogin: existingToken.isLogin || token.isLogin,
|
||||
isSponsor: existingToken.isSponsor,
|
||||
isDrive: existingToken.isDrive || token.isDrive,
|
||||
isBlogger: existingToken.isBlogger || token.isBlogger,
|
||||
isPhotos: existingToken.isPhotos || token.isPhotos,
|
||||
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
|
||||
});
|
||||
}
|
||||
return token.isLogin && networkSvc.request({
|
||||
method: 'GET',
|
||||
@ -296,7 +297,7 @@ export default {
|
||||
addPhotosAccount() {
|
||||
return this.startOauth2(photosScopes);
|
||||
},
|
||||
getChanges(token) {
|
||||
getChanges(token, startPageToken, spaces) {
|
||||
const result = {
|
||||
changes: [],
|
||||
};
|
||||
@ -307,20 +308,21 @@ export default {
|
||||
url: 'https://www.googleapis.com/drive/v3/changes',
|
||||
params: {
|
||||
pageToken,
|
||||
spaces: 'appDataFolder',
|
||||
spaces,
|
||||
pageSize: 1000,
|
||||
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file/name,file/properties)',
|
||||
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file/name,file/appProperties)',
|
||||
},
|
||||
}).then((res) => {
|
||||
result.changes = result.changes.concat(res.body.changes.filter(item => item.fileId));
|
||||
if (res.body.nextPageToken) {
|
||||
return getPage(res.body.nextPageToken);
|
||||
}
|
||||
result.nextPageToken = res.body.newStartPageToken;
|
||||
return result;
|
||||
});
|
||||
})
|
||||
.then((res) => {
|
||||
result.changes = result.changes.concat(res.body.changes.filter(item => item.fileId));
|
||||
if (res.body.nextPageToken) {
|
||||
return getPage(res.body.nextPageToken);
|
||||
}
|
||||
result.startPageToken = res.body.newStartPageToken;
|
||||
return result;
|
||||
});
|
||||
|
||||
return getPage(refreshedToken.nextPageToken);
|
||||
return getPage(startPageToken);
|
||||
});
|
||||
},
|
||||
uploadFile(token, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate) {
|
||||
@ -338,6 +340,9 @@ export default {
|
||||
.then(refreshedToken => this.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||
params: {
|
||||
fields: 'id,name,mimeType,appProperties',
|
||||
},
|
||||
})
|
||||
.then(res => res.body));
|
||||
},
|
||||
@ -369,20 +374,21 @@ export default {
|
||||
pageSize: 1000,
|
||||
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
|
||||
},
|
||||
}).then((res) => {
|
||||
res.body.revisions.forEach((revision) => {
|
||||
store.commit('userInfo/addItem', {
|
||||
id: revision.lastModifyingUser.permissionId,
|
||||
name: revision.lastModifyingUser.displayName,
|
||||
imageUrl: revision.lastModifyingUser.photoLink,
|
||||
})
|
||||
.then((res) => {
|
||||
res.body.revisions.forEach((revision) => {
|
||||
store.commit('userInfo/addItem', {
|
||||
id: revision.lastModifyingUser.permissionId,
|
||||
name: revision.lastModifyingUser.displayName,
|
||||
imageUrl: revision.lastModifyingUser.photoLink,
|
||||
});
|
||||
revisions.push(revision);
|
||||
});
|
||||
revisions.push(revision);
|
||||
if (res.body.nextPageToken) {
|
||||
return getPage(res.body.nextPageToken);
|
||||
}
|
||||
return revisions;
|
||||
});
|
||||
if (res.body.nextPageToken) {
|
||||
return getPage(res.body.nextPageToken);
|
||||
}
|
||||
return revisions;
|
||||
});
|
||||
|
||||
return getPage();
|
||||
});
|
||||
|
@ -17,8 +17,8 @@ const getMonetize = () => Promise.resolve()
|
||||
});
|
||||
|
||||
const isGoogleSponsor = () => {
|
||||
const loginToken = store.getters['data/loginToken'];
|
||||
return loginToken && loginToken.isSponsor;
|
||||
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||
return sponsorToken && sponsorToken.isSponsor;
|
||||
};
|
||||
|
||||
const checkPayment = () => {
|
||||
|
@ -5,12 +5,13 @@ import diffUtils from './diffUtils';
|
||||
import networkSvc from './networkSvc';
|
||||
import providerRegistry from './providers/providerRegistry';
|
||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||
import './providers/googleDriveWorkspaceProvider';
|
||||
|
||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||
const autoSyncAfter = utils.randomize(60 * 1000); // 60 sec
|
||||
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||
|
||||
let workspaceProvider;
|
||||
let syncProvider;
|
||||
|
||||
/**
|
||||
* Use a lock in the local storage to prevent multiple windows concurrency.
|
||||
@ -22,14 +23,7 @@ const getLastStoredSyncActivity = () =>
|
||||
/**
|
||||
* Return true if workspace sync is possible.
|
||||
*/
|
||||
const isWorkspaceSyncPossible = () => {
|
||||
const loginToken = store.getters['data/loginToken'];
|
||||
if (!loginToken && Object.keys(store.getters['data/syncData']).length) {
|
||||
// Reset sync data if token was removed
|
||||
store.dispatch('data/setSyncData', {});
|
||||
}
|
||||
return !!loginToken;
|
||||
};
|
||||
const isWorkspaceSyncPossible = () => !!store.getters['workspace/syncToken'];
|
||||
|
||||
/**
|
||||
* Return true if file has at least one explicit sync location.
|
||||
@ -55,8 +49,11 @@ function isSyncWindow() {
|
||||
* Return true if auto sync can start, ie if lastSyncActivity is old enough.
|
||||
*/
|
||||
function isAutoSyncReady() {
|
||||
const storedLastSyncActivity = getLastStoredSyncActivity();
|
||||
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
||||
let autoSyncEvery = store.getters['data/computedSettings'].autoSyncEvery;
|
||||
if (autoSyncEvery < minAutoSyncEvery) {
|
||||
autoSyncEvery = minAutoSyncEvery;
|
||||
}
|
||||
return Date.now() > autoSyncEvery + getLastStoredSyncActivity();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,7 +88,6 @@ function cleanSyncedContent(syncedContent) {
|
||||
|
||||
/**
|
||||
* Apply changes retrieved from the main provider. Update sync data accordingly.
|
||||
* @param {*} changes The changes to apply.
|
||||
*/
|
||||
function applyChanges(changes) {
|
||||
const storeItemMap = { ...store.getters.allItemMap };
|
||||
@ -99,7 +95,7 @@ function applyChanges(changes) {
|
||||
let syncDataChanged = false;
|
||||
|
||||
changes.forEach((change) => {
|
||||
const existingSyncData = syncData[change.fileId];
|
||||
const existingSyncData = syncData[change.syncDataId];
|
||||
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
|
||||
if (change.removed && existingSyncData) {
|
||||
if (existingItem) {
|
||||
@ -107,19 +103,19 @@ function applyChanges(changes) {
|
||||
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
|
||||
delete storeItemMap[existingItem.id];
|
||||
}
|
||||
delete syncData[change.fileId];
|
||||
delete syncData[change.syncDataId];
|
||||
syncDataChanged = true;
|
||||
} else if (!change.removed && change.item && change.item.hash) {
|
||||
if (!existingSyncData || (existingSyncData.hash !== change.item.hash && (
|
||||
!existingItem || existingItem.hash !== change.item.hash
|
||||
))) {
|
||||
// Put object in the store
|
||||
if (change.item.type !== 'content') { // Merge contents later
|
||||
// Put object in the store, except for content and data which will be merge later
|
||||
if (change.item.type !== 'content' && change.item.type !== 'data') {
|
||||
store.commit(`${change.item.type}/setItem`, change.item);
|
||||
storeItemMap[change.item.id] = change.item;
|
||||
}
|
||||
}
|
||||
syncData[change.fileId] = change.syncData;
|
||||
syncData[change.syncDataId] = change.syncData;
|
||||
syncDataChanged = true;
|
||||
}
|
||||
});
|
||||
@ -221,7 +217,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||
];
|
||||
if (isWorkspaceSyncPossible()) {
|
||||
syncLocations.unshift({ id: 'main', providerId: mainProvider.id, fileId });
|
||||
syncLocations.unshift({ id: 'main', providerId: syncProvider.id, fileId });
|
||||
}
|
||||
let result;
|
||||
syncLocations.some((syncLocation) => {
|
||||
@ -355,7 +351,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
||||
}
|
||||
|
||||
// If content was just created, restart sync to create the file as well
|
||||
if (provider === mainProvider &&
|
||||
if (provider === syncProvider &&
|
||||
!store.getters['data/syncDataByItemId'][fileId]
|
||||
) {
|
||||
syncContext.restart = true;
|
||||
@ -396,21 +392,25 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a data item, typically settings and templates.
|
||||
* Sync a data item, typically settings, workspaces and templates.
|
||||
*/
|
||||
function syncDataItem(dataId) {
|
||||
const item = store.state.data.itemMap[dataId];
|
||||
const getItem = () => store.state.data.itemMap[dataId]
|
||||
|| store.state.data.lsItemMap[dataId];
|
||||
|
||||
const item = getItem();
|
||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
||||
// Sync if item hash and syncData hash are inconsistent
|
||||
if (syncData && item && item.hash === syncData.hash) {
|
||||
return null;
|
||||
}
|
||||
const token = mainProvider.getToken();
|
||||
return token && mainProvider.downloadData(token, dataId)
|
||||
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
return syncToken && syncProvider.downloadData(syncToken, dataId)
|
||||
.then((serverItem = null) => {
|
||||
const dataSyncData = store.getters['data/dataSyncData'][dataId];
|
||||
let mergedItem = (() => {
|
||||
const clientItem = utils.deepCopy(store.state.data.itemMap[dataId]);
|
||||
const clientItem = utils.deepCopy(getItem());
|
||||
if (!clientItem) {
|
||||
return serverItem;
|
||||
}
|
||||
@ -445,15 +445,15 @@ function syncDataItem(dataId) {
|
||||
});
|
||||
|
||||
// Retrieve item with new `hash` and freeze it
|
||||
mergedItem = utils.deepCopy(store.state.data.itemMap[dataId]);
|
||||
mergedItem = utils.deepCopy(getItem());
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (serverItem && serverItem.hash === mergedItem.hash) {
|
||||
return null;
|
||||
}
|
||||
return mainProvider.uploadData(
|
||||
token,
|
||||
return syncProvider.uploadData(
|
||||
syncToken,
|
||||
mergedItem,
|
||||
dataId,
|
||||
);
|
||||
@ -469,14 +469,26 @@ function syncDataItem(dataId) {
|
||||
/**
|
||||
* Sync the whole workspace with the main provider and the current file explicit locations.
|
||||
*/
|
||||
function syncWorkspace() {
|
||||
function syncWorkspace(workspace, syncToken) {
|
||||
const syncContext = new SyncContext();
|
||||
const mainToken = store.getters['data/loginToken'];
|
||||
return mainProvider.getChanges(mainToken)
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// Store the sub in the DB since it's not safely stored in the token
|
||||
const localSettings = store.getters['data/localSettings'];
|
||||
if (!localSettings.syncSub) {
|
||||
store.dispatch('data/patchLocalSettings', {
|
||||
workspaceSyncSub: syncToken.sub,
|
||||
});
|
||||
} else if (localSettings.syncSub !== syncToken.sub) {
|
||||
throw new Error('Synchronization failed due to token inconsistency.');
|
||||
}
|
||||
})
|
||||
.then(() => syncProvider.getChanges(syncToken))
|
||||
.then((changes) => {
|
||||
// Apply changes
|
||||
applyChanges(changes);
|
||||
mainProvider.setAppliedChanges(mainToken, changes);
|
||||
syncProvider.setAppliedChanges(changes);
|
||||
|
||||
// Prevent from sending items too long after changes have been retrieved
|
||||
const syncStartTime = Date.now();
|
||||
@ -504,8 +516,8 @@ function syncWorkspace() {
|
||||
// Add file if content has been uploaded
|
||||
(item.type !== 'file' || syncDataByItemId[`${id}/content`])
|
||||
) {
|
||||
result = mainProvider.saveItem(
|
||||
mainToken,
|
||||
result = syncProvider.saveItem(
|
||||
syncToken,
|
||||
// Use deepCopy to freeze objects
|
||||
utils.deepCopy(item),
|
||||
utils.deepCopy(existingSyncData),
|
||||
@ -529,19 +541,20 @@ function syncWorkspace() {
|
||||
...store.state.syncLocation.itemMap,
|
||||
...store.state.publishLocation.itemMap,
|
||||
...store.state.content.itemMap,
|
||||
...store.state.data.itemMap,
|
||||
};
|
||||
const syncData = store.getters['data/syncData'];
|
||||
let result;
|
||||
Object.entries(syncData).some(([, existingSyncData]) => {
|
||||
if (!storeItemMap[existingSyncData.itemId] &&
|
||||
// We don't want to delete data items, especially on first sync
|
||||
existingSyncData.type !== 'data' &&
|
||||
// Remove content only if file has been removed
|
||||
(existingSyncData.type !== 'content' || !storeItemMap[existingSyncData.itemId.split('/')[0]])
|
||||
) {
|
||||
// Use deepCopy to freeze objects
|
||||
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
||||
result = mainProvider
|
||||
.removeItem(mainToken, syncDataToRemove, ifNotTooLate)
|
||||
result = syncProvider
|
||||
.removeItem(syncToken, syncDataToRemove, ifNotTooLate)
|
||||
.then(() => {
|
||||
const syncDataCopy = { ...store.getters['data/syncData'] };
|
||||
delete syncDataCopy[syncDataToRemove.id];
|
||||
@ -590,7 +603,10 @@ function syncWorkspace() {
|
||||
return Promise.resolve()
|
||||
.then(() => saveNextItem())
|
||||
.then(() => removeNextItem())
|
||||
.then(() => syncDataItem('settings'))
|
||||
// Sync settings only in the main workspace
|
||||
.then(() => workspace.id === 'main' && syncDataItem('settings'))
|
||||
// Sync workspaces only in the main workspace
|
||||
.then(() => workspace.id === 'main' && syncDataItem('workspaces'))
|
||||
.then(() => syncDataItem('templates'))
|
||||
.then(() => {
|
||||
const currentFileId = store.getters['file/current'].id;
|
||||
@ -605,14 +621,14 @@ function syncWorkspace() {
|
||||
() => {
|
||||
if (syncContext.restart) {
|
||||
// Restart sync
|
||||
return syncWorkspace();
|
||||
return syncWorkspace(workspace, syncToken);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(err) => {
|
||||
if (err && err.message === 'TOO_LATE') {
|
||||
// Restart sync
|
||||
return syncWorkspace();
|
||||
return syncWorkspace(workspace, syncToken);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
@ -660,12 +676,14 @@ function requestSync() {
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (isWorkspaceSyncPossible()) {
|
||||
return syncWorkspace();
|
||||
return syncWorkspace(
|
||||
store.getters['workspace/currentWorkspace'],
|
||||
store.getters['workspace/syncToken']);
|
||||
}
|
||||
if (hasCurrentFileSyncLocations()) {
|
||||
// Only sync current file if data sync is unavailable.
|
||||
// We also could sync files that are out-of-sync but it would
|
||||
// require to load the syncedContent objects of all files.
|
||||
// Only sync current file if workspace sync is unavailable.
|
||||
// We could also sync files that are out-of-sync but it would
|
||||
// require to load all the syncedContent objects from the DB.
|
||||
return syncFile(store.getters['file/current'].id);
|
||||
}
|
||||
return null;
|
||||
@ -692,14 +710,14 @@ export default {
|
||||
// Load workspaces and tokens from localStorage
|
||||
localDbSvc.syncLocalStorage();
|
||||
|
||||
// Try to find a suitable workspace provider
|
||||
workspaceProvider = providerRegistry.providers[utils.queryParams.providerId];
|
||||
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
|
||||
workspaceProvider = googleDriveAppDataProvider;
|
||||
// Try to find a suitable workspace sync provider
|
||||
syncProvider = providerRegistry.providers[utils.queryParams.providerId];
|
||||
if (!syncProvider || !syncProvider.initWorkspace) {
|
||||
syncProvider = googleDriveAppDataProvider;
|
||||
}
|
||||
|
||||
return workspaceProvider.initWorkspace()
|
||||
.then(workspace => store.commit('workspace/setCurrentWorkspaceId', workspace.id))
|
||||
return syncProvider.initWorkspace()
|
||||
.then(workspace => store.dispatch('workspace/setCurrentWorkspaceId', workspace.id))
|
||||
.then(() => localDbSvc.init())
|
||||
.then(() => {
|
||||
// Sync periodically
|
||||
|
@ -4,24 +4,26 @@ import defaultProperties from '../data/defaultFileProperties.yml';
|
||||
|
||||
const origin = `${location.protocol}//${location.host}`;
|
||||
|
||||
// For uid()
|
||||
// For utils.uid()
|
||||
const uidLength = 16;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
const radix = alphabet.length;
|
||||
const array = new Uint32Array(uidLength);
|
||||
|
||||
// For parseQueryParams()
|
||||
// For utils.parseQueryParams()
|
||||
const parseQueryParams = (params) => {
|
||||
const result = {};
|
||||
params.split('&').forEach((param) => {
|
||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||
result[key] = value;
|
||||
if (key) {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// For addQueryParams()
|
||||
// For utils.addQueryParams()
|
||||
const urlParser = window.document.createElement('a');
|
||||
|
||||
export default {
|
||||
@ -121,20 +123,43 @@ export default {
|
||||
return setInterval(() => func(), this.randomize(interval));
|
||||
},
|
||||
parseQueryParams,
|
||||
addQueryParams(url = '', params = {}) {
|
||||
addQueryParams(url = '', params = {}, hash = false) {
|
||||
const keys = Object.keys(params).filter(key => params[key] != null);
|
||||
urlParser.href = url;
|
||||
if (!keys.length) {
|
||||
return urlParser.href;
|
||||
}
|
||||
if (urlParser.search) {
|
||||
urlParser.search += '&';
|
||||
const serializedParams = keys.map(key =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
||||
if (hash) {
|
||||
if (urlParser.hash) {
|
||||
urlParser.hash += '&';
|
||||
} else {
|
||||
urlParser.hash = '#';
|
||||
}
|
||||
urlParser.hash += serializedParams;
|
||||
} else {
|
||||
urlParser.search = '?';
|
||||
if (urlParser.search) {
|
||||
urlParser.search += '&';
|
||||
} else {
|
||||
urlParser.search = '?';
|
||||
}
|
||||
urlParser.search += serializedParams;
|
||||
}
|
||||
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
||||
return urlParser.href;
|
||||
},
|
||||
resolveUrl(url) {
|
||||
return this.addQueryParams(url);
|
||||
},
|
||||
createHiddenIframe(url) {
|
||||
const iframeElt = document.createElement('iframe');
|
||||
iframeElt.style.position = 'absolute';
|
||||
iframeElt.style.left = '-9999px';
|
||||
iframeElt.style.width = '1px';
|
||||
iframeElt.style.height = '1px';
|
||||
iframeElt.src = url;
|
||||
return iframeElt;
|
||||
},
|
||||
wrapRange(range, eltProperties) {
|
||||
const rangeLength = `${range}`.length;
|
||||
let wrappedLength = 0;
|
||||
|
@ -87,6 +87,9 @@ const tokenSetter = providerId => ({ getters, dispatch }, token) => {
|
||||
});
|
||||
};
|
||||
|
||||
// For workspaces
|
||||
const urlParser = window.document.createElement('a');
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
@ -118,18 +121,24 @@ export default {
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
workspaces: (state) => {
|
||||
workspaces: (state, getters, rootState, rootGetters) => {
|
||||
const workspaces = (state.lsItemMap.workspaces || empty('workspaces')).data;
|
||||
const result = {};
|
||||
const sanitizedWorkspaces = {};
|
||||
const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken'];
|
||||
Object.entries(workspaces).forEach(([id, workspace]) => {
|
||||
result[id] = {
|
||||
...workspace,
|
||||
const sanitizedWorkspace = {
|
||||
id,
|
||||
providerId: workspace.providerId || 'googleDriveWorkspace',
|
||||
url: utils.addQueryParams('app'),
|
||||
providerId: mainWorkspaceToken && 'googleDriveAppData',
|
||||
sub: mainWorkspaceToken && mainWorkspaceToken.sub,
|
||||
...workspace,
|
||||
};
|
||||
// Rebuild the url with current hostname
|
||||
urlParser.href = workspace.url || 'app';
|
||||
const params = utils.parseQueryParams(urlParser.hash.slice(1));
|
||||
sanitizedWorkspace.url = utils.addQueryParams('app', params, true);
|
||||
sanitizedWorkspaces[id] = sanitizedWorkspace;
|
||||
});
|
||||
return result;
|
||||
return sanitizedWorkspaces;
|
||||
},
|
||||
settings: getter('settings'),
|
||||
computedSettings: (state, getters) => {
|
||||
@ -202,13 +211,6 @@ export default {
|
||||
githubTokens: (state, getters) => getters.tokens.github || {},
|
||||
wordpressTokens: (state, getters) => getters.tokens.wordpress || {},
|
||||
zendeskTokens: (state, getters) => getters.tokens.zendesk || {},
|
||||
loginToken: (state, getters) => {
|
||||
// Return the first google token that has the isLogin flag
|
||||
const googleTokens = getters.googleTokens;
|
||||
const loginSubs = Object.keys(googleTokens)
|
||||
.filter(sub => googleTokens[sub].isLogin);
|
||||
return googleTokens[loginSubs[0]];
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setWorkspaces: setter('workspaces'),
|
||||
|
@ -123,7 +123,7 @@ export default {
|
||||
}
|
||||
},
|
||||
createNewDiscussion({ commit, dispatch, rootGetters }, selection) {
|
||||
const loginToken = rootGetters['data/loginToken'];
|
||||
const loginToken = rootGetters['workspace/loginToken'];
|
||||
if (!loginToken) {
|
||||
dispatch('modal/signInForComment', {
|
||||
onResolve: () => googleHelper.signin()
|
||||
|
@ -18,6 +18,7 @@ import modal from './modal';
|
||||
import notification from './notification';
|
||||
import queue from './queue';
|
||||
import userInfo from './userInfo';
|
||||
import workspace from './workspace';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@ -41,6 +42,7 @@ const store = new Vuex.Store({
|
||||
notification,
|
||||
queue,
|
||||
userInfo,
|
||||
workspace,
|
||||
},
|
||||
state: {
|
||||
offline: false,
|
||||
@ -55,8 +57,8 @@ const store = new Vuex.Store({
|
||||
return result;
|
||||
},
|
||||
isSponsor: (state, getters) => {
|
||||
const loginToken = getters['data/loginToken'];
|
||||
return state.monetizeSponsor || (loginToken && loginToken.isSponsor);
|
||||
const sponsorToken = getters['workspace/sponsorToken'];
|
||||
return state.monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
|
@ -75,8 +75,13 @@ export default {
|
||||
resolveText: 'Yes, revert',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
removeWorkspace: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>This will clean your workspace locally. Are you sure?</p>',
|
||||
resolveText: 'Yes, clean',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
reset: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>This will clean your local files and settings. Are you sure?</p>',
|
||||
content: '<p>This will clean all your workspaces locally. Are you sure?</p>',
|
||||
resolveText: 'Yes, clean',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
|
@ -1,38 +1,50 @@
|
||||
import utils from '../services/utils';
|
||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
|
||||
const idShifter = offset => (state, getters) => {
|
||||
const ids = Object.keys(getters.currentFileDiscussions);
|
||||
const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length;
|
||||
return ids[idx % ids.length];
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
currentWorkspaceId: null,
|
||||
lastFocus: 0,
|
||||
},
|
||||
mutations: {
|
||||
setCurrentWorkspaceId: (state, value) => {
|
||||
state.currentWorkspaceId = value;
|
||||
},
|
||||
setLastFocus: (state, value) => {
|
||||
state.lastFocus = value;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
mainWorkspace: (state, getters, rootState, rootGetters) => {
|
||||
const workspaces = rootGetters['data/workspaces'];
|
||||
return workspaces.main;
|
||||
},
|
||||
currentWorkspace: (state, getters, rootState, rootGetters) => {
|
||||
const workspaces = rootGetters['data/workspaces'];
|
||||
return workspaces[state.currentWorkspaceId] || workspaces.main;
|
||||
return workspaces[state.currentWorkspaceId] || getters.mainWorkspace;
|
||||
},
|
||||
lastSyncActivityKey: (state, getters) => {
|
||||
const workspaceId = getters.currentWorkspace.id;
|
||||
return `${workspaceId}/lastSyncActivity`;
|
||||
lastSyncActivityKey: (state, getters) => `${getters.currentWorkspace.id}/lastSyncActivity`,
|
||||
lastFocusKey: (state, getters) => `${getters.currentWorkspace.id}/lastWindowFocus`,
|
||||
mainWorkspaceToken: (state, getters, rootState, rootGetters) => {
|
||||
const googleTokens = rootGetters['data/googleTokens'];
|
||||
const loginSubs = Object.keys(googleTokens)
|
||||
.filter(sub => googleTokens[sub].isLogin);
|
||||
return googleTokens[loginSubs[0]];
|
||||
},
|
||||
lastFocusKey: (state, getters) => {
|
||||
const workspaceId = getters.currentWorkspace.id;
|
||||
return `${workspaceId}/lastWindowFocus`;
|
||||
syncToken: (state, getters, rootState, rootGetters) => {
|
||||
const workspace = getters.currentWorkspace;
|
||||
if (workspace.providerId === 'googleDriveWorkspace') {
|
||||
const googleTokens = rootGetters['data/googleTokens'];
|
||||
return googleTokens[workspace.sub];
|
||||
}
|
||||
return getters.mainWorkspaceToken;
|
||||
},
|
||||
lastFocus: (state, getters) => parseInt(localStorage.getItem(getters.lastFocusKey), 10) || 0,
|
||||
loginToken: (state, getters) => getters.syncToken,
|
||||
sponsorToken: (state, getters) => getters.mainWorkspaceToken,
|
||||
},
|
||||
actions: {
|
||||
setCurrentWorkspaceId: ({ commit, getters }, value) => {
|
||||
commit('setCurrentWorkspaceId', value);
|
||||
const lastFocus = parseInt(localStorage.getItem(getters.lastFocusKey), 10) || 0;
|
||||
commit('setLastFocus', lastFocus);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user