Github workspace (part 1)
This commit is contained in:
parent
790ac996dd
commit
53ccee0d84
@ -6,11 +6,12 @@
|
|||||||
|
|
||||||
https://stackedit.io/
|
https://stackedit.io/
|
||||||
|
|
||||||
### NEW!!! Embed StackEdit in any website!
|
### Ecosystem
|
||||||
|
|
||||||
See https://github.com/benweet/stackedit.js
|
- [Chrome app](https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg)
|
||||||
|
- NEW! Embed StackEdit in any website with [stackedit.js](https://github.com/benweet/stackedit.js)
|
||||||
Chrome extension: https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha
|
- NEW! [Chrome extension](https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha) that uses stackedit.js
|
||||||
|
- [Community](https://community.stackedit.io/)
|
||||||
|
|
||||||
### Build Setup
|
### Build Setup
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ import DropboxPublishModal from './modals/providers/DropboxPublishModal';
|
|||||||
import GithubAccountModal from './modals/providers/GithubAccountModal';
|
import GithubAccountModal from './modals/providers/GithubAccountModal';
|
||||||
import GithubOpenModal from './modals/providers/GithubOpenModal';
|
import GithubOpenModal from './modals/providers/GithubOpenModal';
|
||||||
import GithubSaveModal from './modals/providers/GithubSaveModal';
|
import GithubSaveModal from './modals/providers/GithubSaveModal';
|
||||||
|
import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
|
||||||
import GithubPublishModal from './modals/providers/GithubPublishModal';
|
import GithubPublishModal from './modals/providers/GithubPublishModal';
|
||||||
import GistSyncModal from './modals/providers/GistSyncModal';
|
import GistSyncModal from './modals/providers/GistSyncModal';
|
||||||
import GistPublishModal from './modals/providers/GistPublishModal';
|
import GistPublishModal from './modals/providers/GistPublishModal';
|
||||||
@ -84,6 +85,7 @@ export default {
|
|||||||
GithubAccountModal,
|
GithubAccountModal,
|
||||||
GithubOpenModal,
|
GithubOpenModal,
|
||||||
GithubSaveModal,
|
GithubSaveModal,
|
||||||
|
GithubWorkspaceModal,
|
||||||
GithubPublishModal,
|
GithubPublishModal,
|
||||||
GistSyncModal,
|
GistSyncModal,
|
||||||
GistPublishModal,
|
GistPublishModal,
|
||||||
@ -176,6 +178,10 @@ export default {
|
|||||||
hr {
|
hr {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__inner-1 {
|
.modal__inner-1 {
|
||||||
@ -221,9 +227,9 @@ export default {
|
|||||||
|
|
||||||
.modal__image {
|
.modal__image {
|
||||||
float: left;
|
float: left;
|
||||||
width: 64px;
|
width: 60px;
|
||||||
height: 64px;
|
height: 60px;
|
||||||
margin: 1.5em 1.5em 0.5em 0;
|
margin: 1.5em 1.2em 0.5em 0;
|
||||||
|
|
||||||
& + *::after {
|
& + *::after {
|
||||||
content: '';
|
content: '';
|
||||||
@ -262,6 +268,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal__info--multiline {
|
||||||
|
padding-top: 0.1em;
|
||||||
|
padding-bottom: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
.modal__button-bar {
|
.modal__button-bar {
|
||||||
margin-top: 1.75rem;
|
margin-top: 1.75rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapMutations, mapGetters } from 'vuex';
|
import { mapMutations, mapGetters } from 'vuex';
|
||||||
import providerRegistry from '../../services/providers/providerRegistry';
|
import providerRegistry from '../../services/providers/common/providerRegistry';
|
||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import UserImage from '../UserImage';
|
import UserImage from '../UserImage';
|
||||||
import UserName from '../UserName';
|
import UserName from '../UserName';
|
||||||
@ -48,7 +48,7 @@ let cachedFileId;
|
|||||||
let revisionsPromise;
|
let revisionsPromise;
|
||||||
let revisionContentPromises;
|
let revisionContentPromises;
|
||||||
const pageSize = 30;
|
const pageSize = 30;
|
||||||
const spacerThreshold = 12 * 60 * 60 * 1000; // 12h
|
const spacerThreshold = 60 * 60 * 1000; // 1h
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
import TurndownService from 'turndown/lib/turndown.browser.umd';
|
import TurndownService from 'turndown/lib/turndown.browser.umd';
|
||||||
import htmlSanitizer from '../../libs/htmlSanitizer';
|
import htmlSanitizer from '../../libs/htmlSanitizer';
|
||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import providerUtils from '../../services/providers/providerUtils';
|
import Provider from '../../services/providers/common/Provider';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
||||||
@ -56,7 +56,7 @@ export default {
|
|||||||
const file = evt.target.files[0];
|
const file = evt.target.files[0];
|
||||||
readFile(file)
|
readFile(file)
|
||||||
.then(content => this.$store.dispatch('createFile', {
|
.then(content => this.$store.dispatch('createFile', {
|
||||||
...providerUtils.parseContent(content),
|
...Provider.parseContent(content),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
})
|
})
|
||||||
.then(item => this.$store.commit('file/setCurrentId', item.id)));
|
.then(item => this.$store.commit('file/setCurrentId', item.id)));
|
||||||
@ -65,7 +65,7 @@ export default {
|
|||||||
const file = evt.target.files[0];
|
const file = evt.target.files[0];
|
||||||
readFile(file)
|
readFile(file)
|
||||||
.then(content => this.$store.dispatch('createFile', {
|
.then(content => this.$store.dispatch('createFile', {
|
||||||
...providerUtils.parseContent(
|
...Provider.parseContent(
|
||||||
turndownService.turndown(
|
turndownService.turndown(
|
||||||
htmlSanitizer.sanitizeHtml(content)
|
htmlSanitizer.sanitizeHtml(content)
|
||||||
.replace(/ /g, ' '), // Replace non-breaking spaces with classic spaces
|
.replace(/ /g, ' '), // Replace non-breaking spaces with classic spaces
|
||||||
|
@ -7,14 +7,18 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<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">
|
<menu-entry @click.native="addCouchdbWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
||||||
<span>Add CouchDB workspace</span>
|
<span>Add CouchDB workspace</span>
|
||||||
</menu-entry>
|
</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">
|
<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>
|
||||||
@ -40,6 +44,18 @@ export default {
|
|||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
addCouchdbWorkspace() {
|
||||||
|
return this.$store.dispatch('modal/open', {
|
||||||
|
type: 'couchdbWorkspace',
|
||||||
|
})
|
||||||
|
.catch(() => {}); // Cancel
|
||||||
|
},
|
||||||
|
addGithubWorkspace() {
|
||||||
|
return this.$store.dispatch('modal/open', {
|
||||||
|
type: 'githubWorkspace',
|
||||||
|
})
|
||||||
|
.catch(() => {}); // Cancel
|
||||||
|
},
|
||||||
addGoogleDriveWorkspace() {
|
addGoogleDriveWorkspace() {
|
||||||
return googleHelper.addDriveAccount(true)
|
return googleHelper.addDriveAccount(true)
|
||||||
.then(token => this.$store.dispatch('modal/open', {
|
.then(token => this.$store.dispatch('modal/open', {
|
||||||
@ -48,12 +64,6 @@ 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');
|
||||||
},
|
},
|
||||||
|
@ -10,9 +10,9 @@
|
|||||||
<br>
|
<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>
|
<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>
|
<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>
|
<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">
|
<div class="modal__info">
|
||||||
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
|
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__error modal__error--file-properties">{{error}}</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>
|
<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>
|
<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>
|
<span class="token key atrule">emoji</span><span class="token punctuation">:</span>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="bloggerPage"></icon-provider>
|
<icon-provider provider-id="bloggerPage"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Blog URL" error="blogUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="blogger"></icon-provider>
|
<icon-provider provider-id="blogger"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Blog URL" error="blogUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="couchdb"></icon-provider>
|
<icon-provider provider-id="couchdb"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Database URL" error="dbUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</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">
|
||||||
<div class="form-entry__checkbox">
|
<div class="form-entry__checkbox">
|
||||||
<label>
|
<label>
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
|
<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>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="Template">
|
<form-entry label="Template">
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
|
<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>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="gist"></icon-provider>
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Filename" error="filename">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<form-entry label="Existing Gist ID" info="optional">
|
<form-entry label="Existing Gist ID" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<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>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="Template">
|
<form-entry label="Template">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="gist"></icon-provider>
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Filename" error="filename">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<form-entry label="Existing Gist ID" info="optional">
|
<form-entry label="Existing Gist ID" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<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>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</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">
|
||||||
<div class="form-entry__checkbox">
|
<div class="form-entry__checkbox">
|
||||||
<label>
|
<label>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -14,13 +14,13 @@
|
|||||||
<form-entry label="Branch" info="optional">
|
<form-entry label="Branch" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<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>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="File path" error="path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> docs/README.md
|
<b>Example:</b> path/to/README.md
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -14,14 +14,14 @@
|
|||||||
<form-entry label="Branch" info="optional">
|
<form-entry label="Branch" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<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>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="File path" error="path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> docs/README.md<br>
|
<b>Example:</b> path/to/README.md<br>
|
||||||
If the file exists, it will be replaced.
|
If the file exists, it will be overwritten.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="Template">
|
<form-entry label="Template">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -14,14 +14,14 @@
|
|||||||
<form-entry label="Branch" info="optional">
|
<form-entry label="Branch" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<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>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="File path" error="path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> docs/README.md<br>
|
<b>Example:</b> path/to/README.md<br>
|
||||||
If the file exists, it will be replaced.
|
If the file exists, it will be overwritten.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
</div>
|
</div>
|
||||||
@ -49,23 +49,18 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (!this.repoUrl) {
|
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
||||||
|
if (!parsedRepo) {
|
||||||
this.setError('repoUrl');
|
this.setError('repoUrl');
|
||||||
}
|
}
|
||||||
if (!this.path) {
|
if (!this.path) {
|
||||||
this.setError('path');
|
this.setError('path');
|
||||||
}
|
}
|
||||||
if (this.repoUrl && this.path) {
|
if (parsedRepo && this.path) {
|
||||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
|
||||||
if (!parsedRepo) {
|
|
||||||
this.setError('repoUrl');
|
|
||||||
} else {
|
|
||||||
// Return new location
|
|
||||||
const location = githubProvider.makeLocation(
|
const location = githubProvider.makeLocation(
|
||||||
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
|
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
|
||||||
this.config.resolve(location);
|
this.config.resolve(location);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
66
src/components/modals/providers/GithubWorkspaceModal.vue
Normal file
66
src/components/modals/providers/GithubWorkspaceModal.vue
Normal 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>
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</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">
|
||||||
<div class="form-entry__checkbox">
|
<div class="form-entry__checkbox">
|
||||||
<label>
|
<label>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Folder ID" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Folder ID" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Folder ID" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="wordpress"></icon-provider>
|
<icon-provider provider-id="wordpress"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Site domain" error="domain">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="zendesk"></icon-provider>
|
<icon-provider provider-id="zendesk"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Site URL" error="siteUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="zendesk"></icon-provider>
|
<icon-provider provider-id="zendesk"></icon-provider>
|
||||||
</div>
|
</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">
|
<form-entry label="Section ID" error="sectionId">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
|
@ -15,6 +15,7 @@ export default () => ({
|
|||||||
dropboxPublishTemplate: 'styledHtml',
|
dropboxPublishTemplate: 'styledHtml',
|
||||||
githubRepoFullAccess: false,
|
githubRepoFullAccess: false,
|
||||||
githubRepoUrl: '',
|
githubRepoUrl: '',
|
||||||
|
githubWorkspaceRepoUrl: '',
|
||||||
githubPublishTemplate: 'jekyllSite',
|
githubPublishTemplate: 'jekyllSite',
|
||||||
gistIsPublic: false,
|
gistIsPublic: false,
|
||||||
gistPublishTemplate: 'plainText',
|
gistPublishTemplate: 'plainText',
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# light or dark
|
# light or dark
|
||||||
colorTheme: light
|
colorTheme: light
|
||||||
# Auto-sync frequency (in ms). Minimum is 60000.
|
|
||||||
autoSyncEvery: 60000
|
|
||||||
# Adjust font size in editor and preview
|
# Adjust font size in editor and preview
|
||||||
fontSizeFactor: 1
|
fontSizeFactor: 1
|
||||||
# Adjust maximum text width in editor and preview
|
# Adjust maximum text width in editor and preview
|
||||||
maxWidthFactor: 1
|
maxWidthFactor: 1
|
||||||
|
# Auto-sync frequency (in ms). Minimum is 60000.
|
||||||
|
autoSyncEvery: 60000
|
||||||
|
|
||||||
# Editor settings
|
# Editor settings
|
||||||
editor:
|
editor:
|
||||||
@ -54,7 +54,7 @@ wkhtmltopdf:
|
|||||||
marginRight: 25
|
marginRight: 25
|
||||||
marginBottom: 25
|
marginBottom: 25
|
||||||
marginLeft: 25
|
marginLeft: 25
|
||||||
# `A3`, `A4`, `Legal` or `Letter`
|
# A3, A4, Legal or Letter
|
||||||
pageSize: A4
|
pageSize: A4
|
||||||
|
|
||||||
# Options passed to pandoc
|
# Options passed to pandoc
|
||||||
@ -77,6 +77,11 @@ turndown:
|
|||||||
linkStyle: inlined
|
linkStyle: inlined
|
||||||
linkReferenceStyle: full
|
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
|
# Default content for new files
|
||||||
newFileContent: |
|
newFileContent: |
|
||||||
|
|
||||||
|
@ -3,5 +3,6 @@ export default (id = null) => ({
|
|||||||
type: 'syncedContent',
|
type: 'syncedContent',
|
||||||
historyData: {},
|
historyData: {},
|
||||||
syncHistory: {},
|
syncHistory: {},
|
||||||
|
v: 0,
|
||||||
hash: 0,
|
hash: 0,
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Welcome to StackEdit!
|
# 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
|
# Files
|
||||||
|
@ -16,8 +16,8 @@ export default {
|
|||||||
return 'google-drive';
|
return 'google-drive';
|
||||||
case 'googlePhotos':
|
case 'googlePhotos':
|
||||||
return 'google-photos';
|
return 'google-photos';
|
||||||
case 'dropboxRestricted':
|
case 'githubWorkspace':
|
||||||
return 'dropbox';
|
return 'github';
|
||||||
case 'gist':
|
case 'gist':
|
||||||
return 'github';
|
return 'github';
|
||||||
case 'bloggerPage':
|
case 'bloggerPage':
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import providerRegistry from './providerRegistry';
|
import Provider from './common/Provider';
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'bloggerPage',
|
id: 'bloggerPage',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
const token = store.getters['data/googleTokens'][location.sub];
|
const token = store.getters['data/googleTokens'][location.sub];
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import providerRegistry from './providerRegistry';
|
import Provider from './common/Provider';
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'blogger',
|
id: 'blogger',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
const token = store.getters['data/googleTokens'][location.sub];
|
const token = store.getters['data/googleTokens'][location.sub];
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
import emptyContent from '../../data/emptyContent';
|
import providerRegistry from './providerRegistry';
|
||||||
import store from '../../store';
|
import emptyContent from '../../../data/emptyContent';
|
||||||
import utils from '../utils';
|
import utils from '../../utils';
|
||||||
|
import store from '../../../store';
|
||||||
|
|
||||||
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
|
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
|
||||||
|
|
||||||
export default {
|
export default class Provider {
|
||||||
serializeContent(content) {
|
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;
|
let result = content.text;
|
||||||
const data = {};
|
const data = {};
|
||||||
if (content.properties.length > 1) {
|
if (content.properties.length > 1) {
|
||||||
@ -25,8 +34,12 @@ export default {
|
|||||||
result += `<!--stackedit_data:\n${serializedData}\n-->`;
|
result += `<!--stackedit_data:\n${serializedData}\n-->`;
|
||||||
}
|
}
|
||||||
return result;
|
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);
|
const result = utils.deepCopy(store.state.content.itemMap[id]) || emptyContent(id);
|
||||||
result.text = utils.sanitizeText(serializedContent);
|
result.text = utils.sanitizeText(serializedContent);
|
||||||
result.history = [];
|
result.history = [];
|
||||||
@ -51,15 +64,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return utils.addItemHash(result);
|
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) {
|
static openFileWithLocation(allLocations, criteria) {
|
||||||
return allLocations.some((location) => {
|
const location = utils.search(allLocations, criteria);
|
||||||
// If every field fits the criteria
|
if (location) {
|
||||||
if (Object.entries(criteria).every(([key, value]) => value === location[key])) {
|
// Found one, open it if it exists
|
||||||
// Found one location that fits, open it if it exists
|
|
||||||
const file = store.state.file.itemMap[location.fileId];
|
const file = store.state.file.itemMap[location.fileId];
|
||||||
if (file) {
|
if (file) {
|
||||||
store.commit('file/setCurrentId', file.id);
|
store.commit('file/setCurrentId', file.id);
|
||||||
@ -70,10 +83,7 @@ export default {
|
|||||||
parentId: null,
|
parentId: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
}
|
||||||
});
|
}
|
||||||
},
|
|
||||||
};
|
|
@ -1,7 +1,6 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import couchdbHelper from './helpers/couchdbHelper';
|
import couchdbHelper from './helpers/couchdbHelper';
|
||||||
import providerRegistry from './providerRegistry';
|
import Provider from './common/Provider';
|
||||||
import providerUtils from './providerUtils';
|
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
const getSyncData = (fileId) => {
|
const getSyncData = (fileId) => {
|
||||||
@ -11,18 +10,20 @@ const getSyncData = (fileId) => {
|
|||||||
: Promise.reject(); // No need for a proper error message.
|
: Promise.reject(); // No need for a proper error message.
|
||||||
};
|
};
|
||||||
|
|
||||||
export default providerRegistry.register({
|
let syncLastSeq;
|
||||||
|
|
||||||
|
export default new Provider({
|
||||||
id: 'couchdbWorkspace',
|
id: 'couchdbWorkspace',
|
||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['workspace/syncToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
initWorkspace() {
|
initWorkspace() {
|
||||||
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
|
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
|
||||||
const workspaceIdParams = {
|
const workspaceParams = {
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
dbUrl,
|
dbUrl,
|
||||||
};
|
};
|
||||||
const workspaceId = utils.makeWorkspaceId(workspaceIdParams);
|
const workspaceId = utils.makeWorkspaceId(workspaceParams);
|
||||||
const getToken = () => store.getters['data/couchdbTokens'][workspaceId];
|
const getToken = () => store.getters['data/couchdbTokens'][workspaceId];
|
||||||
const getWorkspace = () => store.getters['data/sanitizedWorkspaces'][workspaceId];
|
const getWorkspace = () => store.getters['data/sanitizedWorkspaces'][workspaceId];
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ export default providerRegistry.register({
|
|||||||
}))
|
}))
|
||||||
.then((workspace) => {
|
.then((workspace) => {
|
||||||
// Fix the URL hash
|
// Fix the URL hash
|
||||||
utils.setQueryParams(workspaceIdParams);
|
utils.setQueryParams(workspaceParams);
|
||||||
if (workspace.url !== location.href) {
|
if (workspace.url !== location.href) {
|
||||||
store.dispatch('data/patchWorkspaces', {
|
store.dispatch('data/patchWorkspaces', {
|
||||||
[workspace.id]: {
|
[workspace.id]: {
|
||||||
@ -86,13 +87,13 @@ export default providerRegistry.register({
|
|||||||
change.syncDataId = change.id;
|
change.syncDataId = change.id;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
changes.lastSeq = result.lastSeq;
|
syncLastSeq = result.lastSeq;
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setAppliedChanges(changes) {
|
onChangesApplied() {
|
||||||
store.dispatch('data/patchLocalSettings', {
|
store.dispatch('data/patchLocalSettings', {
|
||||||
syncLastSeq: changes.lastSeq,
|
syncLastSeq,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveSimpleItem(item, syncData) {
|
saveSimpleItem(item, syncData) {
|
||||||
@ -131,9 +132,9 @@ export default providerRegistry.register({
|
|||||||
.then((body) => {
|
.then((body) => {
|
||||||
let item;
|
let item;
|
||||||
if (body.item.type === 'content') {
|
if (body.item.type === 'content') {
|
||||||
item = providerUtils.parseContent(body.attachments.data, body.item.id);
|
item = Provider.parseContent(body.attachments.data, body.item.id);
|
||||||
} else {
|
} 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
|
const rev = body._rev; // eslint-disable-line no-underscore-dangle
|
||||||
if (item.hash !== syncData.hash || rev !== syncData.rev) {
|
if (item.hash !== syncData.hash || rev !== syncData.rev) {
|
||||||
@ -149,18 +150,18 @@ export default providerRegistry.register({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadContent(token, content, syncLocation) {
|
uploadContent(token, content, syncLocation) {
|
||||||
return this.uploadData(content, `${syncLocation.fileId}/content`)
|
return this.uploadData(content)
|
||||||
.then(() => syncLocation);
|
.then(() => syncLocation);
|
||||||
},
|
},
|
||||||
uploadData(item, dataId) {
|
uploadData(item) {
|
||||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
const syncData = store.getters['data/syncDataByItemId'][item.id];
|
||||||
if (syncData && syncData.hash === item.hash) {
|
if (syncData && syncData.hash === item.hash) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
let data;
|
let data;
|
||||||
let dataType;
|
let dataType;
|
||||||
if (item.type === 'content') {
|
if (item.type === 'content') {
|
||||||
data = providerUtils.serializeContent(item);
|
data = Provider.serializeContent(item);
|
||||||
dataType = 'text/plain';
|
dataType = 'text/plain';
|
||||||
} else {
|
} else {
|
||||||
data = JSON.stringify(item);
|
data = JSON.stringify(item);
|
||||||
@ -219,6 +220,6 @@ export default providerRegistry.register({
|
|||||||
return getSyncData(fileId)
|
return getSyncData(fileId)
|
||||||
.then(syncData => couchdbHelper
|
.then(syncData => couchdbHelper
|
||||||
.retrieveDocumentWithAttachments(token, syncData.id, revisionId))
|
.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));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import dropboxHelper from './helpers/dropboxHelper';
|
import dropboxHelper from './helpers/dropboxHelper';
|
||||||
import providerUtils from './providerUtils';
|
import Provider from './common/Provider';
|
||||||
import providerRegistry from './providerRegistry';
|
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
const makePathAbsolute = (token, path) => {
|
const makePathAbsolute = (token, path) => {
|
||||||
@ -17,7 +16,7 @@ const makePathRelative = (token, path) => {
|
|||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'dropbox',
|
id: 'dropbox',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
return store.getters['data/dropboxTokens'][location.sub];
|
return store.getters['data/dropboxTokens'][location.sub];
|
||||||
@ -40,13 +39,13 @@ export default providerRegistry.register({
|
|||||||
makePathRelative(token, syncLocation.path),
|
makePathRelative(token, syncLocation.path),
|
||||||
syncLocation.dropboxFileId,
|
syncLocation.dropboxFileId,
|
||||||
)
|
)
|
||||||
.then(({ content }) => providerUtils.parseContent(content, `${syncLocation.fileId}/content`));
|
.then(({ content }) => Provider.parseContent(content, `${syncLocation.fileId}/content`));
|
||||||
},
|
},
|
||||||
uploadContent(token, content, syncLocation) {
|
uploadContent(token, content, syncLocation) {
|
||||||
return dropboxHelper.uploadFile(
|
return dropboxHelper.uploadFile(
|
||||||
token,
|
token,
|
||||||
makePathRelative(token, syncLocation.path),
|
makePathRelative(token, syncLocation.path),
|
||||||
providerUtils.serializeContent(content),
|
Provider.serializeContent(content),
|
||||||
syncLocation.dropboxFileId,
|
syncLocation.dropboxFileId,
|
||||||
)
|
)
|
||||||
.then(dropboxFile => ({
|
.then(dropboxFile => ({
|
||||||
@ -74,7 +73,7 @@ export default providerRegistry.register({
|
|||||||
if (!path) {
|
if (!path) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (providerUtils.openFileWithLocation(store.getters['syncLocation/items'], {
|
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], {
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
path,
|
path,
|
||||||
})) {
|
})) {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import githubHelper from './helpers/githubHelper';
|
import githubHelper from './helpers/githubHelper';
|
||||||
import providerUtils from './providerUtils';
|
import Provider from './common/Provider';
|
||||||
import providerRegistry from './providerRegistry';
|
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'gist',
|
id: 'gist',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
return store.getters['data/githubTokens'][location.sub];
|
return store.getters['data/githubTokens'][location.sub];
|
||||||
@ -18,7 +17,7 @@ export default providerRegistry.register({
|
|||||||
},
|
},
|
||||||
downloadContent(token, syncLocation) {
|
downloadContent(token, syncLocation) {
|
||||||
return githubHelper.downloadGist(token, syncLocation.gistId, syncLocation.filename)
|
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) {
|
uploadContent(token, content, syncLocation) {
|
||||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||||
@ -27,7 +26,7 @@ export default providerRegistry.register({
|
|||||||
token,
|
token,
|
||||||
description,
|
description,
|
||||||
syncLocation.filename,
|
syncLocation.filename,
|
||||||
providerUtils.serializeContent(content),
|
Provider.serializeContent(content),
|
||||||
syncLocation.isPublic,
|
syncLocation.isPublic,
|
||||||
syncLocation.gistId,
|
syncLocation.gistId,
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import githubHelper from './helpers/githubHelper';
|
import githubHelper from './helpers/githubHelper';
|
||||||
import providerUtils from './providerUtils';
|
import Provider from './common/Provider';
|
||||||
import providerRegistry from './providerRegistry';
|
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
const savedSha = {};
|
const savedSha = {};
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'github',
|
id: 'github',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
return store.getters['data/githubTokens'][location.sub];
|
return store.getters['data/githubTokens'][location.sub];
|
||||||
@ -24,9 +23,9 @@ export default providerRegistry.register({
|
|||||||
)
|
)
|
||||||
.then(({ sha, content }) => {
|
.then(({ sha, content }) => {
|
||||||
savedSha[syncLocation.id] = sha;
|
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) {
|
uploadContent(token, content, syncLocation) {
|
||||||
let result = Promise.resolve();
|
let result = Promise.resolve();
|
||||||
@ -43,7 +42,7 @@ export default providerRegistry.register({
|
|||||||
syncLocation.repo,
|
syncLocation.repo,
|
||||||
syncLocation.branch,
|
syncLocation.branch,
|
||||||
syncLocation.path,
|
syncLocation.path,
|
||||||
providerUtils.serializeContent(content),
|
Provider.serializeContent(content),
|
||||||
sha,
|
sha,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -69,7 +68,7 @@ export default providerRegistry.register({
|
|||||||
openFile(token, syncLocation) {
|
openFile(token, syncLocation) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.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...
|
// File exists and has just been opened. Next...
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -109,7 +108,7 @@ export default providerRegistry.register({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
parseRepoUrl(url) {
|
parseRepoUrl(url) {
|
||||||
const parsedRepo = url.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
const parsedRepo = url && url.match(/([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||||
return parsedRepo && {
|
return parsedRepo && {
|
||||||
owner: parsedRepo[1],
|
owner: parsedRepo[1],
|
||||||
repo: parsedRepo[2],
|
repo: parsedRepo[2],
|
||||||
|
500
src/services/providers/githubWorkspaceProvider.js
Normal file
500
src/services/providers/githubWorkspaceProvider.js
Normal 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`));
|
||||||
|
},
|
||||||
|
});
|
@ -1,9 +1,11 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import providerRegistry from './providerRegistry';
|
import Provider from './common/Provider';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
export default providerRegistry.register({
|
let syncStartPageToken;
|
||||||
|
|
||||||
|
export default new Provider({
|
||||||
id: 'googleDriveAppData',
|
id: 'googleDriveAppData',
|
||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['workspace/syncToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
@ -42,13 +44,13 @@ export default providerRegistry.register({
|
|||||||
change.syncDataId = change.fileId;
|
change.syncDataId = change.fileId;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
changes.startPageToken = result.startPageToken;
|
syncStartPageToken = result.startPageToken;
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setAppliedChanges(changes) {
|
onChangesApplied() {
|
||||||
store.dispatch('data/patchLocalSettings', {
|
store.dispatch('data/patchLocalSettings', {
|
||||||
syncStartPageToken: changes.startPageToken,
|
syncStartPageToken,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveSimpleItem(item, syncData, ifNotTooLate) {
|
saveSimpleItem(item, syncData, ifNotTooLate) {
|
||||||
@ -83,7 +85,7 @@ export default providerRegistry.register({
|
|||||||
const syncToken = store.getters['workspace/syncToken'];
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
return googleHelper.downloadAppDataFile(syncToken, syncData.id)
|
return googleHelper.downloadAppDataFile(syncToken, syncData.id)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const item = JSON.parse(data);
|
const item = utils.addItemHash(JSON.parse(data));
|
||||||
if (item.hash !== syncData.hash) {
|
if (item.hash !== syncData.hash) {
|
||||||
store.dispatch('data/patchSyncData', {
|
store.dispatch('data/patchSyncData', {
|
||||||
[syncData.id]: {
|
[syncData.id]: {
|
||||||
@ -96,11 +98,11 @@ export default providerRegistry.register({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||||
return this.uploadData(content, `${syncLocation.fileId}/content`, ifNotTooLate)
|
return this.uploadData(content, ifNotTooLate)
|
||||||
.then(() => syncLocation);
|
.then(() => syncLocation);
|
||||||
},
|
},
|
||||||
uploadData(item, dataId, ifNotTooLate) {
|
uploadData(item, ifNotTooLate) {
|
||||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
const syncData = store.getters['data/syncDataByItemId'][item.id];
|
||||||
if (syncData && syncData.hash === item.hash) {
|
if (syncData && syncData.hash === item.hash) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import providerUtils from './providerUtils';
|
import Provider from './common/Provider';
|
||||||
import providerRegistry from './providerRegistry';
|
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'googleDrive',
|
id: 'googleDrive',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
const token = store.getters['data/googleTokens'][location.sub];
|
const token = store.getters['data/googleTokens'][location.sub];
|
||||||
@ -110,7 +109,7 @@ export default providerRegistry.register({
|
|||||||
},
|
},
|
||||||
downloadContent(token, syncLocation) {
|
downloadContent(token, syncLocation) {
|
||||||
return googleHelper.downloadFile(token, syncLocation.driveFileId)
|
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) {
|
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||||
@ -124,7 +123,7 @@ export default providerRegistry.register({
|
|||||||
name,
|
name,
|
||||||
parents,
|
parents,
|
||||||
undefined,
|
undefined,
|
||||||
providerUtils.serializeContent(content),
|
Provider.serializeContent(content),
|
||||||
undefined,
|
undefined,
|
||||||
syncLocation.driveFileId,
|
syncLocation.driveFileId,
|
||||||
undefined,
|
undefined,
|
||||||
@ -156,7 +155,7 @@ export default providerRegistry.register({
|
|||||||
if (!driveFile) {
|
if (!driveFile) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (providerUtils.openFileWithLocation(store.getters['syncLocation/items'], {
|
if (Provider.openFileWithLocation(store.getters['syncLocation/items'], {
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
driveFileId: driveFile.id,
|
driveFileId: driveFile.id,
|
||||||
})) {
|
})) {
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import providerRegistry from './providerRegistry';
|
import Provider from './common/Provider';
|
||||||
import providerUtils from './providerUtils';
|
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
let fileIdToOpen;
|
|
||||||
|
|
||||||
const getSyncData = (fileId) => {
|
const getSyncData = (fileId) => {
|
||||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
||||||
return syncData
|
return syncData
|
||||||
@ -13,19 +10,22 @@ const getSyncData = (fileId) => {
|
|||||||
: Promise.reject(); // No need for a proper error message.
|
: Promise.reject(); // No need for a proper error message.
|
||||||
};
|
};
|
||||||
|
|
||||||
export default providerRegistry.register({
|
let fileIdToOpen;
|
||||||
|
let syncStartPageToken;
|
||||||
|
|
||||||
|
export default new Provider({
|
||||||
id: 'googleDriveWorkspace',
|
id: 'googleDriveWorkspace',
|
||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['workspace/syncToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
initWorkspace() {
|
initWorkspace() {
|
||||||
const makeWorkspaceIdParams = folderId => ({
|
const makeWorkspaceParams = folderId => ({
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
folderId,
|
folderId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeWorkspaceId = folderId => folderId && utils.makeWorkspaceId(
|
const makeWorkspaceId = folderId => folderId && utils.makeWorkspaceId(
|
||||||
makeWorkspaceIdParams(folderId));
|
makeWorkspaceParams(folderId));
|
||||||
|
|
||||||
const getWorkspace = folderId =>
|
const getWorkspace = folderId =>
|
||||||
store.getters['data/sanitizedWorkspaces'][makeWorkspaceId(folderId)];
|
store.getters['data/sanitizedWorkspaces'][makeWorkspaceId(folderId)];
|
||||||
@ -155,7 +155,7 @@ export default providerRegistry.register({
|
|||||||
}))
|
}))
|
||||||
.then((workspace) => {
|
.then((workspace) => {
|
||||||
// Fix the URL hash
|
// Fix the URL hash
|
||||||
utils.setQueryParams(makeWorkspaceIdParams(workspace.folderId));
|
utils.setQueryParams(makeWorkspaceParams(workspace.folderId));
|
||||||
if (workspace.url !== location.href) {
|
if (workspace.url !== location.href) {
|
||||||
store.dispatch('data/patchWorkspaces', {
|
store.dispatch('data/patchWorkspaces', {
|
||||||
[workspace.id]: {
|
[workspace.id]: {
|
||||||
@ -339,13 +339,13 @@ export default providerRegistry.register({
|
|||||||
changes.push(contentChange);
|
changes.push(contentChange);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
changes.startPageToken = result.startPageToken;
|
syncStartPageToken = result.startPageToken;
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setAppliedChanges(changes) {
|
onChangesApplied() {
|
||||||
store.dispatch('data/patchLocalSettings', {
|
store.dispatch('data/patchLocalSettings', {
|
||||||
syncStartPageToken: changes.startPageToken,
|
syncStartPageToken,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveSimpleItem(item, syncData, ifNotTooLate) {
|
saveSimpleItem(item, syncData, ifNotTooLate) {
|
||||||
@ -419,7 +419,7 @@ export default providerRegistry.register({
|
|||||||
}
|
}
|
||||||
return googleHelper.downloadFile(token, syncData.id)
|
return googleHelper.downloadFile(token, syncData.id)
|
||||||
.then((content) => {
|
.then((content) => {
|
||||||
const item = providerUtils.parseContent(content, `${syncLocation.fileId}/content`);
|
const item = Provider.parseContent(content, `${syncLocation.fileId}/content`);
|
||||||
if (item.hash !== contentSyncData.hash) {
|
if (item.hash !== contentSyncData.hash) {
|
||||||
store.dispatch('data/patchSyncData', {
|
store.dispatch('data/patchSyncData', {
|
||||||
[contentSyncData.id]: {
|
[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) {
|
if (fileIdToOpen && fileIdToOpen === syncData.id) {
|
||||||
fileIdToOpen = null;
|
fileIdToOpen = null;
|
||||||
// Open the file once downloaded content has been stored
|
// Open the file once downloaded content has been stored
|
||||||
@ -474,7 +474,7 @@ export default providerRegistry.register({
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
providerUtils.serializeContent(content),
|
Provider.serializeContent(content),
|
||||||
undefined,
|
undefined,
|
||||||
syncData.id,
|
syncData.id,
|
||||||
undefined,
|
undefined,
|
||||||
@ -494,7 +494,7 @@ export default providerRegistry.register({
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
folderId: workspace.folderId,
|
folderId: workspace.folderId,
|
||||||
},
|
},
|
||||||
providerUtils.serializeContent(content),
|
Provider.serializeContent(content),
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
@ -523,8 +523,8 @@ export default providerRegistry.register({
|
|||||||
}))
|
}))
|
||||||
.then(() => syncLocation);
|
.then(() => syncLocation);
|
||||||
},
|
},
|
||||||
uploadData(item, dataId, ifNotTooLate) {
|
uploadData(item, ifNotTooLate) {
|
||||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
const syncData = store.getters['data/syncDataByItemId'][item.id];
|
||||||
if (syncData && syncData.hash === item.hash) {
|
if (syncData && syncData.hash === item.hash) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -570,6 +570,6 @@ export default providerRegistry.register({
|
|||||||
getRevisionContent(token, fileId, revisionId) {
|
getRevisionContent(token, fileId, revisionId) {
|
||||||
return getSyncData(fileId)
|
return getSyncData(fileId)
|
||||||
.then(syncData => googleHelper.downloadFileRevision(token, syncData.id, revisionId))
|
.then(syncData => googleHelper.downloadFileRevision(token, syncData.id, revisionId))
|
||||||
.then(content => providerUtils.parseContent(content, `${fileId}/content`));
|
.then(content => Provider.parseContent(content, `${fileId}/content`));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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 {
|
export default {
|
||||||
startOauth2(scopes, sub = null, silent = false) {
|
startOauth2(scopes, sub = null, silent = false) {
|
||||||
return networkSvc.startOauth2(
|
return networkSvc.startOauth2(
|
||||||
@ -51,7 +61,7 @@ export default {
|
|||||||
const token = {
|
const token = {
|
||||||
scopes,
|
scopes,
|
||||||
accessToken,
|
accessToken,
|
||||||
name: res.body.name,
|
name: res.body.login,
|
||||||
sub: `${res.body.id}`,
|
sub: `${res.body.id}`,
|
||||||
repoFullAccess: scopes.indexOf('repo') !== -1,
|
repoFullAccess: scopes.indexOf('repo') !== -1,
|
||||||
};
|
};
|
||||||
@ -63,21 +73,58 @@ export default {
|
|||||||
addAccount(repoFullAccess = false) {
|
addAccount(repoFullAccess = false) {
|
||||||
return this.startOauth2(getScopes({ repoFullAccess }));
|
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) {
|
uploadFile(token, owner, repo, branch, path, content, sha) {
|
||||||
return request(token, {
|
return repoRequest(token, owner, repo, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`,
|
url: `contents/${encodeURIComponent(path)}`,
|
||||||
body: {
|
body: {
|
||||||
message: 'Uploaded by https://stackedit.io/',
|
message: getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path),
|
||||||
content: utils.encodeBase64(content),
|
content: utils.encodeBase64(content),
|
||||||
sha,
|
sha,
|
||||||
branch,
|
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) {
|
downloadFile(token, owner, repo, branch, path) {
|
||||||
return request(token, {
|
return repoRequest(token, owner, repo, {
|
||||||
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`,
|
url: `contents/${encodeURIComponent(path)}`,
|
||||||
params: { ref: branch },
|
params: { ref: branch },
|
||||||
})
|
})
|
||||||
.then(res => ({
|
.then(res => ({
|
||||||
|
@ -614,11 +614,8 @@ export default {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'img': {
|
case 'img': {
|
||||||
let view = new google.picker.PhotosView();
|
const view = new google.picker.PhotosView();
|
||||||
view.setType('flat');
|
view.setType('highlights');
|
||||||
pickerBuilder.addView(view);
|
|
||||||
view = new google.picker.PhotosView();
|
|
||||||
view.setType('ofuser');
|
|
||||||
pickerBuilder.addView(view);
|
pickerBuilder.addView(view);
|
||||||
pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
|
pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
|
||||||
break;
|
break;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import wordpressHelper from './helpers/wordpressHelper';
|
import wordpressHelper from './helpers/wordpressHelper';
|
||||||
import providerRegistry from './providerRegistry';
|
import Provider from './common/Provider';
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'wordpress',
|
id: 'wordpress',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
return store.getters['data/wordpressTokens'][location.sub];
|
return store.getters['data/wordpressTokens'][location.sub];
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import zendeskHelper from './helpers/zendeskHelper';
|
import zendeskHelper from './helpers/zendeskHelper';
|
||||||
import providerRegistry from './providerRegistry';
|
import Provider from './common/Provider';
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default new Provider({
|
||||||
id: 'zendesk',
|
id: 'zendesk',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
return store.getters['data/zendeskTokens'][location.sub];
|
return store.getters['data/zendeskTokens'][location.sub];
|
||||||
|
@ -3,7 +3,7 @@ import store from '../store';
|
|||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import networkSvc from './networkSvc';
|
import networkSvc from './networkSvc';
|
||||||
import exportSvc from './exportSvc';
|
import exportSvc from './exportSvc';
|
||||||
import providerRegistry from './providers/providerRegistry';
|
import providerRegistry from './providers/common/providerRegistry';
|
||||||
|
|
||||||
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ function publishFile(fileId) {
|
|||||||
return loadContent(fileId)
|
return loadContent(fileId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const publishLocations = [
|
const publishLocations = [
|
||||||
...store.getters['publishLocation/groupedByFileId'][fileId] || [],
|
...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [],
|
||||||
];
|
];
|
||||||
const publishOneContentLocation = () => {
|
const publishOneContentLocation = () => {
|
||||||
const publishLocation = publishLocations.shift();
|
const publishLocation = publishLocations.shift();
|
||||||
|
@ -3,17 +3,23 @@ import store from '../store';
|
|||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import diffUtils from './diffUtils';
|
import diffUtils from './diffUtils';
|
||||||
import networkSvc from './networkSvc';
|
import networkSvc from './networkSvc';
|
||||||
import providerRegistry from './providers/providerRegistry';
|
import providerRegistry from './providers/common/providerRegistry';
|
||||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||||
import './providers/googleDriveWorkspaceProvider';
|
|
||||||
import './providers/couchdbWorkspaceProvider';
|
import './providers/couchdbWorkspaceProvider';
|
||||||
|
import './providers/githubWorkspaceProvider';
|
||||||
|
import './providers/googleDriveWorkspaceProvider';
|
||||||
import tempFileSvc from './tempFileSvc';
|
import tempFileSvc from './tempFileSvc';
|
||||||
|
|
||||||
|
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||||
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
const restartContentSyncAfter = 500; // Restart if an authorize window pops up
|
||||||
const maxContentHistory = 20;
|
const maxContentHistory = 20;
|
||||||
|
|
||||||
|
const LAST_SEEN = 0;
|
||||||
|
const LAST_MERGED = 1;
|
||||||
|
const LAST_SENT = 2;
|
||||||
|
|
||||||
let actionProvider;
|
let actionProvider;
|
||||||
let workspaceProvider;
|
let workspaceProvider;
|
||||||
|
|
||||||
@ -69,6 +75,32 @@ function setLastSyncActivity() {
|
|||||||
localStorage.setItem(store.getters['workspace/lastSyncActivityKey'], currentDate);
|
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.
|
* Clean a syncedContent.
|
||||||
*/
|
*/
|
||||||
@ -103,8 +135,10 @@ function applyChanges(changes) {
|
|||||||
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
|
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
|
||||||
if (!change.item && existingSyncData) {
|
if (!change.item && existingSyncData) {
|
||||||
// Item was removed
|
// Item was removed
|
||||||
|
if (syncData[change.syncDataId]) {
|
||||||
delete syncData[change.syncDataId];
|
delete syncData[change.syncDataId];
|
||||||
saveSyncData = true;
|
saveSyncData = true;
|
||||||
|
}
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
// Remove object from the store
|
// Remove object from the store
|
||||||
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
|
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
|
||||||
@ -112,8 +146,10 @@ function applyChanges(changes) {
|
|||||||
}
|
}
|
||||||
} else if (change.item && change.item.hash) {
|
} else if (change.item && change.item.hash) {
|
||||||
// Item was modifed
|
// Item was modifed
|
||||||
|
if ((existingSyncData || {}).hash !== change.syncData.hash) {
|
||||||
syncData[change.syncDataId] = change.syncData;
|
syncData[change.syncDataId] = change.syncData;
|
||||||
saveSyncData = true;
|
saveSyncData = true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
// If no sync data or existing one is different
|
// If no sync data or existing one is different
|
||||||
(existingSyncData || {}).hash !== change.item.hash
|
(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.
|
* Create a sync location by uploading the current file content.
|
||||||
*/
|
*/
|
||||||
@ -157,8 +189,8 @@ function createSyncLocation(syncLocation) {
|
|||||||
}, syncLocation)
|
}, syncLocation)
|
||||||
.then(syncLocationToStore => localDbSvc.loadSyncedContent(fileId)
|
.then(syncLocationToStore => localDbSvc.loadSyncedContent(fileId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const newSyncedContent = utils.deepCopy(
|
const newSyncedContent = utils.deepCopy(upgradeSyncedContent(
|
||||||
store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
|
store.state.syncedContent.itemMap[`${fileId}/syncedContent`]));
|
||||||
const newSyncHistoryItem = [];
|
const newSyncHistoryItem = [];
|
||||||
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||||
newSyncHistoryItem[LAST_SEEN] = content.hash;
|
newSyncHistoryItem[LAST_SEEN] = content.hash;
|
||||||
@ -172,6 +204,17 @@ 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 {
|
class SyncContext {
|
||||||
restart = false;
|
restart = false;
|
||||||
attempted = {};
|
attempted = {};
|
||||||
@ -189,7 +232,8 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
const getFile = () => store.state.file.itemMap[fileId];
|
const getFile = () => store.state.file.itemMap[fileId];
|
||||||
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
|
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 getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
||||||
|
|
||||||
const isTempFile = () => {
|
const isTempFile = () => {
|
||||||
@ -206,8 +250,8 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const locations = [
|
const locations = [
|
||||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
...store.getters['syncLocation/filteredGroupedByFileId'][fileId] || [],
|
||||||
...store.getters['publishLocation/groupedByFileId'][fileId] || [],
|
...store.getters['publishLocation/filteredGroupedByFileId'][fileId] || [],
|
||||||
];
|
];
|
||||||
if (locations.length) {
|
if (locations.length) {
|
||||||
// If file has explicit sync/publish locations, it's not a temp file
|
// 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 attemptedLocations = {};
|
||||||
const syncOneContentLocation = () => {
|
const syncOneContentLocation = () => {
|
||||||
const syncLocations = [
|
const syncLocations = [
|
||||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
...store.getters['syncLocation/filteredGroupedByFileId'][fileId] || [],
|
||||||
];
|
];
|
||||||
if (isWorkspaceSyncPossible()) {
|
if (isWorkspaceSyncPossible()) {
|
||||||
syncLocations.unshift({ id: 'main', providerId: workspaceProvider.id, fileId });
|
syncLocations.unshift({ id: 'main', providerId: workspaceProvider.id, fileId });
|
||||||
@ -289,7 +333,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
properties: utils.sanitizeText(mergedContent.properties),
|
properties: utils.sanitizeText(mergedContent.properties),
|
||||||
discussions: mergedContent.discussions,
|
discussions: mergedContent.discussions,
|
||||||
comments: mergedContent.comments,
|
comments: mergedContent.comments,
|
||||||
hash: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Retrieve content with new `hash` and freeze it
|
// Retrieve content with new `hash` and freeze it
|
||||||
@ -342,21 +385,11 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
return null;
|
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
|
// Upload merged content
|
||||||
return provider.uploadContent(token, {
|
return provider.uploadContent(token, {
|
||||||
...mergedContent,
|
...mergedContent,
|
||||||
history: mergedContentHistory.slice(0, maxContentHistory),
|
history: mergedContentHistory.slice(0, maxContentHistory),
|
||||||
}, syncLocation, ifNotTooLate)
|
}, syncLocation, tooLateChecker(restartContentSyncAfter))
|
||||||
.then((syncLocationToStore) => {
|
.then((syncLocationToStore) => {
|
||||||
// Replace sync location if modified
|
// Replace sync location if modified
|
||||||
if (utils.serializeObject(syncLocation) !==
|
if (utils.serializeObject(syncLocation) !==
|
||||||
@ -465,13 +498,11 @@ function syncDataItem(dataId) {
|
|||||||
if (serverItem && serverItem.hash === mergedItem.hash) {
|
if (serverItem && serverItem.hash === mergedItem.hash) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return workspaceProvider.uploadData(mergedItem, dataId);
|
return workspaceProvider.uploadData(mergedItem, tooLateChecker(restartContentSyncAfter));
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => store.dispatch('data/patchDataSyncData', {
|
||||||
store.dispatch('data/patchDataSyncData', {
|
|
||||||
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
|
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,16 +530,12 @@ function syncWorkspace() {
|
|||||||
.then((changes) => {
|
.then((changes) => {
|
||||||
// Apply changes
|
// Apply changes
|
||||||
applyChanges(changes);
|
applyChanges(changes);
|
||||||
workspaceProvider.setAppliedChanges(changes);
|
if (workspaceProvider.onChangesApplied) {
|
||||||
|
workspaceProvider.onChangesApplied();
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent from sending items too long after changes have been retrieved
|
// Prevent from sending items too long after changes have been retrieved
|
||||||
const syncStartTime = Date.now();
|
const ifNotTooLate = tooLateChecker(restartSyncAfter);
|
||||||
const ifNotTooLate = cb => (res) => {
|
|
||||||
if (syncStartTime + restartSyncAfter < Date.now()) {
|
|
||||||
throw new Error('TOO_LATE');
|
|
||||||
}
|
|
||||||
return cb(res);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Called until no item to save
|
// Called until no item to save
|
||||||
const saveNextItem = ifNotTooLate(() => {
|
const saveNextItem = ifNotTooLate(() => {
|
||||||
@ -529,7 +556,8 @@ function syncWorkspace() {
|
|||||||
// Add file if content has been added
|
// Add file if content has been added
|
||||||
&& (item.type !== 'file' || syncDataByItemId[`${id}/content`])
|
&& (item.type !== 'file' || syncDataByItemId[`${id}/content`])
|
||||||
) {
|
) {
|
||||||
promise = workspaceProvider.saveSimpleItem(
|
promise = workspaceProvider
|
||||||
|
.saveSimpleItem(
|
||||||
// Use deepCopy to freeze objects
|
// Use deepCopy to freeze objects
|
||||||
utils.deepCopy(item),
|
utils.deepCopy(item),
|
||||||
utils.deepCopy(existingSyncData),
|
utils.deepCopy(existingSyncData),
|
||||||
@ -612,6 +640,9 @@ function syncWorkspace() {
|
|||||||
.then(() => syncNextFile());
|
.then(() => syncNextFile());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSyncEnd = () => Promise.resolve(
|
||||||
|
workspaceProvider.onSyncEnd && workspaceProvider.onSyncEnd());
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => saveNextItem())
|
.then(() => saveNextItem())
|
||||||
.then(() => removeNextItem())
|
.then(() => removeNextItem())
|
||||||
@ -629,6 +660,13 @@ function syncWorkspace() {
|
|||||||
}
|
}
|
||||||
return syncNextFile();
|
return syncNextFile();
|
||||||
})
|
})
|
||||||
|
.then(
|
||||||
|
() => onSyncEnd(),
|
||||||
|
err => onSyncEnd().then(() => {
|
||||||
|
throw err;
|
||||||
|
}, () => {
|
||||||
|
throw err;
|
||||||
|
}))
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
if (syncContext.restart) {
|
if (syncContext.restart) {
|
||||||
|
@ -4,6 +4,10 @@ import store from '../store';
|
|||||||
const promised = {};
|
const promised = {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
addInfo({ id, name, imageUrl }) {
|
||||||
|
promised[id] = true;
|
||||||
|
store.commit('userInfo/addItem', { id, name, imageUrl });
|
||||||
|
},
|
||||||
getInfo(userId) {
|
getInfo(userId) {
|
||||||
if (!promised[userId]) {
|
if (!promised[userId]) {
|
||||||
// Try to find a token with this sub
|
// Try to find a token with this sub
|
||||||
|
@ -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() {
|
uid() {
|
||||||
crypto.getRandomValues(array);
|
crypto.getRandomValues(array);
|
||||||
return array.cl_map(value => alphabet[value % radix]).join('');
|
return array.cl_map(value => alphabet[value % radix]).join('');
|
||||||
@ -132,26 +144,39 @@ export default {
|
|||||||
}
|
}
|
||||||
return hash;
|
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) {
|
addItemHash(item) {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
hash: this.hash(this.serializeObject({
|
hash: this.getItemHash(item),
|
||||||
...item,
|
|
||||||
// These properties must not be part of the hash
|
|
||||||
history: undefined,
|
|
||||||
hash: undefined,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
makeWorkspaceId(params) {
|
makeWorkspaceId(params) {
|
||||||
return Math.abs(this.hash(this.serializeObject(params))).toString(36);
|
return Math.abs(this.hash(this.serializeObject(params))).toString(36);
|
||||||
},
|
},
|
||||||
encodeBase64(str) {
|
encodeBase64(str, urlSafe = false) {
|
||||||
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
const result = btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
||||||
(match, p1) => String.fromCharCode(`0x${p1}`)));
|
(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) {
|
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(''));
|
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`).join(''));
|
||||||
},
|
},
|
||||||
computeProperties(yamlProperties) {
|
computeProperties(yamlProperties) {
|
||||||
@ -214,6 +239,9 @@ export default {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
concatPaths(...paths) {
|
||||||
|
return paths.join('/').replace(/\/+/g, '/');
|
||||||
|
},
|
||||||
getHostname(url) {
|
getHostname(url) {
|
||||||
urlParser.href = url;
|
urlParser.href = url;
|
||||||
return urlParser.hostname;
|
return urlParser.hostname;
|
||||||
@ -221,7 +249,7 @@ export default {
|
|||||||
createHiddenIframe(url) {
|
createHiddenIframe(url) {
|
||||||
const iframeElt = document.createElement('iframe');
|
const iframeElt = document.createElement('iframe');
|
||||||
iframeElt.style.position = 'absolute';
|
iframeElt.style.position = 'absolute';
|
||||||
iframeElt.style.left = '-9999px';
|
iframeElt.style.left = '-99px';
|
||||||
iframeElt.style.width = '1px';
|
iframeElt.style.width = '1px';
|
||||||
iframeElt.style.height = '1px';
|
iframeElt.style.height = '1px';
|
||||||
iframeElt.src = url;
|
iframeElt.src = url;
|
||||||
|
@ -96,8 +96,8 @@ export default {
|
|||||||
rootGetters['folder/items'].forEach((item) => {
|
rootGetters['folder/items'].forEach((item) => {
|
||||||
nodeMap[item.id] = new Node(item, [], true);
|
nodeMap[item.id] = new Node(item, [], true);
|
||||||
});
|
});
|
||||||
const syncLocationsByFileId = rootGetters['syncLocation/groupedByFileId'];
|
const syncLocationsByFileId = rootGetters['syncLocation/filteredGroupedByFileId'];
|
||||||
const publishLocationsByFileId = rootGetters['publishLocation/groupedByFileId'];
|
const publishLocationsByFileId = rootGetters['publishLocation/filteredGroupedByFileId'];
|
||||||
rootGetters['file/items'].forEach((item) => {
|
rootGetters['file/items'].forEach((item) => {
|
||||||
const locations = [
|
const locations = [
|
||||||
...syncLocationsByFileId[item.id] || [],
|
...syncLocationsByFileId[item.id] || [],
|
||||||
|
@ -14,12 +14,13 @@ import folder from './folder';
|
|||||||
import layout from './layout';
|
import layout from './layout';
|
||||||
import modal from './modal';
|
import modal from './modal';
|
||||||
import notification from './notification';
|
import notification from './notification';
|
||||||
import publishLocation from './publishLocation';
|
|
||||||
import queue from './queue';
|
import queue from './queue';
|
||||||
import syncedContent from './syncedContent';
|
import syncedContent from './syncedContent';
|
||||||
import syncLocation from './syncLocation';
|
|
||||||
import userInfo from './userInfo';
|
import userInfo from './userInfo';
|
||||||
import workspace from './workspace';
|
import workspace from './workspace';
|
||||||
|
import locationTemplate from './locationTemplate';
|
||||||
|
import emptyPublishLocation from '../data/emptyPublishLocation';
|
||||||
|
import emptySyncLocation from '../data/emptySyncLocation';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
@ -39,10 +40,10 @@ const store = new Vuex.Store({
|
|||||||
layout,
|
layout,
|
||||||
modal,
|
modal,
|
||||||
notification,
|
notification,
|
||||||
publishLocation,
|
publishLocation: locationTemplate(emptyPublishLocation),
|
||||||
queue,
|
queue,
|
||||||
syncedContent,
|
syncedContent,
|
||||||
syncLocation,
|
syncLocation: locationTemplate(emptySyncLocation),
|
||||||
userInfo,
|
userInfo,
|
||||||
workspace,
|
workspace,
|
||||||
},
|
},
|
||||||
@ -59,6 +60,41 @@ const store = new Vuex.Store({
|
|||||||
utils.types.forEach(type => Object.assign(result, state[type].itemMap));
|
utils.types.forEach(type => Object.assign(result, state[type].itemMap));
|
||||||
return result;
|
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) => {
|
isSponsor: (state, getters) => {
|
||||||
const sponsorToken = getters['workspace/sponsorToken'];
|
const sponsorToken = getters['workspace/sponsorToken'];
|
||||||
return state.light || state.monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);
|
return state.light || state.monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);
|
||||||
|
43
src/store/locationTemplate.js
Normal file
43
src/store/locationTemplate.js
Normal 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;
|
||||||
|
};
|
@ -3,10 +3,7 @@ import utils from '../services/utils';
|
|||||||
|
|
||||||
export default (empty, simpleHash = false) => {
|
export default (empty, simpleHash = false) => {
|
||||||
// Use Date.now() as a simple hash function, which is ok for not-synced types
|
// 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({
|
const hashFunc = simpleHash ? Date.now : item => utils.getItemHash(item);
|
||||||
...item,
|
|
||||||
hash: undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -19,7 +16,7 @@ export default (empty, simpleHash = false) => {
|
|||||||
mutations: {
|
mutations: {
|
||||||
setItem(state, value) {
|
setItem(state, value) {
|
||||||
const item = Object.assign(empty(value.id), value);
|
const item = Object.assign(empty(value.id), value);
|
||||||
if (!item.hash) {
|
if (!item.hash || !simpleHash) {
|
||||||
item.hash = hashFunc(item);
|
item.hash = hashFunc(item);
|
||||||
}
|
}
|
||||||
Vue.set(state.itemMap, item.id, item);
|
Vue.set(state.itemMap, item.id, item);
|
||||||
|
@ -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;
|
|
@ -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;
|
|
@ -36,6 +36,10 @@ export default {
|
|||||||
const googleTokens = rootGetters['data/googleTokens'];
|
const googleTokens = rootGetters['data/googleTokens'];
|
||||||
return googleTokens[workspace.sub];
|
return googleTokens[workspace.sub];
|
||||||
}
|
}
|
||||||
|
case 'githubWorkspace': {
|
||||||
|
const githubTokens = rootGetters['data/githubTokens'];
|
||||||
|
return githubTokens[workspace.sub];
|
||||||
|
}
|
||||||
case 'couchdbWorkspace': {
|
case 'couchdbWorkspace': {
|
||||||
const couchdbTokens = rootGetters['data/couchdbTokens'];
|
const couchdbTokens = rootGetters['data/couchdbTokens'];
|
||||||
return couchdbTokens[workspace.id];
|
return couchdbTokens[workspace.id];
|
||||||
|
Loading…
Reference in New Issue
Block a user