CouchDB workspace (part 1)
This commit is contained in:
parent
ec0d5aac3e
commit
aac305e410
4
src/assets/iconCouchdb.svg
Normal file
4
src/assets/iconCouchdb.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M405.365,303.996c0,20.375 -11.207,30.563 -31.582,31.582l-248.584,0c-20.376,0 -31.583,-10.188 -31.583,-31.582c0,-20.376 11.207,-30.564 31.583,-31.583l249.602,0c20.376,1.019 30.564,11.207 30.564,31.583Zm-30.564,46.864l-249.602,0c-20.376,0 -31.583,10.188 -31.583,31.582c0,20.376 11.207,30.564 31.583,31.583l249.602,0c20.376,0 31.583,-10.188 31.583,-31.583c-1.019,-21.394 -11.207,-31.582 -31.583,-31.582Zm77.428,-172.175c-20.376,0 -31.582,10.188 -31.582,30.563l0,172.175c0,20.376 11.206,30.564 31.582,31.583c30.564,-1.019 46.864,-31.583 46.864,-93.729l0,-77.427c0,-41.771 -16.3,-62.146 -46.864,-63.165Zm-404.458,0c-30.564,1.019 -46.864,21.394 -46.864,63.165l0,77.427c0,62.146 16.3,92.71 46.864,93.729c20.376,0 31.582,-10.188 31.582,-31.583l0,-171.156c-1.019,-20.375 -11.206,-30.563 -31.582,-31.582Zm404.458,-15.282c0,-51.958 -27.507,-76.409 -77.428,-77.428l-249.602,0c-50.94,1.019 -77.428,26.489 -77.428,77.428c30.563,0 46.864,16.301 46.864,46.864c0,30.564 16.301,46.864 46.864,46.864l218.021,0c30.563,0 46.864,-16.3 46.864,-46.864c-1.019,-31.582 16.3,-45.845 45.845,-46.864Z" style="fill:#e42528;fill-rule:nonzero;"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -33,6 +33,8 @@
|
|||||||
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
||||||
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
||||||
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
||||||
|
<couchdb-workspace-modal v-else-if="config.type === 'couchdbWorkspace'"></couchdb-workspace-modal>
|
||||||
|
<couchdb-credentials-modal v-else-if="config.type === 'couchdbCredentials'"></couchdb-credentials-modal>
|
||||||
<modal-inner v-else aria-label="Dialog">
|
<modal-inner v-else aria-label="Dialog">
|
||||||
<div class="modal__content" v-html="config.content"></div>
|
<div class="modal__content" v-html="config.content"></div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
@ -81,6 +83,8 @@ import BloggerPublishModal from './modals/providers/BloggerPublishModal';
|
|||||||
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
|
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
|
||||||
import ZendeskAccountModal from './modals/providers/ZendeskAccountModal';
|
import ZendeskAccountModal from './modals/providers/ZendeskAccountModal';
|
||||||
import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
|
import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
|
||||||
|
import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal';
|
||||||
|
import CouchdbCredentialsModal from './modals/providers/CouchdbCredentialsModal';
|
||||||
|
|
||||||
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield')
|
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield')
|
||||||
// Filter enabled and visible element
|
// Filter enabled and visible element
|
||||||
@ -122,6 +126,8 @@ export default {
|
|||||||
BloggerPagePublishModal,
|
BloggerPagePublishModal,
|
||||||
ZendeskAccountModal,
|
ZendeskAccountModal,
|
||||||
ZendeskPublishModal,
|
ZendeskPublishModal,
|
||||||
|
CouchdbWorkspaceModal,
|
||||||
|
CouchdbCredentialsModal,
|
||||||
},
|
},
|
||||||
computed: mapGetters('modal', [
|
computed: mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="tour-step" :class="'tour-step--' + step" :style="stepStyle">
|
<div class="tour-step" :class="'tour-step--' + step" :style="stepStyle">
|
||||||
<div class="tour-step__inner" v-if="step === 'welcome'">
|
<div class="tour-step__inner" v-if="step === 'welcome'">
|
||||||
<h2>Welcome to StackEdit!</h2>
|
<h2>Welcome to StackEdit!</h2>
|
||||||
<p>Greater, lighter, faster... StackEdit 5 is here!</p>
|
<p>Greater, lighter, faster... <b>StackEdit 5</b> is here!</p>
|
||||||
<p>Please click <b>Next</b> to take a quick tour.</p>
|
<p>Please click <b>Next</b> to take a quick tour.</p>
|
||||||
<div class="tour-step__button-bar">
|
<div class="tour-step__button-bar">
|
||||||
<button class="button" @click="finish">Skip</button>
|
<button class="button" @click="finish">Skip</button>
|
||||||
@ -139,7 +139,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$tour-step-background: mix(#fafafa, $selection-highlighting-color, 80%);
|
$tour-step-background: mix(#fafafa, $selection-highlighting-color, 80%);
|
||||||
$tour-step-width: 220px;
|
$tour-step-width: 240px;
|
||||||
|
|
||||||
.tour-step__inner {
|
.tour-step__inner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -1,31 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="menu-info-entries" v-if="!loginToken">
|
<div class="menu-info-entries">
|
||||||
<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" v-if="loginToken">
|
||||||
|
<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" v-if="syncToken">
|
||||||
|
<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>
|
||||||
|
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-info-entries" 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>
|
|
||||||
</div>
|
|
||||||
<menu-entry v-if="!loginToken" @click.native="signin">
|
<menu-entry v-if="!loginToken" @click.native="signin">
|
||||||
<icon-login slot="icon"></icon-login>
|
<icon-login slot="icon"></icon-login>
|
||||||
<div>Sign in with Google</div>
|
<div>Sign in with Google</div>
|
||||||
<span>Back up and sync your main workspace.</span>
|
<span>Sync your main workspace and unlock functionalities.</span>
|
||||||
</menu-entry>
|
</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>
|
||||||
@ -103,6 +101,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters('workspace', [
|
...mapGetters('workspace', [
|
||||||
'currentWorkspace',
|
'currentWorkspace',
|
||||||
|
'syncToken',
|
||||||
'loginToken',
|
'loginToken',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,11 @@
|
|||||||
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
||||||
<span>Add Google Drive workspace</span>
|
<span>Add Google Drive workspace</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addCouchdbWorkspace">
|
||||||
|
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
||||||
|
<span>Add CouchDB workspace</span>
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
<menu-entry @click.native="manageWorkspaces">
|
<menu-entry @click.native="manageWorkspaces">
|
||||||
<icon-database slot="icon"></icon-database>
|
<icon-database slot="icon"></icon-database>
|
||||||
<span>Manage workspaces</span>
|
<span>Manage workspaces</span>
|
||||||
@ -44,6 +49,12 @@ export default {
|
|||||||
}))
|
}))
|
||||||
.catch(() => {}); // Cancel
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
|
addCouchdbWorkspace() {
|
||||||
|
return this.$store.dispatch('modal/open', {
|
||||||
|
type: 'couchdbWorkspace',
|
||||||
|
})
|
||||||
|
.catch(() => {}); // Cancel
|
||||||
|
},
|
||||||
manageWorkspaces() {
|
manageWorkspaces() {
|
||||||
return this.$store.dispatch('modal/open', 'workspaceManagement');
|
return this.$store.dispatch('modal/open', 'workspaceManagement');
|
||||||
},
|
},
|
||||||
@ -62,9 +73,5 @@ export default {
|
|||||||
.workspace__name {
|
.workspace__name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
|
||||||
.menu-entry div & {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import htmlSanitizer from '../../libs/htmlSanitizer';
|
|
||||||
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||||
import faq from '../../data/faq.md';
|
import faq from '../../data/faq.md';
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ export default {
|
|||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
faq() {
|
faq() {
|
||||||
return htmlSanitizer.sanitizeHtml(markdownConversionSvc.defaultConverter.render(faq));
|
return markdownConversionSvc.defaultConverter.render(faq);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
54
src/components/modals/providers/CouchdbCredentialsModal.vue
Normal file
54
src/components/modals/providers/CouchdbCredentialsModal.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<modal-inner aria-label="Insert image">
|
||||||
|
<div class="modal__content">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="couchdb"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>Please provide your credentials to login to <b>CouchDB</b>.</p>
|
||||||
|
<form-entry label="Name" error="name">
|
||||||
|
<input slot="field" class="textfield" type="text" v-model.trim="name" @keyup.enter="resolve()">
|
||||||
|
</form-entry>
|
||||||
|
<form-entry label="Password" error="password">
|
||||||
|
<input slot="field" class="textfield" type="password" v-model.trim="password" @keyup.enter="resolve()">
|
||||||
|
</form-entry>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
|
export default modalTemplate({
|
||||||
|
data: () => ({
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
}),
|
||||||
|
created() {
|
||||||
|
this.name = this.config.token.name;
|
||||||
|
this.password = this.config.token.password;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolve() {
|
||||||
|
if (!this.name) {
|
||||||
|
this.setError('name');
|
||||||
|
}
|
||||||
|
if (!this.password) {
|
||||||
|
this.setError('password');
|
||||||
|
}
|
||||||
|
if (this.name && this.password) {
|
||||||
|
const token = {
|
||||||
|
...this.config.token,
|
||||||
|
name: this.name,
|
||||||
|
password: this.password,
|
||||||
|
};
|
||||||
|
this.$store.dispatch('data/setCouchdbToken', token);
|
||||||
|
this.config.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
63
src/components/modals/providers/CouchdbWorkspaceModal.vue
Normal file
63
src/components/modals/providers/CouchdbWorkspaceModal.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<modal-inner aria-label="Add CouchDB workspace">
|
||||||
|
<div class="modal__content">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="couchdb"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will create a workspace synchronized with a <b>CouchDB</b> database.</p>
|
||||||
|
<form-entry label="Database URL" error="dbUrl">
|
||||||
|
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keyup.enter="resolve()">
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> https://instance.smileupps.com/stackedit-workspace
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" v-if="!showInfo" @click="showInfo = true">More info</a>
|
||||||
|
</div>
|
||||||
|
</form-entry>
|
||||||
|
<div class="couchdb-workspace__info" v-if="showInfo" v-html="info"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
import utils from '../../../services/utils';
|
||||||
|
import couchdbSetup from '../../../data/couchdbSetup.md';
|
||||||
|
import markdownConversionSvc from '../../../services/markdownConversionSvc';
|
||||||
|
|
||||||
|
export default modalTemplate({
|
||||||
|
data: () => ({
|
||||||
|
dbUrl: '',
|
||||||
|
showInfo: false,
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
info() {
|
||||||
|
return markdownConversionSvc.defaultConverter.render(couchdbSetup);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolve() {
|
||||||
|
if (!this.dbUrl) {
|
||||||
|
this.setError('dbUrl');
|
||||||
|
} else {
|
||||||
|
const url = utils.addQueryParams('app', {
|
||||||
|
providerId: 'couchdbWorkspace',
|
||||||
|
dbUrl: this.dbUrl,
|
||||||
|
}, true);
|
||||||
|
this.config.resolve();
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.couchdb-workspace__info {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
</style>
|
27
src/data/couchdbSetup.md
Normal file
27
src/data/couchdbSetup.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
### Pre-requisites
|
||||||
|
|
||||||
|
- CouchDB 0.11 or later,
|
||||||
|
- In order to work with https://stackedit.io, your database needs to be accessible over HTTPS.
|
||||||
|
|
||||||
|
> **Tip:** [Smileupps](https://www.smileupps.com/) provide free CouchDB hosting.
|
||||||
|
|
||||||
|
### Enable CORS
|
||||||
|
|
||||||
|
Add the following key/value pairs to your CouchDB configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
[httpd]
|
||||||
|
enable_cors = true
|
||||||
|
|
||||||
|
[cors]
|
||||||
|
origins = https://stackedit.io
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Create the database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT https://instance.smileupps.com/stackedit-workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
> You may want to restrict access to the database by [specifying members](http://docs.couchdb.org/en/latest/api/database/security.html).
|
@ -7,6 +7,7 @@ We recommend syncing your workspace to make sure files won't be lost in case you
|
|||||||
|
|
||||||
If you sign in with Google, your main workspace will be stored in Google Drive (in your [app data folder](https://developers.google.com/drive/v3/web/appdata)).
|
If you sign in with Google, your main workspace will be stored in Google Drive (in your [app data folder](https://developers.google.com/drive/v3/web/appdata)).
|
||||||
If you open a Google Drive workspace, the files in the workspace will be stored inside a Google Drive folder which you can share with other users.
|
If you open a Google Drive workspace, the files in the workspace will be stored inside a Google Drive folder which you can share with other users.
|
||||||
|
If you open a CouchDB workspace, the files in the workspace will be stored in the CouchDB database which can be hosted on premises for privacy concerns.
|
||||||
|
|
||||||
**Can StackEdit access my data without telling me?**
|
**Can StackEdit access my data without telling me?**
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ export default {
|
|||||||
return 'github';
|
return 'github';
|
||||||
case 'bloggerPage':
|
case 'bloggerPage':
|
||||||
return 'blogger';
|
return 'blogger';
|
||||||
|
case 'couchdbWorkspace':
|
||||||
|
return 'couchdb';
|
||||||
default:
|
default:
|
||||||
return this.providerId;
|
return this.providerId;
|
||||||
}
|
}
|
||||||
@ -70,4 +72,8 @@ export default {
|
|||||||
.icon-provider--zendesk {
|
.icon-provider--zendesk {
|
||||||
background-image: url(../assets/iconZendesk.svg);
|
background-image: url(../assets/iconZendesk.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-provider--couchdb {
|
||||||
|
background-image: url(../assets/iconCouchdb.svg);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -459,7 +459,7 @@ const localDbSvc = {
|
|||||||
*/
|
*/
|
||||||
removeWorkspace(id) {
|
removeWorkspace(id) {
|
||||||
const workspaces = {
|
const workspaces = {
|
||||||
...this.workspaces,
|
...store.getters['data/workspaces'],
|
||||||
};
|
};
|
||||||
delete workspaces[id];
|
delete workspaces[id];
|
||||||
store.dispatch('data/setWorkspaces', workspaces);
|
store.dispatch('data/setWorkspaces', workspaces);
|
||||||
|
@ -221,6 +221,7 @@ export default {
|
|||||||
store.commit('updateLastOfflineCheck');
|
store.commit('updateLastOfflineCheck');
|
||||||
}
|
}
|
||||||
const xhr = new window.XMLHttpRequest();
|
const xhr = new window.XMLHttpRequest();
|
||||||
|
xhr.withCredentials = config.withCredentials || false;
|
||||||
let timeoutId;
|
let timeoutId;
|
||||||
|
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
|
87
src/services/providers/couchdbWorkspaceProvider.js
Normal file
87
src/services/providers/couchdbWorkspaceProvider.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import store from '../../store';
|
||||||
|
import couchdbHelper from './helpers/couchdbHelper';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
import utils from '../utils';
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
id: 'couchdbWorkspace',
|
||||||
|
getToken() {
|
||||||
|
return store.getters['workspace/syncToken'];
|
||||||
|
},
|
||||||
|
initWorkspace() {
|
||||||
|
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
|
||||||
|
const workspaceIdParams = {
|
||||||
|
providerId: this.id,
|
||||||
|
dbUrl,
|
||||||
|
};
|
||||||
|
const workspaceId = utils.makeWorkspaceId(workspaceIdParams);
|
||||||
|
const getToken = () => store.getters['data/couchdbTokens'][workspaceId];
|
||||||
|
const getWorkspace = () => store.getters['data/sanitizedWorkspaces'][workspaceId];
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
// Create token
|
||||||
|
store.dispatch('data/setCouchdbToken', {
|
||||||
|
sub: workspaceId,
|
||||||
|
dbUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => getWorkspace() || couchdbHelper.getDb(getToken())
|
||||||
|
.then((db) => {
|
||||||
|
store.dispatch('data/patchWorkspaces', {
|
||||||
|
[workspaceId]: {
|
||||||
|
id: workspaceId,
|
||||||
|
name: db.db_name,
|
||||||
|
providerId: this.id,
|
||||||
|
dbUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return getWorkspace();
|
||||||
|
}, () => {
|
||||||
|
throw new Error(`${dbUrl} is not accessible. Make sure you have the right permissions.`);
|
||||||
|
}))
|
||||||
|
.then((workspace) => {
|
||||||
|
// Fix the URL hash
|
||||||
|
utils.setQueryParams(workspaceIdParams);
|
||||||
|
if (workspace.url !== location.href) {
|
||||||
|
store.dispatch('data/patchWorkspaces', {
|
||||||
|
[workspace.id]: {
|
||||||
|
...workspace,
|
||||||
|
url: location.href,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return getWorkspace();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getChanges() {
|
||||||
|
const workspace = store.getters['workspace/currentWorkspace'];
|
||||||
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
|
const lastSeq = store.getters['data/localSettings'].syncLastSeq;
|
||||||
|
return couchdbHelper.getChanges(syncToken, lastSeq, true)
|
||||||
|
.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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Build sync data
|
||||||
|
change.syncData = {
|
||||||
|
id: change.fileId,
|
||||||
|
itemId: change.item.id,
|
||||||
|
type: change.item.type,
|
||||||
|
hash: change.item.hash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
change.syncDataId = change.fileId;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
changes.startPageToken = result.startPageToken;
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -100,7 +100,7 @@ export default providerRegistry.register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Return the workspace
|
// Return the workspace
|
||||||
return getWorkspace(folder.id);
|
return store.getters['data/sanitizedWorkspaces'][workspaceId];
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
@ -148,7 +148,15 @@ export default providerRegistry.register({
|
|||||||
.then((workspace) => {
|
.then((workspace) => {
|
||||||
// Fix the URL hash
|
// Fix the URL hash
|
||||||
utils.setQueryParams(makeWorkspaceIdParams(workspace.folderId));
|
utils.setQueryParams(makeWorkspaceIdParams(workspace.folderId));
|
||||||
return workspace;
|
if (workspace.url !== location.href) {
|
||||||
|
store.dispatch('data/patchWorkspaces', {
|
||||||
|
[workspace.id]: {
|
||||||
|
...workspace,
|
||||||
|
url: location.href,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return store.getters['data/sanitizedWorkspaces'][workspace.id];
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
performAction() {
|
performAction() {
|
||||||
|
50
src/services/providers/helpers/couchdbHelper.js
Normal file
50
src/services/providers/helpers/couchdbHelper.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import networkSvc from '../../networkSvc';
|
||||||
|
import utils from '../../utils';
|
||||||
|
import store from '../../../store';
|
||||||
|
|
||||||
|
const request = (token, options = {}) => {
|
||||||
|
const baseUrl = `${token.dbUrl}/`;
|
||||||
|
const getLastToken = () => store.getters['data/couchdbTokens'][token.sub];
|
||||||
|
|
||||||
|
const ifUnauthorized = cb => (err) => {
|
||||||
|
if (err.status !== 401) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return cb(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUnauthorized = () => networkSvc.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: utils.resolveUrl(baseUrl, '../_session'),
|
||||||
|
withCredentials: true,
|
||||||
|
body: {
|
||||||
|
name: getLastToken().name,
|
||||||
|
password: getLastToken().password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(ifUnauthorized(() => store.dispatch('modal/open', {
|
||||||
|
type: 'couchdbCredentials',
|
||||||
|
token: getLastToken(),
|
||||||
|
})
|
||||||
|
.then(onUnauthorized)));
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...options,
|
||||||
|
url: utils.resolveUrl(baseUrl, options.path || '.'),
|
||||||
|
withCredentials: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return networkSvc.request(config)
|
||||||
|
.catch(ifUnauthorized(() => onUnauthorized()
|
||||||
|
.then(() => networkSvc.request(config))));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getDb(token) {
|
||||||
|
return request(token)
|
||||||
|
.then(res => res.body);
|
||||||
|
},
|
||||||
|
getChanges() {
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
@ -6,6 +6,7 @@ import networkSvc from './networkSvc';
|
|||||||
import providerRegistry from './providers/providerRegistry';
|
import providerRegistry from './providers/providerRegistry';
|
||||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||||
import './providers/googleDriveWorkspaceProvider';
|
import './providers/googleDriveWorkspaceProvider';
|
||||||
|
import './providers/couchdbWorkspaceProvider';
|
||||||
|
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||||
|
@ -171,8 +171,19 @@ export default {
|
|||||||
}
|
}
|
||||||
return urlParser.href;
|
return urlParser.href;
|
||||||
},
|
},
|
||||||
resolveUrl(url) {
|
resolveUrl(baseUrl, path) {
|
||||||
return this.addQueryParams(url);
|
const oldBaseElt = document.getElementsByTagName('base')[0];
|
||||||
|
const oldHref = oldBaseElt && oldBaseElt.href;
|
||||||
|
const newBaseElt = oldBaseElt || document.head.appendChild(document.createElement('base'));
|
||||||
|
newBaseElt.href = baseUrl;
|
||||||
|
urlParser.href = path;
|
||||||
|
const result = urlParser.href;
|
||||||
|
if (oldBaseElt) {
|
||||||
|
oldBaseElt.href = oldHref;
|
||||||
|
} else {
|
||||||
|
document.head.removeChild(newBaseElt);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
createHiddenIframe(url) {
|
createHiddenIframe(url) {
|
||||||
const iframeElt = document.createElement('iframe');
|
const iframeElt = document.createElement('iframe');
|
||||||
|
@ -207,6 +207,7 @@ export default {
|
|||||||
dataSyncData: getter('dataSyncData'),
|
dataSyncData: getter('dataSyncData'),
|
||||||
tokens: getter('tokens'),
|
tokens: getter('tokens'),
|
||||||
googleTokens: (state, getters) => getters.tokens.google || {},
|
googleTokens: (state, getters) => getters.tokens.google || {},
|
||||||
|
couchdbTokens: (state, getters) => getters.tokens.couchdb || {},
|
||||||
dropboxTokens: (state, getters) => getters.tokens.dropbox || {},
|
dropboxTokens: (state, getters) => getters.tokens.dropbox || {},
|
||||||
githubTokens: (state, getters) => getters.tokens.github || {},
|
githubTokens: (state, getters) => getters.tokens.github || {},
|
||||||
wordpressTokens: (state, getters) => getters.tokens.wordpress || {},
|
wordpressTokens: (state, getters) => getters.tokens.wordpress || {},
|
||||||
@ -281,6 +282,7 @@ export default {
|
|||||||
patchDataSyncData: patcher('dataSyncData'),
|
patchDataSyncData: patcher('dataSyncData'),
|
||||||
patchTokens: patcher('tokens'),
|
patchTokens: patcher('tokens'),
|
||||||
setGoogleToken: tokenSetter('google'),
|
setGoogleToken: tokenSetter('google'),
|
||||||
|
setCouchdbToken: tokenSetter('couchdb'),
|
||||||
setDropboxToken: tokenSetter('dropbox'),
|
setDropboxToken: tokenSetter('dropbox'),
|
||||||
setGithubToken: tokenSetter('github'),
|
setGithubToken: tokenSetter('github'),
|
||||||
setWordpressToken: tokenSetter('wordpress'),
|
setWordpressToken: tokenSetter('wordpress'),
|
||||||
|
@ -121,7 +121,7 @@ export default {
|
|||||||
onResolve,
|
onResolve,
|
||||||
}),
|
}),
|
||||||
sponsorOnly: ({ dispatch }) => dispatch('open', {
|
sponsorOnly: ({ dispatch }) => dispatch('open', {
|
||||||
content: '<p>This feature is restricted to <b>sponsor users</b> as it relies on server resources.</p>',
|
content: '<p>This feature is restricted to sponsors as it relies on server resources.</p>',
|
||||||
resolveText: 'Ok, I understand',
|
resolveText: 'Ok, I understand',
|
||||||
}),
|
}),
|
||||||
paymentSuccess: ({ dispatch }) => dispatch('open', {
|
paymentSuccess: ({ dispatch }) => dispatch('open', {
|
||||||
|
@ -31,13 +31,30 @@ export default {
|
|||||||
},
|
},
|
||||||
syncToken: (state, getters, rootState, rootGetters) => {
|
syncToken: (state, getters, rootState, rootGetters) => {
|
||||||
const workspace = getters.currentWorkspace;
|
const workspace = getters.currentWorkspace;
|
||||||
if (workspace.providerId === 'googleDriveWorkspace') {
|
switch (workspace.providerId) {
|
||||||
const googleTokens = rootGetters['data/googleTokens'];
|
case 'googleDriveWorkspace': {
|
||||||
return googleTokens[workspace.sub];
|
const googleTokens = rootGetters['data/googleTokens'];
|
||||||
|
return googleTokens[workspace.sub];
|
||||||
|
}
|
||||||
|
case 'couchdbWorkspace': {
|
||||||
|
const couchdbTokens = rootGetters['data/couchdbTokens'];
|
||||||
|
return couchdbTokens[workspace.id];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return getters.mainWorkspaceToken;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loginToken: (state, getters, rootState, rootGetters) => {
|
||||||
|
const workspace = getters.currentWorkspace;
|
||||||
|
switch (workspace.providerId) {
|
||||||
|
case 'googleDriveWorkspace': {
|
||||||
|
const googleTokens = rootGetters['data/googleTokens'];
|
||||||
|
return googleTokens[workspace.sub];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return getters.mainWorkspaceToken;
|
||||||
}
|
}
|
||||||
return getters.mainWorkspaceToken;
|
|
||||||
},
|
},
|
||||||
loginToken: (state, getters) => getters.syncToken,
|
|
||||||
sponsorToken: (state, getters) => getters.mainWorkspaceToken,
|
sponsorToken: (state, getters) => getters.mainWorkspaceToken,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
Loading…
Reference in New Issue
Block a user