Github workspace (part 1)

This commit is contained in:
Benoit Schweblin 2018-04-27 15:37:05 +01:00
parent 790ac996dd
commit 53ccee0d84
59 changed files with 1056 additions and 333 deletions

View File

@ -6,11 +6,12 @@
https://stackedit.io/
### NEW!!! Embed StackEdit in any website!
### Ecosystem
See https://github.com/benweet/stackedit.js
Chrome extension: https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha
- [Chrome app](https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg)
- NEW! Embed StackEdit in any website with [stackedit.js](https://github.com/benweet/stackedit.js)
- NEW! [Chrome extension](https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha) that uses stackedit.js
- [Community](https://community.stackedit.io/)
### Build Setup

View File

@ -41,6 +41,7 @@ import DropboxPublishModal from './modals/providers/DropboxPublishModal';
import GithubAccountModal from './modals/providers/GithubAccountModal';
import GithubOpenModal from './modals/providers/GithubOpenModal';
import GithubSaveModal from './modals/providers/GithubSaveModal';
import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal';
import GistSyncModal from './modals/providers/GistSyncModal';
import GistPublishModal from './modals/providers/GistPublishModal';
@ -84,6 +85,7 @@ export default {
GithubAccountModal,
GithubOpenModal,
GithubSaveModal,
GithubWorkspaceModal,
GithubPublishModal,
GistSyncModal,
GistPublishModal,
@ -176,6 +178,10 @@ export default {
hr {
margin: 0.5em 0;
}
p {
line-height: 1.5;
}
}
.modal__inner-1 {
@ -221,9 +227,9 @@ export default {
.modal__image {
float: left;
width: 64px;
height: 64px;
margin: 1.5em 1.5em 0.5em 0;
width: 60px;
height: 60px;
margin: 1.5em 1.2em 0.5em 0;
& + *::after {
content: '';
@ -262,6 +268,11 @@ export default {
}
}
.modal__info--multiline {
padding-top: 0.1em;
padding-bottom: 0.1em;
}
.modal__button-bar {
margin-top: 1.75rem;
text-align: right;

View File

@ -31,7 +31,7 @@
<script>
import { mapMutations, mapGetters } from 'vuex';
import providerRegistry from '../../services/providers/providerRegistry';
import providerRegistry from '../../services/providers/common/providerRegistry';
import MenuEntry from './common/MenuEntry';
import UserImage from '../UserImage';
import UserName from '../UserName';
@ -48,7 +48,7 @@ let cachedFileId;
let revisionsPromise;
let revisionContentPromises;
const pageSize = 30;
const spacerThreshold = 12 * 60 * 60 * 1000; // 12h
const spacerThreshold = 60 * 60 * 1000; // 1h
export default {
components: {

View File

@ -27,7 +27,7 @@
import TurndownService from 'turndown/lib/turndown.browser.umd';
import htmlSanitizer from '../../libs/htmlSanitizer';
import MenuEntry from './common/MenuEntry';
import providerUtils from '../../services/providers/providerUtils';
import Provider from '../../services/providers/common/Provider';
import store from '../../store';
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
@ -56,7 +56,7 @@ export default {
const file = evt.target.files[0];
readFile(file)
.then(content => this.$store.dispatch('createFile', {
...providerUtils.parseContent(content),
...Provider.parseContent(content),
name: file.name,
})
.then(item => this.$store.commit('file/setCurrentId', item.id)));
@ -65,7 +65,7 @@ export default {
const file = evt.target.files[0];
readFile(file)
.then(content => this.$store.dispatch('createFile', {
...providerUtils.parseContent(
...Provider.parseContent(
turndownService.turndown(
htmlSanitizer.sanitizeHtml(content)
.replace(/&#160;/g, ' '), // Replace non-breaking spaces with classic spaces

View File

@ -7,14 +7,18 @@
</menu-entry>
</div>
<hr>
<menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<span>Add Google Drive workspace</span>
</menu-entry>
<menu-entry @click.native="addCouchdbWorkspace">
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
<span>Add CouchDB workspace</span>
</menu-entry>
<menu-entry @click.native="addGithubWorkspace">
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
<span>Add GitHub workspace</span>
</menu-entry>
<menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<span>Add Google Drive workspace</span>
</menu-entry>
<menu-entry @click.native="manageWorkspaces">
<icon-database slot="icon"></icon-database>
<span>Manage workspaces</span>
@ -40,6 +44,18 @@ export default {
]),
},
methods: {
addCouchdbWorkspace() {
return this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace',
})
.catch(() => {}); // Cancel
},
addGithubWorkspace() {
return this.$store.dispatch('modal/open', {
type: 'githubWorkspace',
})
.catch(() => {}); // Cancel
},
addGoogleDriveWorkspace() {
return googleHelper.addDriveAccount(true)
.then(token => this.$store.dispatch('modal/open', {
@ -48,12 +64,6 @@ export default {
}))
.catch(() => {}); // Cancel
},
addCouchdbWorkspace() {
return this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace',
})
.catch(() => {}); // Cancel
},
manageWorkspaces() {
return this.$store.dispatch('modal/open', 'workspaceManagement');
},

View File

@ -10,9 +10,9 @@
<br>
<a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg">Chrome app</a> <a target="_blank" href="https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha">Chrome extension</a>
<br>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<a target="_blank" href="https://community.stackedit.io/">Community</a> <a target="_blank" href="https://community.stackedit.io/c/how-to">Tutos and How To</a>
<br>
<a target="_blank" href="https://community.stackedit.io/">Community</a>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<div class="modal__info">
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
</div>

View File

@ -54,7 +54,7 @@
</div>
</div>
<div class="modal__error modal__error--file-properties">{{error}}</div>
<div class="modal__info">
<div class="modal__info modal__info--multiline">
<p><strong>ProTip:</strong> You can manually toggle extensions:</p>
<pre class=" language-yaml"><code class="prism language-yaml"><span class="token key atrule">extensions</span><span class="token punctuation">:</span>
<span class="token key atrule">emoji</span><span class="token punctuation">:</span>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="bloggerPage"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
<form-entry label="Blog URL" error="blogUrl">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="blogger"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
<form-entry label="Blog URL" error="blogUrl">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<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>
<p>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" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider>
</div>
<p>This will link your <b>Dropbox</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>Dropbox</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>

View File

@ -4,12 +4,12 @@
<div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
If the file exists, it will be replaced.
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Template">

View File

@ -4,12 +4,12 @@
<div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p>
<p>Save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
If the file exists, it will be replaced.
If the file exists, it will be overwritten.
</div>
</form-entry>
</div>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="gist"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
<p>Publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
<form-entry label="Filename" error="filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
</form-entry>
@ -18,7 +18,7 @@
<form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info">
If the file exists in the Gist, it will be replaced.
If the file exists in the Gist, it will be overwritten.
</div>
</form-entry>
<form-entry label="Template">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="gist"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synchronized.</p>
<p>Save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synchronized.</p>
<form-entry label="Filename" error="filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
</form-entry>
@ -18,7 +18,7 @@
<form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info">
If the file exists in the Gist, it will be replaced.
If the file exists in the Gist, it will be overwritten.
</div>
</form-entry>
</div>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will link your <b>GitHub</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>GitHub</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will open a file from your <b>GitHub</b> repository and keep it synchronized.</p>
<p>Open a file from your <b>GitHub</b> repository and keep it synchronized.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -14,13 +14,13 @@
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the <code>master</code> branch will be used.
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md
<b>Example:</b> path/to/README.md
</div>
</form-entry>
</div>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -14,14 +14,14 @@
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the master branch will be used.
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md<br>
If the file exists, it will be replaced.
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Template">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p>
<p>Save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -14,14 +14,14 @@
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the <code>master</code> branch will be used.
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md<br>
If the file exists, it will be replaced.
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
</div>
@ -49,23 +49,18 @@ export default modalTemplate({
},
methods: {
resolve() {
if (!this.repoUrl) {
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
}
if (!this.path) {
this.setError('path');
}
if (this.repoUrl && this.path) {
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
} else {
// Return new location
if (parsedRepo && this.path) {
const location = githubProvider.makeLocation(
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
this.config.resolve(location);
}
}
},
},
});

View File

@ -0,0 +1,66 @@
<template>
<modal-inner aria-label="Synchronize with GitHub">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>Create a workspace synchronized with a <b>GitHub</b> repository folder.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="Folder path" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the root folder will be used.
</div>
</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 githubProvider from '../../../services/providers/githubProvider';
import utils from '../../../services/utils';
import modalTemplate from '../common/modalTemplate';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
repoUrl: 'githubWorkspaceRepoUrl',
},
methods: {
resolve() {
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
} else {
const path = this.path && this.path.replace(/^\//, '');
const url = utils.addQueryParams('app', {
providerId: 'githubWorkspace',
repo: `${parsedRepo.owner}/${parsedRepo.repo}`,
branch: this.branch || 'master',
path: path || undefined,
}, true);
this.config.resolve();
window.open(url);
}
},
},
});
</script>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will link your <b>Google Drive</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>Google Drive</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
<form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p>
<p>Save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p>
<form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will create a workspace synchronized with a <b>Google Drive</b> folder.</p>
<p>Create a workspace synchronized with a <b>Google Drive</b> folder.</p>
<form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="wordpress"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p>
<form-entry label="Site domain" error="domain">
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider>
</div>
<p>This will link your <b>Zendesk</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>Zendesk</b> account to <b>StackEdit</b>.</p>
<form-entry label="Site URL" error="siteUrl">
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p>
<form-entry label="Section ID" error="sectionId">
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -15,6 +15,7 @@ export default () => ({
dropboxPublishTemplate: 'styledHtml',
githubRepoFullAccess: false,
githubRepoUrl: '',
githubWorkspaceRepoUrl: '',
githubPublishTemplate: 'jekyllSite',
gistIsPublic: false,
gistPublishTemplate: 'plainText',

View File

@ -1,11 +1,11 @@
# light or dark
colorTheme: light
# Auto-sync frequency (in ms). Minimum is 60000.
autoSyncEvery: 60000
# Adjust font size in editor and preview
fontSizeFactor: 1
# Adjust maximum text width in editor and preview
maxWidthFactor: 1
# Auto-sync frequency (in ms). Minimum is 60000.
autoSyncEvery: 60000
# Editor settings
editor:
@ -54,7 +54,7 @@ wkhtmltopdf:
marginRight: 25
marginBottom: 25
marginLeft: 25
# `A3`, `A4`, `Legal` or `Letter`
# A3, A4, Legal or Letter
pageSize: A4
# Options passed to pandoc
@ -77,6 +77,11 @@ turndown:
linkStyle: inlined
linkReferenceStyle: full
github:
createFileMessage: Create {{path}} from https://stackedit.io/
updateFileMessage: Update {{path}} from https://stackedit.io/
deleteFileMessage: Delete {{path}} from https://stackedit.io/
# Default content for new files
newFileContent: |

View File

@ -3,5 +3,6 @@ export default (id = null) => ({
type: 'syncedContent',
historyData: {},
syncHistory: {},
v: 0,
hash: 0,
});

View File

@ -1,6 +1,6 @@
# Welcome to StackEdit!
Hi! I'm your first Markdown file in **StackEdit**. If you want to learn about StackEdit, you can read me. If you want to play with Markdown, you can edit me. If you have finished with me, you can just create new files by opening the **file explorer** on the left corner of the navigation bar.
Hi! I'm your first Markdown file in **StackEdit**. If you want to learn about StackEdit, you can read me. If you want to play with Markdown, you can edit me. Once you have finished with me, you can create new files by opening the **file explorer** on the left corner of the navigation bar.
# Files

View File

@ -16,8 +16,8 @@ export default {
return 'google-drive';
case 'googlePhotos':
return 'google-photos';
case 'dropboxRestricted':
return 'dropbox';
case 'githubWorkspace':
return 'github';
case 'gist':
return 'github';
case 'bloggerPage':

View File

@ -1,8 +1,8 @@
import store from '../../store';
import googleHelper from './helpers/googleHelper';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
export default providerRegistry.register({
export default new Provider({
id: 'bloggerPage',
getToken(location) {
const token = store.getters['data/googleTokens'][location.sub];

View File

@ -1,8 +1,8 @@
import store from '../../store';
import googleHelper from './helpers/googleHelper';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
export default providerRegistry.register({
export default new Provider({
id: 'blogger',
getToken(location) {
const token = store.getters['data/googleTokens'][location.sub];

View File

@ -1,11 +1,20 @@
import emptyContent from '../../data/emptyContent';
import store from '../../store';
import utils from '../utils';
import providerRegistry from './providerRegistry';
import emptyContent from '../../../data/emptyContent';
import utils from '../../utils';
import store from '../../../store';
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
export default {
serializeContent(content) {
export default class Provider {
constructor(props) {
Object.assign(this, props);
providerRegistry.register(this);
}
/**
* Serialize content in a self contain Markdown compatible format
*/
static serializeContent(content) {
let result = content.text;
const data = {};
if (content.properties.length > 1) {
@ -25,8 +34,12 @@ export default {
result += `<!--stackedit_data:\n${serializedData}\n-->`;
}
return result;
},
parseContent(serializedContent, id) {
}
/**
* Parse content serialized with serializeContent()
*/
static parseContent(serializedContent, id) {
const result = utils.deepCopy(store.state.content.itemMap[id]) || emptyContent(id);
result.text = utils.sanitizeText(serializedContent);
result.history = [];
@ -51,15 +64,15 @@ export default {
}
}
return utils.addItemHash(result);
},
}
/**
* Find and open a file location that fits the criteria
* Find and open a file with location that meets the criteria
*/
openFileWithLocation(allLocations, criteria) {
return allLocations.some((location) => {
// If every field fits the criteria
if (Object.entries(criteria).every(([key, value]) => value === location[key])) {
// Found one location that fits, open it if it exists
static openFileWithLocation(allLocations, criteria) {
const location = utils.search(allLocations, criteria);
if (location) {
// Found one, open it if it exists
const file = store.state.file.itemMap[location.fileId];
if (file) {
store.commit('file/setCurrentId', file.id);
@ -70,10 +83,7 @@ export default {
parentId: null,
});
}
return true;
}
}
return false;
});
},
};
}
}

View File

@ -1,7 +1,6 @@
import store from '../../store';
import couchdbHelper from './helpers/couchdbHelper';
import providerRegistry from './providerRegistry';
import providerUtils from './providerUtils';
import Provider from './common/Provider';
import utils from '../utils';
const getSyncData = (fileId) => {
@ -11,18 +10,20 @@ const getSyncData = (fileId) => {
: Promise.reject(); // No need for a proper error message.
};
export default providerRegistry.register({
let syncLastSeq;
export default new Provider({
id: 'couchdbWorkspace',
getToken() {
return store.getters['workspace/syncToken'];
},
initWorkspace() {
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
const workspaceIdParams = {
const workspaceParams = {
providerId: this.id,
dbUrl,
};
const workspaceId = utils.makeWorkspaceId(workspaceIdParams);
const workspaceId = utils.makeWorkspaceId(workspaceParams);
const getToken = () => store.getters['data/couchdbTokens'][workspaceId];
const getWorkspace = () => store.getters['data/sanitizedWorkspaces'][workspaceId];
@ -51,7 +52,7 @@ export default providerRegistry.register({
}))
.then((workspace) => {
// Fix the URL hash
utils.setQueryParams(workspaceIdParams);
utils.setQueryParams(workspaceParams);
if (workspace.url !== location.href) {
store.dispatch('data/patchWorkspaces', {
[workspace.id]: {
@ -86,13 +87,13 @@ export default providerRegistry.register({
change.syncDataId = change.id;
return true;
});
changes.lastSeq = result.lastSeq;
syncLastSeq = result.lastSeq;
return changes;
});
},
setAppliedChanges(changes) {
onChangesApplied() {
store.dispatch('data/patchLocalSettings', {
syncLastSeq: changes.lastSeq,
syncLastSeq,
});
},
saveSimpleItem(item, syncData) {
@ -131,9 +132,9 @@ export default providerRegistry.register({
.then((body) => {
let item;
if (body.item.type === 'content') {
item = providerUtils.parseContent(body.attachments.data, body.item.id);
item = Provider.parseContent(body.attachments.data, body.item.id);
} else {
item = JSON.parse(body.attachments.data);
item = utils.addItemHash(JSON.parse(body.attachments.data));
}
const rev = body._rev; // eslint-disable-line no-underscore-dangle
if (item.hash !== syncData.hash || rev !== syncData.rev) {
@ -149,18 +150,18 @@ export default providerRegistry.register({
});
},
uploadContent(token, content, syncLocation) {
return this.uploadData(content, `${syncLocation.fileId}/content`)
return this.uploadData(content)
.then(() => syncLocation);
},
uploadData(item, dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
uploadData(item) {
const syncData = store.getters['data/syncDataByItemId'][item.id];
if (syncData && syncData.hash === item.hash) {
return Promise.resolve();
}
let data;
let dataType;
if (item.type === 'content') {
data = providerUtils.serializeContent(item);
data = Provider.serializeContent(item);
dataType = 'text/plain';
} else {
data = JSON.stringify(item);
@ -219,6 +220,6 @@ export default providerRegistry.register({
return getSyncData(fileId)
.then(syncData => couchdbHelper
.retrieveDocumentWithAttachments(token, syncData.id, revisionId))
.then(body => providerUtils.parseContent(body.attachments.data, body.item.id));
.then(body => Provider.parseContent(body.attachments.data, body.item.id));
},
});

View File

@ -1,7 +1,6 @@
import store from '../../store';
import dropboxHelper from './helpers/dropboxHelper';
import providerUtils from './providerUtils';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
import utils from '../utils';
const makePathAbsolute = (token, path) => {
@ -17,7 +16,7 @@ const makePathRelative = (token, path) => {
return path;
};
export default providerRegistry.register({
export default new Provider({
id: 'dropbox',
getToken(location) {
return store.getters['data/dropboxTokens'][location.sub];
@ -40,13 +39,13 @@ export default providerRegistry.register({
makePathRelative(token, syncLocation.path),
syncLocation.dropboxFileId,
)
.then(({ content }) => providerUtils.parseContent(content, `${syncLocation.fileId}/content`));
.then(({ content }) => Provider.parseContent(content, `${syncLocation.fileId}/content`));
},
uploadContent(token, content, syncLocation) {
return dropboxHelper.uploadFile(
token,
makePathRelative(token, syncLocation.path),
providerUtils.serializeContent(content),
Provider.serializeContent(content),
syncLocation.dropboxFileId,
)
.then(dropboxFile => ({
@ -74,7 +73,7 @@ export default providerRegistry.register({
if (!path) {
return null;
}
if (providerUtils.openFileWithLocation(store.getters['syncLocation/items'], {
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], {
providerId: this.id,
path,
})) {

View File

@ -1,10 +1,9 @@
import store from '../../store';
import githubHelper from './helpers/githubHelper';
import providerUtils from './providerUtils';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
import utils from '../utils';
export default providerRegistry.register({
export default new Provider({
id: 'gist',
getToken(location) {
return store.getters['data/githubTokens'][location.sub];
@ -18,7 +17,7 @@ export default providerRegistry.register({
},
downloadContent(token, syncLocation) {
return githubHelper.downloadGist(token, syncLocation.gistId, syncLocation.filename)
.then(content => providerUtils.parseContent(content, `${syncLocation.fileId}/content`));
.then(content => Provider.parseContent(content, `${syncLocation.fileId}/content`));
},
uploadContent(token, content, syncLocation) {
const file = store.state.file.itemMap[syncLocation.fileId];
@ -27,7 +26,7 @@ export default providerRegistry.register({
token,
description,
syncLocation.filename,
providerUtils.serializeContent(content),
Provider.serializeContent(content),
syncLocation.isPublic,
syncLocation.gistId,
)

View File

@ -1,12 +1,11 @@
import store from '../../store';
import githubHelper from './helpers/githubHelper';
import providerUtils from './providerUtils';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
import utils from '../utils';
const savedSha = {};
export default providerRegistry.register({
export default new Provider({
id: 'github',
getToken(location) {
return store.getters['data/githubTokens'][location.sub];
@ -24,9 +23,9 @@ export default providerRegistry.register({
)
.then(({ sha, content }) => {
savedSha[syncLocation.id] = sha;
return providerUtils.parseContent(content, `${syncLocation.fileId}/content`);
return Provider.parseContent(content, `${syncLocation.fileId}/content`);
})
.catch(() => null); // Ignore error, without the sha upload is going to fail anyway
.catch(() => null); // Ignore error, upload is going to fail anyway
},
uploadContent(token, content, syncLocation) {
let result = Promise.resolve();
@ -43,7 +42,7 @@ export default providerRegistry.register({
syncLocation.repo,
syncLocation.branch,
syncLocation.path,
providerUtils.serializeContent(content),
Provider.serializeContent(content),
sha,
);
})
@ -69,7 +68,7 @@ export default providerRegistry.register({
openFile(token, syncLocation) {
return Promise.resolve()
.then(() => {
if (providerUtils.openFileWithLocation(store.getters['syncLocation/items'], syncLocation)) {
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], syncLocation)) {
// File exists and has just been opened. Next...
return null;
}
@ -109,7 +108,7 @@ export default providerRegistry.register({
});
},
parseRepoUrl(url) {
const parsedRepo = url.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
const parsedRepo = url && url.match(/([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
return parsedRepo && {
owner: parsedRepo[1],
repo: parsedRepo[2],

View File

@ -0,0 +1,500 @@
import store from '../../store';
import githubHelper from './helpers/githubHelper';
import Provider from './common/Provider';
import utils from '../utils';
import userSvc from '../userSvc';
const getSyncData = (fileId) => {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
return syncData
? Promise.resolve(syncData)
: Promise.reject(); // No need for a proper error message.
};
const getAbsolutePath = syncData =>
(store.getters['workspace/currentWorkspace'].path || '') + syncData.id;
const getWorkspaceWithOwner = () => {
const workspace = store.getters['workspace/currentWorkspace'];
const [owner, repo] = workspace.repo.split('/');
return {
...workspace,
owner,
repo,
};
};
let treeShaMap;
let treeFolderMap;
let treeFileMap;
let treeDataMap;
let treeSyncLocationMap;
let treePublishLocationMap;
const endsWith = (str, suffix) => str.slice(-suffix.length) === suffix;
export default new Provider({
id: 'githubWorkspace',
getToken() {
return store.getters['workspace/syncToken'];
},
initWorkspace() {
const [owner, repo] = (utils.queryParams.repo || '').split('/');
const branch = utils.queryParams.branch;
const workspaceParams = {
providerId: this.id,
repo: `${owner}/${repo}`,
branch,
};
const path = (utils.queryParams.path || '')
.replace(/^\/*/, '') // Remove leading `/`
.replace(/\/*$/, '/'); // Add trailing `/`
if (path !== '/') {
workspaceParams.path = path;
}
const workspaceId = utils.makeWorkspaceId(workspaceParams);
let workspace = store.getters['data/sanitizedWorkspaces'][workspaceId];
return Promise.resolve()
.then(() => {
// See if we already have a token
if (workspace) {
// Token sub is in the workspace
const token = store.getters['data/githubTokens'][workspace.sub];
if (token) {
return token;
}
}
// If no token has been found, popup an authorize window and get one
return store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
});
})
.then((token) => {
if (!workspace) {
const pathEntries = (path || '').split('/');
const name = pathEntries[pathEntries.length - 2] || repo; // path ends with `/`
workspace = {
...workspaceParams,
id: workspaceId,
sub: token.sub,
name,
};
}
// Fix the URL hash
utils.setQueryParams(workspaceParams);
if (workspace.url !== location.href) {
store.dispatch('data/patchWorkspaces', {
[workspaceId]: {
...workspace,
url: location.href,
},
});
}
return store.getters['data/sanitizedWorkspaces'][workspaceId];
});
},
getChanges() {
const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner();
return githubHelper.getHeadTree(syncToken, owner, repo, branch)
.then((tree) => {
const workspacePath = store.getters['workspace/currentWorkspace'].path || '';
const syncDataByPath = store.getters['data/syncData'];
const syncDataByItemId = store.getters['data/syncDataByItemId'];
// Store all blobs sha
treeShaMap = Object.create(null);
// Store interesting paths
treeFolderMap = Object.create(null);
treeFileMap = Object.create(null);
treeDataMap = Object.create(null);
treeSyncLocationMap = Object.create(null);
treePublishLocationMap = Object.create(null);
tree.filter(({ type, path }) => type === 'blob' && path.indexOf(workspacePath) === 0)
.forEach((blobEntry) => {
// Make path relative
const path = blobEntry.path.slice(workspacePath.length);
// Collect blob sha
treeShaMap[path] = blobEntry.sha;
// Collect parents path
let parentPath = '';
path.split('/').slice(0, -1).forEach((folderName) => {
const folderPath = `${parentPath}${folderName}/`;
treeFolderMap[folderPath] = parentPath;
parentPath = folderPath;
});
// Collect file path
if (path.indexOf('.stackedit-data/') === 0) {
treeDataMap[path] = true;
} else if (endsWith(path, '.md')) {
treeFileMap[path] = parentPath;
} else if (endsWith(path, '.sync')) {
treeSyncLocationMap[path] = true;
} else if (endsWith(path, '.publish')) {
treePublishLocationMap[path] = true;
}
});
// Collect changes
const changes = [];
const pathIds = {};
const syncDataToIgnore = Object.create(null);
const getId = (path) => {
const syncData = syncDataByPath[path];
const id = syncData ? syncData.itemId : utils.uid();
pathIds[path] = id;
return id;
};
// Folder creations/updates
// Assume map entries are sorted from top to bottom
Object.entries(treeFolderMap).forEach(([path, parentPath]) => {
const id = getId(path);
const item = utils.addItemHash({
id,
type: 'folder',
name: path.slice(parentPath.length, -1),
parentId: pathIds[parentPath] || null,
});
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
itemId: id,
type: item.type,
hash: item.hash,
},
});
});
// File creations/updates
Object.entries(treeFileMap).forEach(([path, parentPath]) => {
const id = getId(path);
const item = utils.addItemHash({
id,
type: 'file',
name: path.slice(parentPath.length, -'.md'.length),
parentId: pathIds[parentPath] || null,
});
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
itemId: id,
type: item.type,
hash: item.hash,
},
});
// Content creations/updates
const contentSyncData = syncDataByItemId[`${id}/content`];
if (contentSyncData) {
syncDataToIgnore[contentSyncData.id] = true;
}
if (!contentSyncData || contentSyncData.sha !== treeShaMap[path]) {
// Use `/` as a prefix to get a unique syncData id
changes.push({
syncDataId: `/${path}`,
item: {
id: `${id}/content`,
type: 'content',
// Need a truthy value to force saving sync data
hash: 1,
},
syncData: {
id: `/${path}`,
itemId: `${id}/content`,
type: 'content',
// Need a truthy value to force downloading the content
hash: 1,
},
});
}
});
// Data creations/updates
Object.keys(treeDataMap).forEach((path) => {
try {
const [, id] = path.match(/^\.stackedit-data\/([\s\S]+)\.json$/);
pathIds[path] = id;
const syncData = syncDataByItemId[id];
if (syncData) {
syncDataToIgnore[syncData.id] = true;
}
if (!syncData || syncData.sha !== treeShaMap[path]) {
changes.push({
syncDataId: path,
item: {
id,
type: 'data',
// Need a truthy value to force saving sync data
hash: 1,
},
syncData: {
id: path,
itemId: id,
type: 'data',
// Need a truthy value to force downloading the content
hash: 1,
},
});
}
} catch (e) {
// Ignore parsing errors
}
});
// Location creations/updates
[{
type: 'syncLocation',
map: treeSyncLocationMap,
pathMatcher: /^([\s\S]+)\.([\w-]+)\.sync$/,
}, {
type: 'publishLocation',
map: treePublishLocationMap,
pathMatcher: /^([\s\S]+)\.([\w-]+)\.publish$/,
}]
.forEach(({ type, map, pathMatcher }) => Object.keys(map).forEach((path) => {
try {
const [, filePath, data] = path.match(pathMatcher);
// If there is a corresponding md file in the tree
const fileId = pathIds[`${filePath}.md`];
if (fileId) {
const id = getId(path);
const item = utils.addItemHash({
...JSON.parse(utils.decodeBase64(data)),
id,
type,
fileId,
});
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
itemId: id,
type: item.type,
hash: item.hash,
},
});
}
} catch (e) {
// Ignore parsing errors
}
}));
// Deletions
Object.keys(syncDataByPath).forEach((path) => {
if (!pathIds[path] && !syncDataToIgnore[path]) {
changes.push({ syncDataId: path });
}
});
return changes;
});
},
saveSimpleItem(item) {
const path = store.getters.itemPaths[item.fileId || item.id];
return Promise.resolve()
.then(() => {
const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner();
const syncData = {
itemId: item.id,
type: item.type,
hash: item.hash,
};
if (item.type === 'file') {
syncData.id = `${path}.md`;
} else if (item.type === 'folder') {
syncData.id = path;
}
if (syncData.id) {
return syncData;
}
// locations are stored as paths, so we upload an empty file
const data = utils.encodeBase64(utils.serializeObject({
...item,
id: undefined,
type: undefined,
fileId: undefined,
}), true);
const extension = item.type === 'syncLocation' ? 'sync' : 'publish';
syncData.id = `${path}.${data}.${extension}`;
return githubHelper.uploadFile(
syncToken,
owner,
repo,
branch,
getAbsolutePath(syncData),
'',
treeShaMap[syncData.id],
).then(() => syncData);
});
},
removeItem(syncData) {
// Ignore content deletion
if (syncData.type === 'content') {
return Promise.resolve();
}
const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner();
return githubHelper.removeFile(
syncToken,
owner,
repo,
branch,
getAbsolutePath(syncData),
treeShaMap[syncData.id],
);
},
downloadContent(token, syncLocation) {
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId];
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (!syncData || !contentSyncData) {
return Promise.resolve();
}
const { owner, repo, branch } = getWorkspaceWithOwner();
return githubHelper.downloadFile(token, owner, repo, branch, getAbsolutePath(syncData))
.then(({ sha, content }) => {
const item = Provider.parseContent(content, `${syncLocation.fileId}/content`);
if (item.hash !== contentSyncData.hash) {
store.dispatch('data/patchSyncData', {
[contentSyncData.id]: {
...contentSyncData,
hash: item.hash,
sha,
},
});
}
return item;
});
},
downloadData(dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
if (!syncData) {
return Promise.resolve();
}
const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner();
return githubHelper.downloadFile(syncToken, owner, repo, branch, getAbsolutePath(syncData))
.then(({ sha, content }) => {
const item = JSON.parse(content);
if (item.hash !== syncData.hash) {
store.dispatch('data/patchSyncData', {
[syncData.id]: {
...syncData,
hash: item.hash,
sha,
},
});
}
return item;
});
},
uploadContent(token, content, syncLocation) {
const contentSyncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (contentSyncData && contentSyncData.hash === content.hash) {
return Promise.resolve(syncLocation);
}
const syncData = store.getters['data/syncDataByItemId'][syncLocation.fileId];
const { owner, repo, branch } = getWorkspaceWithOwner();
return githubHelper.uploadFile(
token,
owner,
repo,
branch,
getAbsolutePath(syncData),
Provider.serializeContent(content),
treeShaMap[syncData.id],
)
.then((res) => {
const id = `/${syncData.id}`;
store.dispatch('data/patchSyncData', {
[id]: {
// Build sync data
id,
itemId: content.id,
type: content.type,
hash: content.hash,
sha: res.content.sha,
},
});
return syncLocation;
});
},
uploadData(item) {
const oldSyncData = store.getters['data/syncDataByItemId'][item.id];
if (oldSyncData && oldSyncData.hash === item.hash) {
return Promise.resolve();
}
const syncData = {
id: `.stackedit-data/${item.id}.json`,
itemId: item.id,
type: item.type,
hash: item.hash,
};
const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner();
return githubHelper.uploadFile(
syncToken,
owner,
repo,
branch,
getAbsolutePath(syncData),
JSON.stringify(item),
oldSyncData && oldSyncData.sha,
)
.then(res => store.dispatch('data/patchSyncData', {
[syncData.id]: {
...syncData,
sha: res.content.sha,
},
}));
},
onSyncEnd() {
// Clean up
treeShaMap = null;
treeFolderMap = null;
treeFileMap = null;
treeDataMap = null;
treeSyncLocationMap = null;
treePublishLocationMap = null;
},
listRevisions(token, fileId) {
const { owner, repo, branch } = getWorkspaceWithOwner();
return getSyncData(fileId)
.then(syncData => githubHelper.getCommits(token, owner, repo, branch, syncData.id))
.then(entries => entries.map((entry) => {
let user;
if (entry.author && entry.author.login) {
user = entry.author;
} else if (entry.committer && entry.committer.login) {
user = entry.committer;
}
userSvc.addInfo({ id: user.login, name: user.login, imageUrl: user.avatar_url });
const date = (entry.commit.author && entry.commit.author.date)
|| (entry.commit.committer && entry.commit.committer.date);
return {
id: entry.sha,
sub: user.login,
created: date ? new Date(date).getTime() : 1,
};
})
.sort((revision1, revision2) => revision2.created - revision1.created));
},
getRevisionContent(token, fileId, revisionId) {
const { owner, repo } = getWorkspaceWithOwner();
return getSyncData(fileId)
.then(syncData => githubHelper.downloadFile(
token, owner, repo, revisionId, getAbsolutePath(syncData)))
.then(({ content }) => Provider.parseContent(content, `${fileId}/content`));
},
});

View File

@ -1,9 +1,11 @@
import store from '../../store';
import googleHelper from './helpers/googleHelper';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
import utils from '../utils';
export default providerRegistry.register({
let syncStartPageToken;
export default new Provider({
id: 'googleDriveAppData',
getToken() {
return store.getters['workspace/syncToken'];
@ -42,13 +44,13 @@ export default providerRegistry.register({
change.syncDataId = change.fileId;
return true;
});
changes.startPageToken = result.startPageToken;
syncStartPageToken = result.startPageToken;
return changes;
});
},
setAppliedChanges(changes) {
onChangesApplied() {
store.dispatch('data/patchLocalSettings', {
syncStartPageToken: changes.startPageToken,
syncStartPageToken,
});
},
saveSimpleItem(item, syncData, ifNotTooLate) {
@ -83,7 +85,7 @@ export default providerRegistry.register({
const syncToken = store.getters['workspace/syncToken'];
return googleHelper.downloadAppDataFile(syncToken, syncData.id)
.then((data) => {
const item = JSON.parse(data);
const item = utils.addItemHash(JSON.parse(data));
if (item.hash !== syncData.hash) {
store.dispatch('data/patchSyncData', {
[syncData.id]: {
@ -96,11 +98,11 @@ export default providerRegistry.register({
});
},
uploadContent(token, content, syncLocation, ifNotTooLate) {
return this.uploadData(content, `${syncLocation.fileId}/content`, ifNotTooLate)
return this.uploadData(content, ifNotTooLate)
.then(() => syncLocation);
},
uploadData(item, dataId, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
uploadData(item, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][item.id];
if (syncData && syncData.hash === item.hash) {
return Promise.resolve();
}

View File

@ -1,10 +1,9 @@
import store from '../../store';
import googleHelper from './helpers/googleHelper';
import providerUtils from './providerUtils';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
import utils from '../utils';
export default providerRegistry.register({
export default new Provider({
id: 'googleDrive',
getToken(location) {
const token = store.getters['data/googleTokens'][location.sub];
@ -110,7 +109,7 @@ export default providerRegistry.register({
},
downloadContent(token, syncLocation) {
return googleHelper.downloadFile(token, syncLocation.driveFileId)
.then(content => providerUtils.parseContent(content, `${syncLocation.fileId}/content`));
.then(content => Provider.parseContent(content, `${syncLocation.fileId}/content`));
},
uploadContent(token, content, syncLocation, ifNotTooLate) {
const file = store.state.file.itemMap[syncLocation.fileId];
@ -124,7 +123,7 @@ export default providerRegistry.register({
name,
parents,
undefined,
providerUtils.serializeContent(content),
Provider.serializeContent(content),
undefined,
syncLocation.driveFileId,
undefined,
@ -156,7 +155,7 @@ export default providerRegistry.register({
if (!driveFile) {
return null;
}
if (providerUtils.openFileWithLocation(store.getters['syncLocation/items'], {
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], {
providerId: this.id,
driveFileId: driveFile.id,
})) {

View File

@ -1,11 +1,8 @@
import store from '../../store';
import googleHelper from './helpers/googleHelper';
import providerRegistry from './providerRegistry';
import providerUtils from './providerUtils';
import Provider from './common/Provider';
import utils from '../utils';
let fileIdToOpen;
const getSyncData = (fileId) => {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
return syncData
@ -13,19 +10,22 @@ const getSyncData = (fileId) => {
: Promise.reject(); // No need for a proper error message.
};
export default providerRegistry.register({
let fileIdToOpen;
let syncStartPageToken;
export default new Provider({
id: 'googleDriveWorkspace',
getToken() {
return store.getters['workspace/syncToken'];
},
initWorkspace() {
const makeWorkspaceIdParams = folderId => ({
const makeWorkspaceParams = folderId => ({
providerId: this.id,
folderId,
});
const makeWorkspaceId = folderId => folderId && utils.makeWorkspaceId(
makeWorkspaceIdParams(folderId));
makeWorkspaceParams(folderId));
const getWorkspace = folderId =>
store.getters['data/sanitizedWorkspaces'][makeWorkspaceId(folderId)];
@ -155,7 +155,7 @@ export default providerRegistry.register({
}))
.then((workspace) => {
// Fix the URL hash
utils.setQueryParams(makeWorkspaceIdParams(workspace.folderId));
utils.setQueryParams(makeWorkspaceParams(workspace.folderId));
if (workspace.url !== location.href) {
store.dispatch('data/patchWorkspaces', {
[workspace.id]: {
@ -339,13 +339,13 @@ export default providerRegistry.register({
changes.push(contentChange);
}
});
changes.startPageToken = result.startPageToken;
syncStartPageToken = result.startPageToken;
return changes;
});
},
setAppliedChanges(changes) {
onChangesApplied() {
store.dispatch('data/patchLocalSettings', {
syncStartPageToken: changes.startPageToken,
syncStartPageToken,
});
},
saveSimpleItem(item, syncData, ifNotTooLate) {
@ -419,7 +419,7 @@ export default providerRegistry.register({
}
return googleHelper.downloadFile(token, syncData.id)
.then((content) => {
const item = providerUtils.parseContent(content, `${syncLocation.fileId}/content`);
const item = Provider.parseContent(content, `${syncLocation.fileId}/content`);
if (item.hash !== contentSyncData.hash) {
store.dispatch('data/patchSyncData', {
[contentSyncData.id]: {
@ -428,7 +428,7 @@ export default providerRegistry.register({
},
});
}
// Open the file requested by action if it was to synced yet
// Open the file requested by action if it wasn't synced yet
if (fileIdToOpen && fileIdToOpen === syncData.id) {
fileIdToOpen = null;
// Open the file once downloaded content has been stored
@ -474,7 +474,7 @@ export default providerRegistry.register({
undefined,
undefined,
undefined,
providerUtils.serializeContent(content),
Provider.serializeContent(content),
undefined,
syncData.id,
undefined,
@ -494,7 +494,7 @@ export default providerRegistry.register({
id: item.id,
folderId: workspace.folderId,
},
providerUtils.serializeContent(content),
Provider.serializeContent(content),
undefined,
undefined,
undefined,
@ -523,8 +523,8 @@ export default providerRegistry.register({
}))
.then(() => syncLocation);
},
uploadData(item, dataId, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
uploadData(item, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][item.id];
if (syncData && syncData.hash === item.hash) {
return Promise.resolve();
}
@ -570,6 +570,6 @@ export default providerRegistry.register({
getRevisionContent(token, fileId, revisionId) {
return getSyncData(fileId)
.then(syncData => googleHelper.downloadFileRevision(token, syncData.id, revisionId))
.then(content => providerUtils.parseContent(content, `${fileId}/content`));
.then(content => Provider.parseContent(content, `${fileId}/content`));
},
});

View File

@ -17,6 +17,16 @@ const request = (token, options) => networkSvc.request({
},
});
const repoRequest = (token, owner, repo, options) => request(token, {
...options,
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${options.url}`,
});
const getCommitMessage = (name, path) => {
const message = store.getters['data/computedSettings'].github[name];
return message.replace(/{{path}}/g, path);
};
export default {
startOauth2(scopes, sub = null, silent = false) {
return networkSvc.startOauth2(
@ -51,7 +61,7 @@ export default {
const token = {
scopes,
accessToken,
name: res.body.name,
name: res.body.login,
sub: `${res.body.id}`,
repoFullAccess: scopes.indexOf('repo') !== -1,
};
@ -63,21 +73,58 @@ export default {
addAccount(repoFullAccess = false) {
return this.startOauth2(getScopes({ repoFullAccess }));
},
getTree(token, owner, repo, sha) {
return repoRequest(token, owner, repo, {
url: `git/trees/${encodeURIComponent(sha)}?recursive=1`,
})
.then((res) => {
if (res.body.truncated) {
throw new Error('Git tree too big. Please remove some files in the repository.');
}
return res.body.tree;
});
},
getHeadTree(token, owner, repo, branch) {
return repoRequest(token, owner, repo, {
url: `branches/${encodeURIComponent(branch)}`,
})
.then(res => this.getTree(token, owner, repo, res.body.commit.commit.tree.sha));
},
getCommits(token, owner, repo, sha, path) {
return repoRequest(token, owner, repo, {
url: 'commits',
params: { sha, path },
})
.then(res => res.body);
},
uploadFile(token, owner, repo, branch, path, content, sha) {
return request(token, {
return repoRequest(token, owner, repo, {
method: 'PUT',
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`,
url: `contents/${encodeURIComponent(path)}`,
body: {
message: 'Uploaded by https://stackedit.io/',
message: getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path),
content: utils.encodeBase64(content),
sha,
branch,
},
});
})
.then(res => res.body);
},
removeFile(token, owner, repo, branch, path, sha) {
return repoRequest(token, owner, repo, {
method: 'DELETE',
url: `contents/${encodeURIComponent(path)}`,
body: {
message: getCommitMessage('deleteFileMessage', path),
sha,
branch,
},
})
.then(res => res.body);
},
downloadFile(token, owner, repo, branch, path) {
return request(token, {
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`,
return repoRequest(token, owner, repo, {
url: `contents/${encodeURIComponent(path)}`,
params: { ref: branch },
})
.then(res => ({

View File

@ -614,11 +614,8 @@ export default {
break;
}
case 'img': {
let view = new google.picker.PhotosView();
view.setType('flat');
pickerBuilder.addView(view);
view = new google.picker.PhotosView();
view.setType('ofuser');
const view = new google.picker.PhotosView();
view.setType('highlights');
pickerBuilder.addView(view);
pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
break;

View File

@ -1,8 +1,8 @@
import store from '../../store';
import wordpressHelper from './helpers/wordpressHelper';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
export default providerRegistry.register({
export default new Provider({
id: 'wordpress',
getToken(location) {
return store.getters['data/wordpressTokens'][location.sub];

View File

@ -1,8 +1,8 @@
import store from '../../store';
import zendeskHelper from './helpers/zendeskHelper';
import providerRegistry from './providerRegistry';
import Provider from './common/Provider';
export default providerRegistry.register({
export default new Provider({
id: 'zendesk',
getToken(location) {
return store.getters['data/zendeskTokens'][location.sub];

View File

@ -3,7 +3,7 @@ import store from '../store';
import utils from './utils';
import networkSvc from './networkSvc';
import exportSvc from './exportSvc';
import providerRegistry from './providers/providerRegistry';
import providerRegistry from './providers/common/providerRegistry';
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
@ -67,7 +67,7 @@ function publishFile(fileId) {
return loadContent(fileId)
.then(() => {
const publishLocations = [
...store.getters['publishLocation/groupedByFileId'][fileId] || [],
...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [],
];
const publishOneContentLocation = () => {
const publishLocation = publishLocations.shift();

View File

@ -3,17 +3,23 @@ import store from '../store';
import utils from './utils';
import diffUtils from './diffUtils';
import networkSvc from './networkSvc';
import providerRegistry from './providers/providerRegistry';
import providerRegistry from './providers/common/providerRegistry';
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
import './providers/googleDriveWorkspaceProvider';
import './providers/couchdbWorkspaceProvider';
import './providers/githubWorkspaceProvider';
import './providers/googleDriveWorkspaceProvider';
import tempFileSvc from './tempFileSvc';
const minAutoSyncEvery = 60 * 1000; // 60 sec
const inactivityThreshold = 3 * 1000; // 3 sec
const restartSyncAfter = 30 * 1000; // 30 sec
const minAutoSyncEvery = 60 * 1000; // 60 sec
const restartContentSyncAfter = 500; // Restart if an authorize window pops up
const maxContentHistory = 20;
const LAST_SEEN = 0;
const LAST_MERGED = 1;
const LAST_SENT = 2;
let actionProvider;
let workspaceProvider;
@ -69,6 +75,32 @@ function setLastSyncActivity() {
localStorage.setItem(store.getters['workspace/lastSyncActivityKey'], currentDate);
}
/**
* Upgrade hashes if syncedContent is from an old version
*/
function upgradeSyncedContent(syncedContent) {
if (syncedContent.v) {
return syncedContent;
}
const hashUpgrades = {};
const historyData = {};
const syncHistory = {};
Object.entries(syncedContent.historyData).forEach(([hash, content]) => {
const newContent = utils.addItemHash(content);
historyData[newContent.hash] = newContent;
hashUpgrades[hash] = newContent.hash;
});
Object.entries(syncedContent.syncHistory).forEach(([id, hashEntries]) => {
syncHistory[id] = hashEntries.map(hash => hashUpgrades[hash]);
});
return {
...syncedContent,
historyData,
syncHistory,
v: 1,
};
}
/**
* Clean a syncedContent.
*/
@ -103,8 +135,10 @@ function applyChanges(changes) {
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
if (!change.item && existingSyncData) {
// Item was removed
if (syncData[change.syncDataId]) {
delete syncData[change.syncDataId];
saveSyncData = true;
}
if (existingItem) {
// Remove object from the store
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
@ -112,8 +146,10 @@ function applyChanges(changes) {
}
} else if (change.item && change.item.hash) {
// Item was modifed
if ((existingSyncData || {}).hash !== change.syncData.hash) {
syncData[change.syncDataId] = change.syncData;
saveSyncData = true;
}
if (
// If no sync data or existing one is different
(existingSyncData || {}).hash !== change.item.hash
@ -133,10 +169,6 @@ function applyChanges(changes) {
}
}
const LAST_SEEN = 0;
const LAST_MERGED = 1;
const LAST_SENT = 2;
/**
* Create a sync location by uploading the current file content.
*/
@ -157,8 +189,8 @@ function createSyncLocation(syncLocation) {
}, syncLocation)
.then(syncLocationToStore => localDbSvc.loadSyncedContent(fileId)
.then(() => {
const newSyncedContent = utils.deepCopy(
store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
const newSyncedContent = utils.deepCopy(upgradeSyncedContent(
store.state.syncedContent.itemMap[`${fileId}/syncedContent`]));
const newSyncHistoryItem = [];
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
newSyncHistoryItem[LAST_SEEN] = content.hash;
@ -172,12 +204,23 @@ function createSyncLocation(syncLocation) {
});
}
// Prevent from sending new data too long after old data has been fetched
const tooLateChecker = (timeout) => {
const tooLateAfter = Date.now() + timeout;
return cb => (res) => {
if (tooLateAfter < Date.now()) {
throw new Error('TOO_LATE');
}
return cb(res);
};
};
class SyncContext {
restart = false;
attempted = {};
}
/**
/**
* Sync one file with all its locations.
*/
function syncFile(fileId, syncContext = new SyncContext()) {
@ -189,7 +232,8 @@ function syncFile(fileId, syncContext = new SyncContext()) {
.then(() => {
const getFile = () => store.state.file.itemMap[fileId];
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
const getSyncedContent = () => upgradeSyncedContent(
store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
const isTempFile = () => {
@ -206,8 +250,8 @@ function syncFile(fileId, syncContext = new SyncContext()) {
return true;
}
const locations = [
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
...store.getters['publishLocation/groupedByFileId'][fileId] || [],
...store.getters['syncLocation/filteredGroupedByFileId'][fileId] || [],
...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [],
];
if (locations.length) {
// If file has explicit sync/publish locations, it's not a temp file
@ -227,7 +271,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
const attemptedLocations = {};
const syncOneContentLocation = () => {
const syncLocations = [
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
...store.getters['syncLocation/filteredGroupedByFileId'][fileId] || [],
];
if (isWorkspaceSyncPossible()) {
syncLocations.unshift({ id: 'main', providerId: workspaceProvider.id, fileId });
@ -289,7 +333,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
properties: utils.sanitizeText(mergedContent.properties),
discussions: mergedContent.discussions,
comments: mergedContent.comments,
hash: 0,
});
// Retrieve content with new `hash` and freeze it
@ -342,21 +385,11 @@ function syncFile(fileId, syncContext = new SyncContext()) {
return null;
}
// Prevent from sending new content too long after old content has been fetched
const syncStartTime = Date.now();
const ifNotTooLate = cb => (res) => {
// No time to refresh a token...
if (syncStartTime + 500 < Date.now()) {
throw new Error('TOO_LATE');
}
return cb(res);
};
// Upload merged content
return provider.uploadContent(token, {
...mergedContent,
history: mergedContentHistory.slice(0, maxContentHistory),
}, syncLocation, ifNotTooLate)
}, syncLocation, tooLateChecker(restartContentSyncAfter))
.then((syncLocationToStore) => {
// Replace sync location if modified
if (utils.serializeObject(syncLocation) !==
@ -465,13 +498,11 @@ function syncDataItem(dataId) {
if (serverItem && serverItem.hash === mergedItem.hash) {
return null;
}
return workspaceProvider.uploadData(mergedItem, dataId);
return workspaceProvider.uploadData(mergedItem, tooLateChecker(restartContentSyncAfter));
})
.then(() => {
store.dispatch('data/patchDataSyncData', {
.then(() => store.dispatch('data/patchDataSyncData', {
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
});
});
}));
});
}
@ -499,16 +530,12 @@ function syncWorkspace() {
.then((changes) => {
// Apply changes
applyChanges(changes);
workspaceProvider.setAppliedChanges(changes);
if (workspaceProvider.onChangesApplied) {
workspaceProvider.onChangesApplied();
}
// Prevent from sending items too long after changes have been retrieved
const syncStartTime = Date.now();
const ifNotTooLate = cb => (res) => {
if (syncStartTime + restartSyncAfter < Date.now()) {
throw new Error('TOO_LATE');
}
return cb(res);
};
const ifNotTooLate = tooLateChecker(restartSyncAfter);
// Called until no item to save
const saveNextItem = ifNotTooLate(() => {
@ -529,7 +556,8 @@ function syncWorkspace() {
// Add file if content has been added
&& (item.type !== 'file' || syncDataByItemId[`${id}/content`])
) {
promise = workspaceProvider.saveSimpleItem(
promise = workspaceProvider
.saveSimpleItem(
// Use deepCopy to freeze objects
utils.deepCopy(item),
utils.deepCopy(existingSyncData),
@ -612,6 +640,9 @@ function syncWorkspace() {
.then(() => syncNextFile());
};
const onSyncEnd = () => Promise.resolve(
workspaceProvider.onSyncEnd && workspaceProvider.onSyncEnd());
return Promise.resolve()
.then(() => saveNextItem())
.then(() => removeNextItem())
@ -629,6 +660,13 @@ function syncWorkspace() {
}
return syncNextFile();
})
.then(
() => onSyncEnd(),
err => onSyncEnd().then(() => {
throw err;
}, () => {
throw err;
}))
.then(
() => {
if (syncContext.restart) {

View File

@ -4,6 +4,10 @@ import store from '../store';
const promised = {};
export default {
addInfo({ id, name, imageUrl }) {
promised[id] = true;
store.commit('userInfo/addItem', { id, name, imageUrl });
},
getInfo(userId) {
if (!promised[userId]) {
// Try to find a token with this sub

View File

@ -118,6 +118,18 @@ export default {
}, {});
});
},
search(items, criteria) {
let result;
items.some((item) => {
// If every field fits the criteria
if (Object.entries(criteria).every(([key, value]) => value === item[key])) {
result = item;
return true;
}
return false;
});
return result;
},
uid() {
crypto.getRandomValues(array);
return array.cl_map(value => alphabet[value % radix]).join('');
@ -132,26 +144,39 @@ export default {
}
return hash;
},
getItemHash(item) {
return this.hash(this.serializeObject({
...item,
// These properties must not be part of the hash
id: undefined,
hash: undefined,
history: undefined,
}));
},
addItemHash(item) {
return {
...item,
hash: this.hash(this.serializeObject({
...item,
// These properties must not be part of the hash
history: undefined,
hash: undefined,
})),
hash: this.getItemHash(item),
};
},
makeWorkspaceId(params) {
return Math.abs(this.hash(this.serializeObject(params))).toString(36);
},
encodeBase64(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
encodeBase64(str, urlSafe = false) {
const result = btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(`0x${p1}`)));
if (!urlSafe) {
return result;
}
return result
.replace(/\//g, '_') // Replace `/` with `_`
.replace(/\+/g, '-') // Replace `+` with `-`
.replace(/=+$/, ''); // Remove trailing `=`
},
decodeBase64(str) {
return decodeURIComponent(atob(str).split('').map(
// In case of URL safe base64
const sanitizedStr = str.replace(/_/g, '/').replace(/-/g, '+');
return decodeURIComponent(atob(sanitizedStr).split('').map(
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`).join(''));
},
computeProperties(yamlProperties) {
@ -214,6 +239,9 @@ export default {
}
return result;
},
concatPaths(...paths) {
return paths.join('/').replace(/\/+/g, '/');
},
getHostname(url) {
urlParser.href = url;
return urlParser.hostname;
@ -221,7 +249,7 @@ export default {
createHiddenIframe(url) {
const iframeElt = document.createElement('iframe');
iframeElt.style.position = 'absolute';
iframeElt.style.left = '-9999px';
iframeElt.style.left = '-99px';
iframeElt.style.width = '1px';
iframeElt.style.height = '1px';
iframeElt.src = url;

View File

@ -96,8 +96,8 @@ export default {
rootGetters['folder/items'].forEach((item) => {
nodeMap[item.id] = new Node(item, [], true);
});
const syncLocationsByFileId = rootGetters['syncLocation/groupedByFileId'];
const publishLocationsByFileId = rootGetters['publishLocation/groupedByFileId'];
const syncLocationsByFileId = rootGetters['syncLocation/filteredGroupedByFileId'];
const publishLocationsByFileId = rootGetters['publishLocation/filteredGroupedByFileId'];
rootGetters['file/items'].forEach((item) => {
const locations = [
...syncLocationsByFileId[item.id] || [],

View File

@ -14,12 +14,13 @@ import folder from './folder';
import layout from './layout';
import modal from './modal';
import notification from './notification';
import publishLocation from './publishLocation';
import queue from './queue';
import syncedContent from './syncedContent';
import syncLocation from './syncLocation';
import userInfo from './userInfo';
import workspace from './workspace';
import locationTemplate from './locationTemplate';
import emptyPublishLocation from '../data/emptyPublishLocation';
import emptySyncLocation from '../data/emptySyncLocation';
Vue.use(Vuex);
@ -39,10 +40,10 @@ const store = new Vuex.Store({
layout,
modal,
notification,
publishLocation,
publishLocation: locationTemplate(emptyPublishLocation),
queue,
syncedContent,
syncLocation,
syncLocation: locationTemplate(emptySyncLocation),
userInfo,
workspace,
},
@ -59,6 +60,41 @@ const store = new Vuex.Store({
utils.types.forEach(type => Object.assign(result, state[type].itemMap));
return result;
},
itemPaths: (state) => {
const result = {};
const getPath = (item) => {
let itemPath = result[item.id];
if (!itemPath) {
if (item.parendId === 'trash') {
itemPath = `.stackedit-trash/${item.name}`;
} else {
let name = item.name;
if (item.type === 'folder') {
name += '/';
}
const parent = state.folder.itemMap[item.parentId];
if (!parent) {
itemPath = name;
} else {
itemPath = getPath(parent) + name;
}
}
}
result[item.id] = itemPath;
return itemPath;
};
[...state.folder.items, ...state.file.items].forEach(item => getPath(item));
return result;
},
pathItems: (state, getters) => {
const result = {};
const itemPaths = getters.itemPaths;
const allItemMap = getters.allItemMap;
Object.entries(itemPaths).forEach(([id, path]) => {
result[path] = allItemMap[id];
});
return result;
},
isSponsor: (state, getters) => {
const sponsorToken = getters['workspace/sponsorToken'];
return state.light || state.monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);

View File

@ -0,0 +1,43 @@
import moduleTemplate from './moduleTemplate';
import providerRegistry from '../services/providers/common/providerRegistry';
const addToGroup = (groups, item) => {
const list = groups[item.fileId] || [];
list.push(item);
groups[item.fileId] = list;
};
export default (empty) => {
const module = moduleTemplate(empty);
module.getters = {
...module.getters,
groupedByFileId: (state, getters) => {
const groups = {};
getters.items.forEach(item => addToGroup(groups, item));
return groups;
},
filteredGroupedByFileId: (state, getters) => {
const groups = {};
getters.items.filter((item) => {
// Filter items that we can't use
const provider = providerRegistry.providers[item.providerId];
return provider && provider.getToken(item);
}).forEach(item => addToGroup(groups, item));
return groups;
},
current: (state, getters, rootState, rootGetters) => {
const locations = getters.filteredGroupedByFileId[rootGetters['file/current'].id] || [];
return locations.map((location) => {
const provider = providerRegistry.providers[location.providerId];
return {
...location,
description: provider.getDescription(location),
url: provider.getUrl(location),
};
});
},
};
return module;
};

View File

@ -3,10 +3,7 @@ import utils from '../services/utils';
export default (empty, simpleHash = false) => {
// Use Date.now() as a simple hash function, which is ok for not-synced types
const hashFunc = simpleHash ? Date.now : item => utils.hash(utils.serializeObject({
...item,
hash: undefined,
}));
const hashFunc = simpleHash ? Date.now : item => utils.getItemHash(item);
return {
namespaced: true,
@ -19,7 +16,7 @@ export default (empty, simpleHash = false) => {
mutations: {
setItem(state, value) {
const item = Object.assign(empty(value.id), value);
if (!item.hash) {
if (!item.hash || !simpleHash) {
item.hash = hashFunc(item);
}
Vue.set(state.itemMap, item.id, item);

View File

@ -1,35 +0,0 @@
import moduleTemplate from './moduleTemplate';
import empty from '../data/emptyPublishLocation';
import providerRegistry from '../services/providers/providerRegistry';
const module = moduleTemplate(empty);
module.getters = {
...module.getters,
groupedByFileId: (state, getters) => {
const result = {};
getters.items.forEach((item) => {
// Filter items that we can't use
const provider = providerRegistry.providers[item.providerId];
if (provider && provider.getToken(item)) {
const list = result[item.fileId] || [];
list.push(item);
result[item.fileId] = list;
}
});
return result;
},
current: (state, getters, rootState, rootGetters) => {
const locations = getters.groupedByFileId[rootGetters['file/current'].id] || [];
return locations.map((location) => {
const provider = providerRegistry.providers[location.providerId];
return {
...location,
description: provider.getDescription(location),
url: provider.getUrl(location),
};
});
},
};
export default module;

View File

@ -1,35 +0,0 @@
import moduleTemplate from './moduleTemplate';
import empty from '../data/emptySyncLocation';
import providerRegistry from '../services/providers/providerRegistry';
const module = moduleTemplate(empty);
module.getters = {
...module.getters,
groupedByFileId: (state, getters) => {
const result = {};
getters.items.forEach((item) => {
// Filter items that we can't use
const provider = providerRegistry.providers[item.providerId];
if (provider && provider.getToken(item)) {
const list = result[item.fileId] || [];
list.push(item);
result[item.fileId] = list;
}
});
return result;
},
current: (state, getters, rootState, rootGetters) => {
const locations = getters.groupedByFileId[rootGetters['file/current'].id] || [];
return locations.map((location) => {
const provider = providerRegistry.providers[location.providerId];
return {
...location,
description: provider.getDescription(location),
url: provider.getUrl(location),
};
});
},
};
export default module;

View File

@ -36,6 +36,10 @@ export default {
const googleTokens = rootGetters['data/googleTokens'];
return googleTokens[workspace.sub];
}
case 'githubWorkspace': {
const githubTokens = rootGetters['data/githubTokens'];
return githubTokens[workspace.sub];
}
case 'couchdbWorkspace': {
const couchdbTokens = rootGetters['data/couchdbTokens'];
return couchdbTokens[workspace.id];