Sync refactoring
This commit is contained in:
parent
a1673d3e87
commit
987d66ef26
@ -353,7 +353,7 @@ export default {
|
||||
.find-replace__find-stats {
|
||||
text-align: right;
|
||||
font-size: 0.75em;
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.find-replace-highlighting {
|
||||
|
@ -116,6 +116,11 @@ export default {
|
||||
hr + hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.textfield {
|
||||
font-size: 14px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-bar__inner {
|
||||
|
@ -2,12 +2,12 @@
|
||||
<div class="tour" @keydown.esc="skip">
|
||||
<div class="tour-step" :class="'tour-step--' + step" :style="stepStyle">
|
||||
<div class="tour-step__inner" v-if="step === 'welcome'">
|
||||
<h2>Welcome to StackEdit!</h2>
|
||||
<p>Greater, lighter, faster... <b>StackEdit 5</b> is here!</p>
|
||||
<h2>Welcome back!</h2>
|
||||
<p>The new <b>StackEdit 5</b> is here!</p>
|
||||
<p>Please click <b>Next</b> to take a quick tour.</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">Skip</button>
|
||||
<button class="button" @click="next">Next</button>
|
||||
<button class="button button--resolve" @click="next">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'editor'">
|
||||
@ -16,7 +16,7 @@
|
||||
<p>Click <icon-side-preview></icon-side-preview> to toggle the side preview.</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">Skip</button>
|
||||
<button class="button" @click="next">Next</button>
|
||||
<button class="button button--resolve" @click="next">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'explorer'">
|
||||
@ -25,7 +25,7 @@
|
||||
<p>Click <icon-folder></icon-folder> to open the file explorer.</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">Skip</button>
|
||||
<button class="button" @click="next">Next</button>
|
||||
<button class="button button--resolve" @click="next">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'menu'">
|
||||
@ -34,7 +34,7 @@
|
||||
<p>Click <icon-provider provider-id="stackedit"></icon-provider> to explore the menu.</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">Skip</button>
|
||||
<button class="button" @click="next">Next</button>
|
||||
<button class="button button--resolve" @click="next">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'end'">
|
||||
@ -42,7 +42,7 @@
|
||||
<p>If you like StackEdit, please rate 5 stars on the <a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg/reviews">Chrome Web Store</a>.</p>
|
||||
<p>You can also star the project on <a target="_blank" href="https://github.com/benweet/stackedit">GitHub</a> and join the <a target="_blank" href="https://community.stackedit.io/">community</a>.</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">Ok</button>
|
||||
<button class="button button--resolve" @click="finish">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -139,12 +139,12 @@ export default {
|
||||
}
|
||||
|
||||
$tour-step-background: mix(#f3f3f3, $selection-highlighting-color, 75%);
|
||||
$tour-step-width: 220px;
|
||||
$tour-step-width: 240px;
|
||||
|
||||
.tour-step__inner {
|
||||
position: absolute;
|
||||
background-color: $tour-step-background;
|
||||
padding: 1.5em 1em 1em;
|
||||
padding: 1em;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.33;
|
||||
width: $tour-step-width;
|
||||
@ -213,6 +213,11 @@ $tour-step-width: 220px;
|
||||
}
|
||||
|
||||
.tour-step__button-bar {
|
||||
margin-top: 1.75em;
|
||||
text-align: right;
|
||||
|
||||
.button {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -10,13 +10,11 @@ export default {
|
||||
props: ['userId'],
|
||||
computed: {
|
||||
url() {
|
||||
userSvc.getInfo(this.userId);
|
||||
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
|
||||
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
userSvc.getInfo(this.userId);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -9,12 +9,10 @@ export default {
|
||||
props: ['userId'],
|
||||
computed: {
|
||||
name() {
|
||||
userSvc.getInfo(this.userId);
|
||||
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
|
||||
return userInfo ? userInfo.name : 'Someone';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
userSvc.getInfo(this.userId);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,26 +1,41 @@
|
||||
<template>
|
||||
<div class="history side-bar__panel">
|
||||
<div class="side-bar__info" v-if="!syncToken">
|
||||
<p>You have to <a href="javascript:void(0)" @click="signin">sign in with Google</a> to enable revision history.</p>
|
||||
<p><b>Note:</b> This will sync your main workspace.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="loading">
|
||||
<p>Loading history…</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-else-if="!revisionsWithSpacer.length">
|
||||
<p><b>{{currentFileName}}</b> has no history.</p>
|
||||
</div>
|
||||
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
|
||||
<div class="history__spacer" v-if="revision.spacer"></div>
|
||||
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
|
||||
<div class="revision__icon">
|
||||
<user-image :user-id="revision.sub"></user-image>
|
||||
<div class="history side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info">
|
||||
<p v-if="syncLocations.length > 1">
|
||||
<select slot="field" class="textfield" v-model="syncLocationId" @keydown.enter="resolve()">
|
||||
<option v-for="location in syncLocations" :key="location.id" :value="location.id">
|
||||
{{ location.description }}
|
||||
</option>
|
||||
</select>
|
||||
</p>
|
||||
<p v-if="!historyContext">Synchronize <b>{{currentFileName}}</b> to enable revision history or <a href="javascript:void(0)" @click="signin">sign in with Google</a> to synchronize your main workspace.</p>
|
||||
<p v-else-if="loading">Loading history…</p>
|
||||
<p v-else-if="!revisionsWithSpacer.length"><b>{{currentFileName}}</b> has no history.</p>
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
|
||||
<div class="menu-entry__icon menu-entry__icon--image">
|
||||
<icon-provider :provider-id="syncLocation.providerId"></icon-provider>
|
||||
</div>
|
||||
<div class="revision__header flex flex--column">
|
||||
<user-name :user-id="revision.sub"></user-name>
|
||||
<div class="revision__created">{{revision.created | formatTime}}</div>
|
||||
</div>
|
||||
</a>
|
||||
<span v-if="syncLocation.url">
|
||||
The following revisions are stored in <a :href="syncLocation.url" target="_blank">{{ syncLocationProviderName }}</a>.
|
||||
</span>
|
||||
<span v-else>
|
||||
The following revisions are stored in {{ syncLocationProviderName }}.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
|
||||
<div class="history__spacer" v-if="revision.spacer"></div>
|
||||
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
|
||||
<div class="revision__icon">
|
||||
<user-image :user-id="revision.sub"></user-image>
|
||||
</div>
|
||||
<div class="revision__header flex flex--column">
|
||||
<user-name :user-id="revision.sub"></user-name>
|
||||
<div class="revision__created">{{revision.created | formatTime}}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history__spacer history__spacer--last" v-if="revisions.length"></div>
|
||||
<div class="flex flex--row flex--end" v-if="showMoreButton">
|
||||
@ -30,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapGetters } from 'vuex';
|
||||
import { mapState, mapMutations, mapGetters } from 'vuex';
|
||||
import providerRegistry from '../../services/providers/common/providerRegistry';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import UserImage from '../UserImage';
|
||||
@ -44,7 +59,7 @@ import syncSvc from '../../services/syncSvc';
|
||||
let editorClassAppliers = [];
|
||||
let previewClassAppliers = [];
|
||||
|
||||
let cachedFileId;
|
||||
let cachedHistoryContextHash;
|
||||
let revisionsPromise;
|
||||
let revisionContentPromises;
|
||||
const pageSize = 30;
|
||||
@ -60,16 +75,73 @@ export default {
|
||||
allRevisions: [],
|
||||
loading: false,
|
||||
showCount: pageSize,
|
||||
syncLocationId: null,
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('workspace', [
|
||||
'syncToken',
|
||||
...mapGetters('data', [
|
||||
'syncDataByItemId',
|
||||
]),
|
||||
...mapGetters('syncLocation', {
|
||||
syncLocations: 'currentWithWorkspaceSyncLocation',
|
||||
}),
|
||||
...mapState('content', [
|
||||
'revisionContent',
|
||||
]),
|
||||
syncLocation() {
|
||||
return utils.someResult(this.syncLocations, (syncLocation) => {
|
||||
if (syncLocation.id === this.syncLocationId) {
|
||||
return syncLocation;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
syncLocationProviderName() {
|
||||
if (!this.syncLocation) {
|
||||
return null;
|
||||
}
|
||||
return providerRegistry.providersById[this.syncLocation.providerId].name;
|
||||
},
|
||||
currentFileName() {
|
||||
return this.$store.getters['file/current'].name;
|
||||
},
|
||||
historyContext() {
|
||||
const { syncLocation } = this;
|
||||
if (syncLocation) {
|
||||
const provider = providerRegistry.providersById[syncLocation.providerId];
|
||||
const token = provider.getToken(syncLocation);
|
||||
const fileId = this.$store.getters['file/current'].id;
|
||||
const contentId = `${fileId}/content`;
|
||||
const historyContext = {
|
||||
token,
|
||||
fileId,
|
||||
contentId,
|
||||
syncLocation: this.syncLocation,
|
||||
};
|
||||
if (syncLocation.id !== 'main') {
|
||||
return historyContext;
|
||||
}
|
||||
|
||||
// Add syncData for workspace sync location
|
||||
const { syncDataByItemId } = this;
|
||||
const fileSyncData = syncDataByItemId[fileId];
|
||||
const contentSyncData = syncDataByItemId[contentId];
|
||||
if (fileSyncData && contentSyncData) {
|
||||
return {
|
||||
...historyContext,
|
||||
fileSyncData,
|
||||
contentSyncData,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
historyContextHash() {
|
||||
return utils.serializeObject(this.historyContext);
|
||||
},
|
||||
revisions() {
|
||||
return this.allRevisions.slice(0, this.showCount);
|
||||
return this.allRevisions.slice()
|
||||
.sort((revision1, revision2) => revision2.created - revision1.created)
|
||||
.slice(0, this.showCount);
|
||||
},
|
||||
revisionsWithSpacer() {
|
||||
let previousCreated = 0;
|
||||
@ -85,12 +157,6 @@ export default {
|
||||
showMoreButton() {
|
||||
return this.showCount < this.allRevisions.length;
|
||||
},
|
||||
refreshTrigger() {
|
||||
return utils.serializeObject([
|
||||
this.$store.getters['file/current'].id,
|
||||
this.syncToken,
|
||||
]);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('content', [
|
||||
@ -113,32 +179,31 @@ export default {
|
||||
open(revision) {
|
||||
let revisionContentPromise = revisionContentPromises[revision.id];
|
||||
if (!revisionContentPromise) {
|
||||
revisionContentPromise = new Promise((resolve, reject) => {
|
||||
const { syncToken } = this;
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
this.$store.dispatch(
|
||||
const historyContext = utils.deepCopy(this.historyContext);
|
||||
if (historyContext) {
|
||||
const provider = providerRegistry.providersById[this.syncLocation.providerId];
|
||||
revisionContentPromise = new Promise((resolve, reject) => this.$store.dispatch(
|
||||
'queue/enqueue',
|
||||
async () => {
|
||||
try {
|
||||
const content = await this.workspaceProvider
|
||||
.getRevisionContent(syncToken, currentFile.id, revision.id);
|
||||
resolve(content);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
revisionContentPromises[revision.id] = revisionContentPromise;
|
||||
revisionContentPromise.catch(() => {
|
||||
revisionContentPromises[revision.id] = null;
|
||||
});
|
||||
() => provider.getFileRevisionContent({
|
||||
...historyContext,
|
||||
revisionId: revision.id,
|
||||
})
|
||||
.then(resolve, reject),
|
||||
));
|
||||
revisionContentPromises[revision.id] = revisionContentPromise;
|
||||
revisionContentPromise.catch((err) => {
|
||||
this.$store.dispatch('notification/error', err);
|
||||
revisionContentPromises[revision.id] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (revisionContentPromise) {
|
||||
revisionContentPromise.then(revisionContent =>
|
||||
this.$store.dispatch('content/setRevisionContent', revisionContent));
|
||||
}
|
||||
revisionContentPromise.then(revisionContent =>
|
||||
this.$store.dispatch('content/setRevisionContent', revisionContent));
|
||||
},
|
||||
refreshHighlighters() {
|
||||
const { revisionContent } = this.$store.state.content;
|
||||
const { revisionContent } = this;
|
||||
editorClassAppliers.forEach(editorClassApplier => editorClassApplier.stop());
|
||||
editorClassAppliers = [];
|
||||
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
|
||||
@ -166,40 +231,40 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Find the workspace provider
|
||||
const workspace = this.$store.getters['workspace/currentWorkspace'];
|
||||
this.workspaceProvider = providerRegistry.providersById[workspace.providerId];
|
||||
|
||||
// Watch file changes
|
||||
this.$watch(
|
||||
() => this.refreshTrigger,
|
||||
() => {
|
||||
watch: {
|
||||
// Fix syncLocationId
|
||||
syncLocation: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (!value) {
|
||||
const firstSyncLocation = this.syncLocations[0];
|
||||
if (firstSyncLocation) {
|
||||
this.syncLocationId = firstSyncLocation.id;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
// Load revision list on context changes
|
||||
historyContextHash: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.allRevisions = [];
|
||||
const { id } = this.$store.getters['file/current'];
|
||||
const { syncToken } = this;
|
||||
if (id && syncToken) {
|
||||
if (id !== cachedFileId) {
|
||||
const historyContext = utils.deepCopy(this.historyContext);
|
||||
if (historyContext) {
|
||||
if (this.historyContextHash !== cachedHistoryContextHash) {
|
||||
this.setRevisionContent();
|
||||
cachedFileId = id;
|
||||
cachedHistoryContextHash = this.historyContextHash;
|
||||
revisionContentPromises = {};
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
revisionsPromise = new Promise((resolve, reject) => {
|
||||
this.$store.dispatch(
|
||||
'queue/enqueue',
|
||||
async () => {
|
||||
try {
|
||||
const revisions = await this.workspaceProvider
|
||||
.listRevisions(syncToken, currentFile.id);
|
||||
resolve(revisions);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
cachedFileId = null;
|
||||
const provider = providerRegistry.providersById[this.syncLocation.providerId];
|
||||
revisionsPromise = new Promise((resolve, reject) => this.$store.dispatch(
|
||||
'queue/enqueue',
|
||||
() => provider
|
||||
.listFileRevisions(historyContext)
|
||||
.then(resolve, reject),
|
||||
))
|
||||
.catch((err) => {
|
||||
this.$store.dispatch('notification/error', err);
|
||||
cachedHistoryContextHash = null;
|
||||
return [];
|
||||
});
|
||||
}
|
||||
@ -211,44 +276,40 @@ export default {
|
||||
});
|
||||
}
|
||||
}
|
||||
}, { immediate: true },
|
||||
);
|
||||
|
||||
const loadOne = () => {
|
||||
if (!this.destroyed) {
|
||||
},
|
||||
},
|
||||
// Load each revision on revision list changes
|
||||
revisions(revisions) {
|
||||
const { historyContext } = this;
|
||||
if (historyContext) {
|
||||
this.$store.dispatch(
|
||||
'queue/enqueue',
|
||||
() => {
|
||||
let loadPromise;
|
||||
this.revisions.some((revision) => {
|
||||
if (!revision.created) {
|
||||
const { syncToken } = this;
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
loadPromise = this.workspaceProvider
|
||||
.loadRevision(syncToken, currentFile.id, revision)
|
||||
.then(() => loadOne());
|
||||
}
|
||||
return loadPromise;
|
||||
});
|
||||
return loadPromise;
|
||||
},
|
||||
() => utils.awaitSequence(revisions, async (revision) => {
|
||||
// Make sure revisions and historyContext haven't changed
|
||||
if (!this.destroyed
|
||||
&& this.revisions === revisions
|
||||
&& this.historyContext === historyContext
|
||||
) {
|
||||
const provider = providerRegistry.providersById[this.syncLocation.providerId];
|
||||
await provider.loadFileRevision({
|
||||
...historyContext,
|
||||
revision,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.$watch(
|
||||
() => this.revisions,
|
||||
() => loadOne(),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Watch diffs changes
|
||||
this.$watch(
|
||||
() => this.$store.state.content.revisionContent,
|
||||
() => this.refreshHighlighters(),
|
||||
);
|
||||
|
||||
// Close revision
|
||||
},
|
||||
// Refresh highlighters on open/close revision
|
||||
revisionContent: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.refreshHighlighters();
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Close revision on escape
|
||||
this.onKeyup = (evt) => {
|
||||
if (evt.which === 27) {
|
||||
// Esc key
|
||||
@ -273,10 +334,6 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.history {
|
||||
padding: 5px 5px 50px;
|
||||
}
|
||||
|
||||
.history__button {
|
||||
font-size: 14px;
|
||||
margin-top: 0.5em;
|
||||
@ -291,7 +348,7 @@ export default {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 24px;
|
||||
left: 19px;
|
||||
border-left: 2px dotted $hr-color;
|
||||
}
|
||||
}
|
||||
@ -302,7 +359,7 @@ export default {
|
||||
|
||||
.revision__button {
|
||||
text-align: left;
|
||||
padding: 15px;
|
||||
padding: 10px;
|
||||
height: auto;
|
||||
text-transform: none;
|
||||
position: relative;
|
||||
@ -312,7 +369,7 @@ export default {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 24px;
|
||||
left: 19px;
|
||||
border-left: 2px solid $hr-color;
|
||||
}
|
||||
|
||||
@ -343,20 +400,21 @@ export default {
|
||||
.revision__header {
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.revision__created {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.layout--revision {
|
||||
.cledit-section *,
|
||||
.cl-preview-section * {
|
||||
color: transparentize($editor-color-light, 0.67) !important;
|
||||
color: transparentize($editor-color-light, 0.5) !important;
|
||||
|
||||
.app--dark & {
|
||||
color: transparentize($editor-color-dark, 0.67) !important;
|
||||
color: transparentize($editor-color-dark, 0.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,10 +15,13 @@
|
||||
<b>{{currentWorkspace.name}}</b> synced with your Google Drive app data folder.
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> synced with a <a :href="currentWorkspaceUrl" target="_blank">Google Drive folder</a>.
|
||||
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">Google Drive folder</a>.
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'couchdbWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> synced with a <a :href="currentWorkspaceUrl" target="_blank">CouchDB database</a>.
|
||||
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">CouchDB database</a>.
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'githubWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a>.
|
||||
</span>
|
||||
</div>
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
|
||||
@ -108,7 +111,7 @@ export default {
|
||||
'loginToken',
|
||||
'userId',
|
||||
]),
|
||||
currentWorkspaceUrl() {
|
||||
workspaceLocationUrl() {
|
||||
const provider = providerRegistry.providersById[this.currentWorkspace.providerId];
|
||||
return provider.getWorkspaceLocationUrl(this.currentWorkspace);
|
||||
},
|
||||
|
@ -7,7 +7,7 @@
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="templates">
|
||||
<icon-code-braces slot="icon"></icon-code-braces>
|
||||
<div>Templates</div>
|
||||
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
|
||||
<span>Configure Handlebars templates for your exports.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="reset">
|
||||
@ -50,6 +50,11 @@ export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
},
|
||||
computed: {
|
||||
templateCount() {
|
||||
return Object.keys(this.$store.getters['data/allTemplatesById']).length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onImportBackup(evt) {
|
||||
const file = evt.target.files[0];
|
||||
|
@ -65,6 +65,7 @@ export default {
|
||||
small {
|
||||
display: block;
|
||||
font-size: 0.75em;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
hr {
|
||||
|
@ -228,8 +228,8 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1--file-properties {
|
||||
max-width: 540px;
|
||||
.modal__inner-1.modal__inner-1--file-properties {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.modal__error--file-properties {
|
||||
|
@ -64,10 +64,6 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1--publish-management {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.publish-entry {
|
||||
padding: 0.5rem 0.25rem;
|
||||
border-bottom: 1px solid $hr-color;
|
||||
|
@ -36,7 +36,7 @@ import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import Tab from './common/Tab';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import defaultSettings from '../../data/defaultSettings.yml';
|
||||
import defaultSettings from '../../data/defaults/defaultSettings.yml';
|
||||
|
||||
const emptySettings = `# Add your custom settings here to override the
|
||||
# default settings.
|
||||
@ -83,8 +83,8 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1--settings {
|
||||
max-width: 600px;
|
||||
.modal__inner-1.modal__inner-1--settings {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.modal__error--settings {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<modal-inner class="modal__inner-1--sponsor" aria-label="Sponsor">
|
||||
<div class="modal__content">
|
||||
<p>Please choose a <b>PayPal</b> option.</p>
|
||||
<p>Please choose a <b>PayPal</b> option:</p>
|
||||
<a class="paypal-option button flex flex--row flex--center" v-for="button in buttons" :key="button.id" :href="button.link">
|
||||
<div class="flex flex--column">
|
||||
<div>{{button.price}}<div class="paypal-option__offer" v-if="button.offer">{{button.offer}}</div></div>
|
||||
@ -65,8 +65,8 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1--sponsor {
|
||||
max-width: 380px;
|
||||
.modal__inner-1.modal__inner-1--sponsor {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.paypal-option {
|
||||
@ -81,7 +81,7 @@ export default {
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
white-space: normal;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="sync-entry__row flex flex--row flex--align-center">
|
||||
<div class="sync-entry__url">
|
||||
{{location.url || 'Workspace location'}}
|
||||
{{location.url || 'Google Drive app data'}}
|
||||
</div>
|
||||
<div class="sync-entry__buttons flex flex--row flex--center" v-if="location.url">
|
||||
<button class="sync-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'Copy URL'">
|
||||
@ -83,10 +83,6 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1--sync-management {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.sync-entry {
|
||||
margin: 1.5em 0;
|
||||
height: auto;
|
||||
|
@ -55,8 +55,8 @@ import { mapGetters } from 'vuex';
|
||||
import utils from '../../services/utils';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import emptyTemplateValue from '../../data/emptyTemplateValue.html';
|
||||
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
|
||||
import emptyTemplateHelpers from '!raw-loader!../../data/empties/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
|
||||
@ -163,7 +163,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.modal__inner-1--templates {
|
||||
max-width: 680px;
|
||||
.modal__inner-1.modal__inner-1--templates {
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
|
@ -126,10 +126,6 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1--workspace-management {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.workspace-entry {
|
||||
margin: 1.75em 0;
|
||||
height: auto;
|
||||
|
@ -54,8 +54,8 @@ export default {
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 2px;
|
||||
|
||||
&:active,
|
||||
|
@ -11,18 +11,18 @@
|
||||
<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="File path" error="path">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> path/to/README.md
|
||||
</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>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
|
@ -11,12 +11,6 @@
|
||||
<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="File path" error="path">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
@ -24,6 +18,12 @@
|
||||
If the file exists, it will be overwritten.
|
||||
</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="Template">
|
||||
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
|
||||
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
|
||||
|
@ -27,12 +27,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../../../services/utils';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
import constants from '../../../data/constants';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
redirectUrl: utils.oauth2RedirectUri,
|
||||
redirectUrl: constants.oauth2RedirectUri,
|
||||
}),
|
||||
computedLocalSettings: {
|
||||
siteUrl: 'zendeskSiteUrl',
|
||||
|
30
src/data/constants.js
Normal file
30
src/data/constants.js
Normal file
@ -0,0 +1,30 @@
|
||||
const origin = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
export default {
|
||||
cleanTrashAfter: 0 * 24 * 60 * 60 * 1000, // 7 days
|
||||
origin,
|
||||
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
||||
types: [
|
||||
'contentState',
|
||||
'syncedContent',
|
||||
'content',
|
||||
'file',
|
||||
'folder',
|
||||
'syncLocation',
|
||||
'publishLocation',
|
||||
'data',
|
||||
],
|
||||
localStorageDataIds: [
|
||||
'workspaces',
|
||||
'settings',
|
||||
'layoutSettings',
|
||||
'tokens',
|
||||
],
|
||||
userIdPrefixes: {
|
||||
db: 'dropbox',
|
||||
gh: 'github',
|
||||
go: 'google',
|
||||
},
|
||||
textMaxLength: 250000,
|
||||
defaultName: 'Untitled',
|
||||
};
|
@ -5,7 +5,7 @@ fontSizeFactor: 1
|
||||
# Adjust maximum text width in editor and preview
|
||||
maxWidthFactor: 1
|
||||
# Auto-sync frequency (in ms). Minimum is 60000.
|
||||
autoSyncEvery: 60000
|
||||
autoSyncEvery: 90000
|
||||
|
||||
# Editor settings
|
||||
editor:
|
||||
@ -77,10 +77,11 @@ turndown:
|
||||
linkStyle: inlined
|
||||
linkReferenceStyle: full
|
||||
|
||||
# GitHub commit messages
|
||||
github:
|
||||
createFileMessage: Create {{path}} from https://stackedit.io/
|
||||
updateFileMessage: Update {{path}} from https://stackedit.io/
|
||||
deleteFileMessage: Delete {{path}} from https://stackedit.io/
|
||||
createFileMessage: '{{path}} created from https://stackedit.io/'
|
||||
updateFileMessage: '{{path}} updated from https://stackedit.io/'
|
||||
deleteFileMessage: '{{path}} deleted from https://stackedit.io/'
|
||||
|
||||
# Default content for new files
|
||||
newFileContent: |
|
@ -2,7 +2,7 @@
|
||||
|
||||
If your workspace is not synced, your files are only stored inside your browser using the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) and are not stored anywhere else.
|
||||
|
||||
We recommend syncing your workspace to make sure files won't be lost in case your browser data is cleared.
|
||||
We recommend syncing your workspace to make sure files won't be lost in case your browser data is cleared. Self-hosted CouchDB backend is best suited for privacy.
|
||||
|
||||
**Can StackEdit access my data without telling me?**
|
||||
|
||||
|
@ -54,7 +54,7 @@ export default {
|
||||
|
||||
const deleteFile = (id) => {
|
||||
if (moveToTrash) {
|
||||
store.commit('file/patchItem', {
|
||||
workspaceSvc.setOrPatchItem({
|
||||
id,
|
||||
parentId: 'trash',
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import utils from './utils';
|
||||
import store from '../store';
|
||||
import welcomeFile from '../data/welcomeFile.md';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
import constants from '../data/constants';
|
||||
|
||||
const dbVersion = 1;
|
||||
const dbStoreName = 'objects';
|
||||
@ -82,7 +83,7 @@ const contentTypes = {
|
||||
};
|
||||
|
||||
const hashMap = {};
|
||||
utils.types.forEach((type) => {
|
||||
constants.types.forEach((type) => {
|
||||
hashMap[type] = Object.create(null);
|
||||
});
|
||||
const lsHashMap = Object.create(null);
|
||||
@ -96,7 +97,7 @@ const localDbSvc = {
|
||||
* Sync data items stored in the localStorage.
|
||||
*/
|
||||
syncLocalStorage() {
|
||||
utils.localStorageDataIds.forEach((id) => {
|
||||
constants.localStorageDataIds.forEach((id) => {
|
||||
const key = `data/${id}`;
|
||||
|
||||
// Skip reloading the layoutSettings
|
||||
@ -327,7 +328,7 @@ const localDbSvc = {
|
||||
if (resetApp) {
|
||||
await Promise.all(Object.keys(store.getters['workspace/workspacesById'])
|
||||
.map(workspaceId => workspaceSvc.removeWorkspace(workspaceId)));
|
||||
utils.localStorageDataIds.forEach((id) => {
|
||||
constants.localStorageDataIds.forEach((id) => {
|
||||
// Clean data stored in localStorage
|
||||
localStorage.removeItem(`data/${id}`);
|
||||
});
|
||||
@ -372,7 +373,7 @@ const localDbSvc = {
|
||||
|
||||
// If app was last opened 7 days ago and synchronization is off
|
||||
if (!store.getters['workspace/syncToken'] &&
|
||||
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
|
||||
(store.state.workspace.lastFocus + constants.cleanTrashAfter < Date.now())
|
||||
) {
|
||||
// Clean files
|
||||
store.getters['file/items']
|
||||
|
@ -1,11 +1,13 @@
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
import constants from '../data/constants';
|
||||
|
||||
const scriptLoadingPromises = Object.create(null);
|
||||
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
|
||||
const authorizeTimeout = 6 * 60 * 1000; // 2 minutes
|
||||
const silentAuthorizeTimeout = 15 * 1000; // 15 secondes (which will be reattempted)
|
||||
const networkTimeout = 30 * 1000; // 30 sec
|
||||
let isConnectionDown = false;
|
||||
const userInactiveAfter = 2 * 60 * 1000; // 2 minutes
|
||||
const userInactiveAfter = 3 * 60 * 1000; // 3 minutes (twice the default sync period)
|
||||
|
||||
|
||||
function parseHeaders(xhr) {
|
||||
@ -54,9 +56,9 @@ export default {
|
||||
// Check browser is online periodically
|
||||
const checkOffline = async () => {
|
||||
const isBrowserOffline = window.navigator.onLine === false;
|
||||
if (!isBrowserOffline &&
|
||||
store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() &&
|
||||
this.isUserActive()
|
||||
if (!isBrowserOffline
|
||||
&& store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now()
|
||||
&& this.isUserActive()
|
||||
) {
|
||||
store.commit('updateLastOfflineCheck');
|
||||
const script = document.createElement('script');
|
||||
@ -121,85 +123,98 @@ export default {
|
||||
}
|
||||
return scriptLoadingPromises[url];
|
||||
},
|
||||
async startOauth2(url, params = {}, silent = false) {
|
||||
// Build the authorize URL
|
||||
const state = utils.uid();
|
||||
params.state = state;
|
||||
params.redirect_uri = utils.oauth2RedirectUri;
|
||||
const authorizeUrl = utils.addQueryParams(url, params);
|
||||
|
||||
let iframeElt;
|
||||
let wnd;
|
||||
if (silent) {
|
||||
// Use an iframe as wnd for silent mode
|
||||
iframeElt = utils.createHiddenIframe(authorizeUrl);
|
||||
document.body.appendChild(iframeElt);
|
||||
wnd = iframeElt.contentWindow;
|
||||
} else {
|
||||
// Open a tab otherwise
|
||||
wnd = window.open(authorizeUrl);
|
||||
if (!wnd) {
|
||||
return Promise.reject(new Error('The authorize window was blocked.'));
|
||||
}
|
||||
}
|
||||
|
||||
let checkClosedInterval;
|
||||
let closeTimeout;
|
||||
let msgHandler;
|
||||
async startOauth2(url, params = {}, silent = false, reattempt = false) {
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (silent) {
|
||||
iframeElt.onerror = () => {
|
||||
reject(new Error('Unknown error.'));
|
||||
};
|
||||
closeTimeout = setTimeout(() => {
|
||||
isConnectionDown = true;
|
||||
store.commit('setOffline', true);
|
||||
store.commit('updateLastOfflineCheck');
|
||||
reject(new Error('You are offline.'));
|
||||
}, networkTimeout);
|
||||
} else {
|
||||
closeTimeout = setTimeout(() => {
|
||||
reject(new Error('Timeout.'));
|
||||
}, oauth2AuthorizationTimeout);
|
||||
}
|
||||
|
||||
msgHandler = (event) => {
|
||||
if (event.source === wnd && event.origin === utils.origin) {
|
||||
const data = utils.parseQueryParams(`${event.data}`.slice(1));
|
||||
if (data.error || data.state !== state) {
|
||||
console.error(data); // eslint-disable-line no-console
|
||||
reject(new Error('Could not get required authorization.'));
|
||||
} else {
|
||||
resolve({
|
||||
accessToken: data.access_token,
|
||||
code: data.code,
|
||||
idToken: data.id_token,
|
||||
expiresIn: data.expires_in,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', msgHandler);
|
||||
if (!silent) {
|
||||
checkClosedInterval = setInterval(() => {
|
||||
if (wnd.closed) {
|
||||
reject(new Error('Authorize window was closed.'));
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
// Build the authorize URL
|
||||
const state = utils.uid();
|
||||
const authorizeUrl = utils.addQueryParams(url, {
|
||||
...params,
|
||||
state,
|
||||
redirect_uri: constants.oauth2RedirectUri,
|
||||
});
|
||||
} finally {
|
||||
clearInterval(checkClosedInterval);
|
||||
if (!silent && !wnd.closed) {
|
||||
wnd.close();
|
||||
|
||||
let iframeElt;
|
||||
let wnd;
|
||||
if (silent) {
|
||||
// Use an iframe as wnd for silent mode
|
||||
iframeElt = utils.createHiddenIframe(authorizeUrl);
|
||||
document.body.appendChild(iframeElt);
|
||||
wnd = iframeElt.contentWindow;
|
||||
} else {
|
||||
// Open a tab otherwise
|
||||
wnd = window.open(authorizeUrl);
|
||||
if (!wnd) {
|
||||
throw new Error('The authorize window was blocked.');
|
||||
}
|
||||
}
|
||||
if (iframeElt) {
|
||||
document.body.removeChild(iframeElt);
|
||||
|
||||
let checkClosedInterval;
|
||||
let closeTimeout;
|
||||
let msgHandler;
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (silent) {
|
||||
iframeElt.onerror = () => {
|
||||
reject(new Error('Unknown error.'));
|
||||
};
|
||||
closeTimeout = setTimeout(() => {
|
||||
if (!reattempt) {
|
||||
reject(new Error('REATTEMPT'));
|
||||
} else {
|
||||
isConnectionDown = true;
|
||||
store.commit('setOffline', true);
|
||||
store.commit('updateLastOfflineCheck');
|
||||
reject(new Error('You are offline.'));
|
||||
}
|
||||
}, silentAuthorizeTimeout);
|
||||
} else {
|
||||
closeTimeout = setTimeout(() => {
|
||||
reject(new Error('Timeout.'));
|
||||
}, authorizeTimeout);
|
||||
}
|
||||
|
||||
msgHandler = (event) => {
|
||||
if (event.source === wnd && event.origin === constants.origin) {
|
||||
const data = utils.parseQueryParams(`${event.data}`.slice(1));
|
||||
if (data.error || data.state !== state) {
|
||||
console.error(data); // eslint-disable-line no-console
|
||||
reject(new Error('Could not get required authorization.'));
|
||||
} else {
|
||||
resolve({
|
||||
accessToken: data.access_token,
|
||||
code: data.code,
|
||||
idToken: data.id_token,
|
||||
expiresIn: data.expires_in,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', msgHandler);
|
||||
if (!silent) {
|
||||
checkClosedInterval = setInterval(() => {
|
||||
if (wnd.closed) {
|
||||
reject(new Error('Authorize window was closed.'));
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
clearInterval(checkClosedInterval);
|
||||
if (!silent && !wnd.closed) {
|
||||
wnd.close();
|
||||
}
|
||||
if (iframeElt) {
|
||||
document.body.removeChild(iframeElt);
|
||||
}
|
||||
clearTimeout(closeTimeout);
|
||||
window.removeEventListener('message', msgHandler);
|
||||
}
|
||||
clearTimeout(closeTimeout);
|
||||
window.removeEventListener('message', msgHandler);
|
||||
} catch (e) {
|
||||
if (e.message === 'REATTEMPT') {
|
||||
return this.startOauth2(url, params, silent, true);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
async request(configParam, offlineCheck = false) {
|
||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
||||
|
||||
export default new Provider({
|
||||
id: 'bloggerPage',
|
||||
name: 'Blogger Page',
|
||||
getToken({ sub }) {
|
||||
const token = store.getters['data/googleTokensBySub'][sub];
|
||||
return token && token.isBlogger ? token : null;
|
||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
||||
|
||||
export default new Provider({
|
||||
id: 'blogger',
|
||||
name: 'Blogger',
|
||||
getToken({ sub }) {
|
||||
const token = store.getters['data/googleTokensBySub'][sub];
|
||||
return token && token.isBlogger ? token : null;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import providerRegistry from './providerRegistry';
|
||||
import emptyContent from '../../../data/emptyContent';
|
||||
import emptyContent from '../../../data/empties/emptyContent';
|
||||
import utils from '../../utils';
|
||||
import store from '../../../store';
|
||||
import workspaceSvc from '../../workspaceSvc';
|
||||
|
||||
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
|
||||
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->\s*$/;
|
||||
|
||||
export default class Provider {
|
||||
prepareChanges = changes => changes
|
||||
@ -70,14 +70,6 @@ export default class Provider {
|
||||
return utils.addItemHash(result);
|
||||
}
|
||||
|
||||
static getContentSyncData(fileId) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
||||
if (!syncData) {
|
||||
throw new Error(); // No need for a proper error message.
|
||||
}
|
||||
return syncData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and open a file with location that meets the criteria
|
||||
*/
|
||||
|
@ -7,6 +7,7 @@ let syncLastSeq;
|
||||
|
||||
export default new Provider({
|
||||
id: 'couchdbWorkspace',
|
||||
name: 'CouchDB',
|
||||
getToken() {
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
@ -91,19 +92,22 @@ export default new Provider({
|
||||
},
|
||||
async saveWorkspaceItem({ item, syncData }) {
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
const { id, rev } = couchdbHelper.uploadDocument({
|
||||
const { id, rev } = await couchdbHelper.uploadDocument({
|
||||
token: syncToken,
|
||||
item,
|
||||
documentId: syncData && syncData.id,
|
||||
rev: syncData && syncData.rev,
|
||||
});
|
||||
|
||||
// Build sync data to save
|
||||
return {
|
||||
// Build sync data
|
||||
id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
rev,
|
||||
syncData: {
|
||||
id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
rev,
|
||||
},
|
||||
};
|
||||
},
|
||||
removeWorkspaceItem({ syncData }) {
|
||||
@ -190,31 +194,34 @@ export default new Provider({
|
||||
},
|
||||
};
|
||||
},
|
||||
async listRevisions(token, fileId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const body = await couchdbHelper.retrieveDocumentWithRevisions(token, syncData.id);
|
||||
async listFileRevisions({ token, contentSyncData }) {
|
||||
const body = await couchdbHelper.retrieveDocumentWithRevisions(token, contentSyncData.id);
|
||||
const revisions = [];
|
||||
body._revs_info.forEach((revInfo) => { // eslint-disable-line no-underscore-dangle
|
||||
body._revs_info.forEach((revInfo, idx) => { // eslint-disable-line no-underscore-dangle
|
||||
if (revInfo.status === 'available') {
|
||||
revisions.push({
|
||||
id: revInfo.rev,
|
||||
sub: null,
|
||||
created: null,
|
||||
created: idx,
|
||||
loaded: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
return revisions;
|
||||
},
|
||||
async loadRevision(token, fileId, revision) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const body = await couchdbHelper.retrieveDocument(token, syncData.id, revision.id);
|
||||
async loadFileRevision({ token, contentSyncData, revision }) {
|
||||
if (revision.loaded) {
|
||||
return false;
|
||||
}
|
||||
const body = await couchdbHelper.retrieveDocument(token, contentSyncData.id, revision.id);
|
||||
revision.sub = body.sub;
|
||||
revision.created = body.time || 1; // Has to be truthy to prevent from loading several times
|
||||
revision.created = body.time;
|
||||
revision.loaded = true;
|
||||
return true;
|
||||
},
|
||||
async getRevisionContent(token, fileId, revisionId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
async getFileRevisionContent({ token, contentSyncData, revisionId }) {
|
||||
const body = await couchdbHelper
|
||||
.retrieveDocumentWithAttachments(token, syncData.id, revisionId);
|
||||
.retrieveDocumentWithAttachments(token, contentSyncData.id, revisionId);
|
||||
return Provider.parseContent(body.attachments.data, body.item.id);
|
||||
},
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ const makePathRelative = (token, path) => {
|
||||
|
||||
export default new Provider({
|
||||
id: 'dropbox',
|
||||
name: 'Dropbox',
|
||||
getToken({ sub }) {
|
||||
return store.getters['data/dropboxTokensBySub'][sub];
|
||||
},
|
||||
@ -27,8 +28,8 @@ export default new Provider({
|
||||
const filename = pathComponents.pop();
|
||||
return `https://www.dropbox.com/home${pathComponents.join('/')}?preview=${filename}`;
|
||||
},
|
||||
getLocationDescription({ path }) {
|
||||
return path;
|
||||
getLocationDescription({ path, dropboxFileId }) {
|
||||
return dropboxFileId || path;
|
||||
},
|
||||
checkPath(path) {
|
||||
return path && path.match(/^\/[^\\<>:"|?*]+$/);
|
||||
@ -122,4 +123,27 @@ export default new Provider({
|
||||
path,
|
||||
};
|
||||
},
|
||||
async listFileRevisions({ token, syncLocation }) {
|
||||
const entries = await dropboxHelper.listRevisions(token, syncLocation.dropboxFileId);
|
||||
return entries.map(entry => ({
|
||||
id: entry.rev,
|
||||
sub: `db:${(entry.sharing_info || {}).modified_by || token.sub}`,
|
||||
created: new Date(entry.server_modified).getTime(),
|
||||
}));
|
||||
},
|
||||
async loadFileRevision() {
|
||||
// Revision are already loaded
|
||||
return false;
|
||||
},
|
||||
async getFileRevisionContent({
|
||||
token,
|
||||
contentId,
|
||||
revisionId,
|
||||
}) {
|
||||
const { content } = await dropboxHelper.downloadFile({
|
||||
token,
|
||||
path: `rev:${revisionId}`,
|
||||
});
|
||||
return Provider.parseContent(content, contentId);
|
||||
},
|
||||
});
|
||||
|
@ -2,9 +2,11 @@ import store from '../../store';
|
||||
import githubHelper from './helpers/githubHelper';
|
||||
import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import userSvc from '../userSvc';
|
||||
|
||||
export default new Provider({
|
||||
id: 'gist',
|
||||
name: 'Gist',
|
||||
getToken({ sub }) {
|
||||
return store.getters['data/githubTokensBySub'][sub];
|
||||
},
|
||||
@ -56,4 +58,37 @@ export default new Provider({
|
||||
gistId,
|
||||
};
|
||||
},
|
||||
async listFileRevisions({ token, syncLocation }) {
|
||||
const entries = await githubHelper.getGistCommits({
|
||||
...syncLocation,
|
||||
token,
|
||||
});
|
||||
|
||||
return entries.map((entry) => {
|
||||
const sub = `gh:${entry.user.id}`;
|
||||
userSvc.addInfo({ id: sub, name: entry.user.login, imageUrl: entry.user.avatar_url });
|
||||
return {
|
||||
sub,
|
||||
id: entry.version,
|
||||
created: new Date(entry.committed_at).getTime(),
|
||||
};
|
||||
});
|
||||
},
|
||||
async loadFileRevision() {
|
||||
// Revision are already loaded
|
||||
return false;
|
||||
},
|
||||
async getFileRevisionContent({
|
||||
token,
|
||||
contentId,
|
||||
syncLocation,
|
||||
revisionId,
|
||||
}) {
|
||||
const data = await githubHelper.downloadGistRevision({
|
||||
...syncLocation,
|
||||
token,
|
||||
sha: revisionId,
|
||||
});
|
||||
return Provider.parseContent(data, contentId);
|
||||
},
|
||||
});
|
||||
|
@ -3,11 +3,13 @@ import githubHelper from './helpers/githubHelper';
|
||||
import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import workspaceSvc from '../workspaceSvc';
|
||||
import userSvc from '../userSvc';
|
||||
|
||||
const savedSha = {};
|
||||
|
||||
export default new Provider({
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
getToken({ sub }) {
|
||||
return store.getters['data/githubTokensBySub'][sub];
|
||||
},
|
||||
@ -23,21 +25,21 @@ export default new Provider({
|
||||
return path;
|
||||
},
|
||||
async downloadContent(token, syncLocation) {
|
||||
try {
|
||||
const { sha, data } = await githubHelper.downloadFile({
|
||||
...syncLocation,
|
||||
token,
|
||||
});
|
||||
savedSha[syncLocation.id] = sha;
|
||||
return Provider.parseContent(data, `${syncLocation.fileId}/content`);
|
||||
} catch (e) {
|
||||
// Ignore error, upload is going to fail anyway
|
||||
return null;
|
||||
}
|
||||
const { sha, data } = await githubHelper.downloadFile({
|
||||
...syncLocation,
|
||||
token,
|
||||
});
|
||||
savedSha[syncLocation.id] = sha;
|
||||
return Provider.parseContent(data, `${syncLocation.fileId}/content`);
|
||||
},
|
||||
async uploadContent(token, content, syncLocation) {
|
||||
if (!savedSha[syncLocation.id]) {
|
||||
await this.downloadContent(token, syncLocation); // Get the last sha
|
||||
try {
|
||||
// Get the last sha
|
||||
await this.downloadContent(token, syncLocation);
|
||||
} catch (e) {
|
||||
// Ignore error
|
||||
}
|
||||
}
|
||||
const sha = savedSha[syncLocation.id];
|
||||
delete savedSha[syncLocation.id];
|
||||
@ -50,7 +52,12 @@ export default new Provider({
|
||||
return syncLocation;
|
||||
},
|
||||
async publish(token, html, metadata, publishLocation) {
|
||||
await this.downloadContent(token, publishLocation); // Get the last sha
|
||||
try {
|
||||
// Get the last sha
|
||||
await this.downloadContent(token, publishLocation);
|
||||
} catch (e) {
|
||||
// Ignore error
|
||||
}
|
||||
const sha = savedSha[publishLocation.id];
|
||||
delete savedSha[publishLocation.id];
|
||||
await githubHelper.uploadFile({
|
||||
@ -109,4 +116,50 @@ export default new Provider({
|
||||
path,
|
||||
};
|
||||
},
|
||||
async listFileRevisions({ token, syncLocation }) {
|
||||
const entries = await githubHelper.getCommits({
|
||||
...syncLocation,
|
||||
token,
|
||||
});
|
||||
|
||||
return entries.map(({
|
||||
author,
|
||||
committer,
|
||||
commit,
|
||||
sha,
|
||||
}) => {
|
||||
let user;
|
||||
if (author && author.login) {
|
||||
user = author;
|
||||
} else if (committer && committer.login) {
|
||||
user = committer;
|
||||
}
|
||||
const sub = `gh:${user.id}`;
|
||||
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
|
||||
const date = (commit.author && commit.author.date)
|
||||
|| (commit.committer && commit.committer.date);
|
||||
return {
|
||||
id: sha,
|
||||
sub,
|
||||
created: date ? new Date(date).getTime() : 1,
|
||||
};
|
||||
});
|
||||
},
|
||||
async loadFileRevision() {
|
||||
// Revision are already loaded
|
||||
return false;
|
||||
},
|
||||
async getFileRevisionContent({
|
||||
token,
|
||||
contentId,
|
||||
syncLocation,
|
||||
revisionId,
|
||||
}) {
|
||||
const { data } = await githubHelper.downloadFile({
|
||||
...syncLocation,
|
||||
token,
|
||||
branch: revisionId,
|
||||
});
|
||||
return Provider.parseContent(data, contentId);
|
||||
},
|
||||
});
|
||||
|
@ -18,6 +18,7 @@ const endsWith = (str, suffix) => str.slice(-suffix.length) === suffix;
|
||||
|
||||
export default new Provider({
|
||||
id: 'githubWorkspace',
|
||||
name: 'GitHub',
|
||||
getToken() {
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
@ -136,76 +137,90 @@ export default new Provider({
|
||||
|
||||
// Collect changes
|
||||
const changes = [];
|
||||
const pathIds = {};
|
||||
const syncDataToKeep = Object.create(null);
|
||||
const idsByPath = {};
|
||||
const syncDataByPath = store.getters['data/syncDataById'];
|
||||
const { itemsByGitPath } = store.getters;
|
||||
const getId = (path) => {
|
||||
const existingItem = itemsByGitPath[path];
|
||||
// Use the item ID only if the item was already synced
|
||||
if (existingItem && syncDataByPath[path]) {
|
||||
pathIds[path] = existingItem.id;
|
||||
return existingItem.id;
|
||||
const { itemIdsByGitPath } = store.getters;
|
||||
const getIdFromPath = (path, isFile) => {
|
||||
let itemId = idsByPath[path];
|
||||
if (!itemId) {
|
||||
const existingItemId = itemIdsByGitPath[path];
|
||||
// We can replace the item only if it was already synced
|
||||
if (existingItemId
|
||||
&& (syncDataByPath[path]
|
||||
// Content may have already be synced
|
||||
|| (isFile && syncDataByPath[`/${path}`]))
|
||||
) {
|
||||
itemId = existingItemId;
|
||||
} else {
|
||||
// Otherwise, make a new ID for a new item
|
||||
itemId = utils.uid();
|
||||
}
|
||||
// If it's a file path, add the content path as well
|
||||
if (isFile) {
|
||||
idsByPath[`/${path}`] = `${itemId}/content`;
|
||||
}
|
||||
idsByPath[path] = itemId;
|
||||
}
|
||||
// Generate a new ID
|
||||
let id = utils.uid();
|
||||
if (path[0] === '/') {
|
||||
id += '/content';
|
||||
}
|
||||
pathIds[path] = id;
|
||||
return id;
|
||||
return itemId;
|
||||
};
|
||||
|
||||
// Folder creations/updates
|
||||
// Assume map entries are sorted from top to bottom
|
||||
Object.entries(treeFolderMap).forEach(([path, parentPath]) => {
|
||||
const item = utils.addItemHash({
|
||||
id: getId(path),
|
||||
type: 'folder',
|
||||
name: path.slice(parentPath.length, -1),
|
||||
parentId: pathIds[parentPath] || null,
|
||||
});
|
||||
changes.push({
|
||||
syncDataId: path,
|
||||
item,
|
||||
syncData: {
|
||||
id: path,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
});
|
||||
if (path === '.stackedit-trash/') {
|
||||
idsByPath[path] = 'trash';
|
||||
} else {
|
||||
const item = utils.addItemHash({
|
||||
id: getIdFromPath(path),
|
||||
type: 'folder',
|
||||
name: path.slice(parentPath.length, -1),
|
||||
parentId: idsByPath[parentPath] || null,
|
||||
});
|
||||
|
||||
const folderSyncData = syncDataByPath[path];
|
||||
if (!folderSyncData || folderSyncData.hash !== item.hash) {
|
||||
changes.push({
|
||||
syncDataId: path,
|
||||
item,
|
||||
syncData: {
|
||||
id: path,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// File/content creations/updates
|
||||
Object.entries(treeFileMap).forEach(([path, parentPath]) => {
|
||||
// Look for content sync data as it's created before file sync data
|
||||
const fileId = getIdFromPath(path, true);
|
||||
const contentPath = `/${path}`;
|
||||
const contentId = getId(contentPath);
|
||||
const contentId = idsByPath[contentPath];
|
||||
|
||||
// File creations/updates
|
||||
const [fileId] = contentId.split('/');
|
||||
const item = utils.addItemHash({
|
||||
id: fileId,
|
||||
type: 'file',
|
||||
name: path.slice(parentPath.length, -'.md'.length),
|
||||
parentId: pathIds[parentPath] || null,
|
||||
});
|
||||
changes.push({
|
||||
syncDataId: path,
|
||||
item,
|
||||
syncData: {
|
||||
id: path,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
parentId: idsByPath[parentPath] || null,
|
||||
});
|
||||
|
||||
const fileSyncData = syncDataByPath[path];
|
||||
if (!fileSyncData || fileSyncData.hash !== item.hash) {
|
||||
changes.push({
|
||||
syncDataId: path,
|
||||
item,
|
||||
syncData: {
|
||||
id: path,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Content creations/updates
|
||||
const contentSyncData = syncDataByPath[contentPath];
|
||||
if (contentSyncData) {
|
||||
syncDataToKeep[path] = true;
|
||||
syncDataToKeep[contentPath] = true;
|
||||
}
|
||||
if (!contentSyncData || contentSyncData.sha !== treeShaMap[path]) {
|
||||
const type = 'content';
|
||||
// Use `/` as a prefix to get a unique syncData id
|
||||
@ -233,11 +248,8 @@ export default new Provider({
|
||||
// Only template data are stored
|
||||
const [, id] = path.match(/^\.stackedit-data\/(templates)\.json$/) || [];
|
||||
if (id) {
|
||||
pathIds[path] = id;
|
||||
idsByPath[path] = id;
|
||||
const syncData = syncDataByItemId[id];
|
||||
if (syncData) {
|
||||
syncDataToKeep[syncData.id] = true;
|
||||
}
|
||||
if (!syncData || syncData.sha !== treeShaMap[path]) {
|
||||
const type = 'data';
|
||||
changes.push({
|
||||
@ -273,14 +285,11 @@ export default new Provider({
|
||||
const [, filePath, data] = path.match(pathMatcher) || [];
|
||||
if (filePath) {
|
||||
// If there is a corresponding md file in the tree
|
||||
const fileId = pathIds[`${filePath}.md`];
|
||||
const fileId = idsByPath[`${filePath}.md`];
|
||||
if (fileId) {
|
||||
// Reuse existing ID or create a new one
|
||||
const existingItem = itemsByGitPath[path];
|
||||
const id = existingItem
|
||||
? existingItem.id
|
||||
: utils.uid();
|
||||
pathIds[path] = id;
|
||||
const id = itemIdsByGitPath[path] || utils.uid();
|
||||
idsByPath[path] = id;
|
||||
|
||||
const item = utils.addItemHash({
|
||||
...JSON.parse(utils.decodeBase64(data)),
|
||||
@ -288,22 +297,26 @@ export default new Provider({
|
||||
type,
|
||||
fileId,
|
||||
});
|
||||
changes.push({
|
||||
syncDataId: path,
|
||||
item,
|
||||
syncData: {
|
||||
id: path,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
});
|
||||
|
||||
const locationSyncData = syncDataByPath[path];
|
||||
if (!locationSyncData || locationSyncData.hash !== item.hash) {
|
||||
changes.push({
|
||||
syncDataId: path,
|
||||
item,
|
||||
syncData: {
|
||||
id: path,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Deletions
|
||||
Object.keys(syncDataByPath).forEach((path) => {
|
||||
if (!pathIds[path] && !syncDataToKeep[path]) {
|
||||
if (!idsByPath[path]) {
|
||||
changes.push({ syncDataId: path });
|
||||
}
|
||||
});
|
||||
@ -331,7 +344,11 @@ export default new Provider({
|
||||
content: '',
|
||||
sha: treeShaMap[syncData.id],
|
||||
});
|
||||
return syncData;
|
||||
|
||||
// Return sync data to save
|
||||
return {
|
||||
syncData,
|
||||
};
|
||||
},
|
||||
async removeWorkspaceItem({ syncData }) {
|
||||
if (treeShaMap[syncData.id]) {
|
||||
@ -435,16 +452,16 @@ export default new Provider({
|
||||
},
|
||||
};
|
||||
},
|
||||
async listRevisions(token, fileId) {
|
||||
async listFileRevisions({ token, fileSyncData }) {
|
||||
const { owner, repo, branch } = store.getters['workspace/currentWorkspace'];
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const entries = await githubHelper.getCommits({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
sha: branch,
|
||||
path: syncData.id,
|
||||
path: getAbsolutePath(fileSyncData),
|
||||
});
|
||||
|
||||
return entries.map(({
|
||||
author,
|
||||
committer,
|
||||
@ -466,17 +483,24 @@ export default new Provider({
|
||||
sub,
|
||||
created: date ? new Date(date).getTime() : 1,
|
||||
};
|
||||
})
|
||||
.sort((revision1, revision2) => revision2.created - revision1.created);
|
||||
});
|
||||
},
|
||||
async getRevisionContent(token, fileId, revisionId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
async loadFileRevision() {
|
||||
// Revisions are already loaded
|
||||
return false;
|
||||
},
|
||||
async getFileRevisionContent({
|
||||
token,
|
||||
contentId,
|
||||
fileSyncData,
|
||||
revisionId,
|
||||
}) {
|
||||
const { data } = await githubHelper.downloadFile({
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token,
|
||||
branch: revisionId,
|
||||
path: getAbsolutePath(syncData),
|
||||
path: getAbsolutePath(fileSyncData),
|
||||
});
|
||||
return Provider.parseContent(data, `${fileId}/content`);
|
||||
return Provider.parseContent(data, contentId);
|
||||
},
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ let syncStartPageToken;
|
||||
|
||||
export default new Provider({
|
||||
id: 'googleDriveAppData',
|
||||
name: 'Google Drive app data',
|
||||
getToken() {
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
@ -69,12 +70,15 @@ export default new Provider({
|
||||
fileId: syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
});
|
||||
// Build sync data
|
||||
|
||||
// Build sync data to save
|
||||
return {
|
||||
id: file.id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
syncData: {
|
||||
id: file.id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
};
|
||||
},
|
||||
removeWorkspaceItem({ syncData, ifNotTooLate }) {
|
||||
@ -163,19 +167,21 @@ export default new Provider({
|
||||
},
|
||||
};
|
||||
},
|
||||
async listRevisions(token, fileId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const revisions = await googleHelper.getAppDataFileRevisions(token, syncData.id);
|
||||
async listFileRevisions({ token, contentSyncData }) {
|
||||
const revisions = await googleHelper.getAppDataFileRevisions(token, contentSyncData.id);
|
||||
return revisions.map(revision => ({
|
||||
id: revision.id,
|
||||
sub: `go:${revision.lastModifyingUser.permissionId}`,
|
||||
created: new Date(revision.modifiedTime).getTime(),
|
||||
}))
|
||||
.sort((revision1, revision2) => revision2.created - revision1.created);
|
||||
}));
|
||||
},
|
||||
async getRevisionContent(token, fileId, revisionId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const content = await googleHelper.downloadAppDataFileRevision(token, syncData.id, revisionId);
|
||||
async loadFileRevision() {
|
||||
// Revisions are already loaded
|
||||
return false;
|
||||
},
|
||||
async getFileRevisionContent({ token, contentSyncData, revisionId }) {
|
||||
const content = await googleHelper
|
||||
.downloadAppDataFileRevision(token, contentSyncData.id, revisionId);
|
||||
return JSON.parse(content);
|
||||
},
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ import workspaceSvc from '../workspaceSvc';
|
||||
|
||||
export default new Provider({
|
||||
id: 'googleDrive',
|
||||
name: 'Google Drive',
|
||||
getToken({ sub }) {
|
||||
const token = store.getters['data/googleTokensBySub'][sub];
|
||||
return token && token.isDrive ? token : null;
|
||||
@ -190,4 +191,26 @@ export default new Provider({
|
||||
}
|
||||
return location;
|
||||
},
|
||||
async listFileRevisions({ token, syncLocation }) {
|
||||
const revisions = await googleHelper.getFileRevisions(token, syncLocation.driveFileId);
|
||||
return revisions.map(revision => ({
|
||||
id: revision.id,
|
||||
sub: `go:${revision.lastModifyingUser.permissionId}`,
|
||||
created: new Date(revision.modifiedTime).getTime(),
|
||||
}));
|
||||
},
|
||||
async loadFileRevision() {
|
||||
// Revision are already loaded
|
||||
return false;
|
||||
},
|
||||
async getFileRevisionContent({
|
||||
token,
|
||||
contentId,
|
||||
syncLocation,
|
||||
revisionId,
|
||||
}) {
|
||||
const content = await googleHelper
|
||||
.downloadFileRevision(token, syncLocation.driveFileId, revisionId);
|
||||
return Provider.parseContent(content, contentId);
|
||||
},
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ let syncStartPageToken;
|
||||
|
||||
export default new Provider({
|
||||
id: 'googleDriveWorkspace',
|
||||
name: 'Google Drive',
|
||||
getToken() {
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
@ -361,12 +362,16 @@ export default new Provider({
|
||||
ifNotTooLate,
|
||||
});
|
||||
}
|
||||
// Build sync data
|
||||
|
||||
// Build sync data to save
|
||||
return {
|
||||
id: file.id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
syncData: {
|
||||
id: file.id,
|
||||
parentIds: file.parents,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
};
|
||||
},
|
||||
async removeWorkspaceItem({ syncData, ifNotTooLate }) {
|
||||
@ -449,6 +454,7 @@ export default new Provider({
|
||||
// Create file sync data
|
||||
newFileSyncData = {
|
||||
id: gdriveFile.id,
|
||||
parentIds: gdriveFile.parents,
|
||||
itemId: file.id,
|
||||
type: file.type,
|
||||
hash: file.hash,
|
||||
@ -495,25 +501,32 @@ export default new Provider({
|
||||
return {
|
||||
syncData: {
|
||||
id: file.id,
|
||||
parentIds: file.parents,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
},
|
||||
};
|
||||
},
|
||||
async listRevisions(token, fileId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const revisions = await googleHelper.getFileRevisions(token, syncData.id);
|
||||
async listFileRevisions({ token, fileSyncData }) {
|
||||
const revisions = await googleHelper.getFileRevisions(token, fileSyncData.id);
|
||||
return revisions.map(revision => ({
|
||||
id: revision.id,
|
||||
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
|
||||
sub: `go:${revision.lastModifyingUser.permissionId}`,
|
||||
created: new Date(revision.modifiedTime).getTime(),
|
||||
}))
|
||||
.sort((revision1, revision2) => revision2.created - revision1.created);
|
||||
}));
|
||||
},
|
||||
async getRevisionContent(token, fileId, revisionId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const content = await googleHelper.downloadFileRevision(token, syncData.id, revisionId);
|
||||
return Provider.parseContent(content, `${fileId}/content`);
|
||||
async loadFileRevision() {
|
||||
// Revision are already loaded
|
||||
return false;
|
||||
},
|
||||
async getFileRevisionContent({
|
||||
token,
|
||||
contentId,
|
||||
fileSyncData,
|
||||
revisionId,
|
||||
}) {
|
||||
const content = await googleHelper.downloadFileRevision(token, fileSyncData.id, revisionId);
|
||||
return Provider.parseContent(content, contentId);
|
||||
},
|
||||
});
|
||||
|
@ -141,6 +141,11 @@ export default {
|
||||
* http://docs.couchdb.org/en/2.1.1/api/document/common.html#delete--db-docid
|
||||
*/
|
||||
async removeDocument(token, documentId, rev) {
|
||||
if (!documentId) {
|
||||
// Prevent from deleting the whole database
|
||||
throw new Error('Missing document ID');
|
||||
}
|
||||
|
||||
return request(token, {
|
||||
method: 'DELETE',
|
||||
path: documentId,
|
||||
|
@ -11,14 +11,14 @@ const getAppKey = (fullAccess) => {
|
||||
const httpHeaderSafeJson = args => args && JSON.stringify(args)
|
||||
.replace(/[\u007f-\uffff]/g, c => `\\u${`000${c.charCodeAt(0).toString(16)}`.slice(-4)}`);
|
||||
|
||||
const request = (token, options, args) => networkSvc.request({
|
||||
const request = ({ accessToken }, options, args) => networkSvc.request({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
'Content-Type': options.body && (typeof options.body === 'string'
|
||||
? 'application/octet-stream' : 'application/json; charset=utf-8'),
|
||||
'Dropbox-API-Arg': httpHeaderSafeJson(args),
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
@ -64,6 +64,28 @@ export default {
|
||||
return this.startOauth2(fullAccess);
|
||||
},
|
||||
|
||||
/**
|
||||
* https://www.dropbox.com/developers/documentation/http/documentation#users-get_account
|
||||
*/
|
||||
async getAccount(token, userId) {
|
||||
const { body } = await request(token, {
|
||||
method: 'POST',
|
||||
url: 'https://api.dropboxapi.com/2/users/get_account',
|
||||
body: {
|
||||
account_id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
// Add user info to the store
|
||||
store.commit('userInfo/addItem', {
|
||||
id: `db:${body.account_id}`,
|
||||
name: body.name.display_name,
|
||||
imageUrl: body.profile_photo_url || '',
|
||||
});
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
/**
|
||||
* https://www.dropbox.com/developers/documentation/http/documentation#files-upload
|
||||
*/
|
||||
@ -104,6 +126,22 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* https://www.dropbox.com/developers/documentation/http/documentation#list-revisions
|
||||
*/
|
||||
async listRevisions(token, fileId) {
|
||||
const res = await request(token, {
|
||||
method: 'POST',
|
||||
url: 'https://api.dropboxapi.com/2/files/list_revisions',
|
||||
body: {
|
||||
path: fileId,
|
||||
mode: 'id',
|
||||
limit: 100,
|
||||
},
|
||||
});
|
||||
return res.body.entries;
|
||||
},
|
||||
|
||||
/**
|
||||
* https://www.dropbox.com/developers/chooser
|
||||
*/
|
||||
|
@ -95,11 +95,14 @@ export default {
|
||||
t: Date.now(), // Prevent from caching
|
||||
},
|
||||
})).body;
|
||||
|
||||
// Add user info to the store
|
||||
store.commit('userInfo/addItem', {
|
||||
id: `gh:${user.id}`,
|
||||
name: user.login,
|
||||
imageUrl: user.avatar_url || '',
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
@ -263,4 +266,35 @@ export default {
|
||||
}
|
||||
return result.content;
|
||||
},
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/gists/#list-gist-commits
|
||||
*/
|
||||
async getGistCommits({
|
||||
token,
|
||||
gistId,
|
||||
}) {
|
||||
const { body } = await request(token, {
|
||||
url: `https://api.github.com/gists/${gistId}/commits`,
|
||||
});
|
||||
return body;
|
||||
},
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/gists/#get-a-specific-revision-of-a-gist
|
||||
*/
|
||||
async downloadGistRevision({
|
||||
token,
|
||||
gistId,
|
||||
filename,
|
||||
sha,
|
||||
}) {
|
||||
const result = (await request(token, {
|
||||
url: `https://api.github.com/gists/${gistId}/${sha}`,
|
||||
})).body.files[filename];
|
||||
if (!result) {
|
||||
throw new Error('Gist file not found.');
|
||||
}
|
||||
return result.content;
|
||||
},
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
||||
|
||||
export default new Provider({
|
||||
id: 'wordpress',
|
||||
name: 'WordPress',
|
||||
getToken(location) {
|
||||
return store.getters['data/wordpressTokensBySub'][location.sub];
|
||||
},
|
||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
||||
|
||||
export default new Provider({
|
||||
id: 'zendesk',
|
||||
name: 'Zendesk',
|
||||
getToken(location) {
|
||||
return store.getters['data/zendeskTokensBySub'][location.sub];
|
||||
},
|
||||
|
@ -79,6 +79,7 @@ const publishFile = async (fileId) => {
|
||||
utils.serializeObject(publishLocationToStore)
|
||||
) {
|
||||
store.commit('publishLocation/patchItem', publishLocationToStore);
|
||||
workspaceSvc.ensureUniqueLocations();
|
||||
}
|
||||
counter += 1;
|
||||
} catch (err) {
|
||||
|
@ -10,6 +10,7 @@ import './providers/githubWorkspaceProvider';
|
||||
import './providers/googleDriveWorkspaceProvider';
|
||||
import tempFileSvc from './tempFileSvc';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
import constants from '../data/constants';
|
||||
|
||||
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||
@ -128,7 +129,7 @@ const cleanSyncedContent = (syncedContent) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply changes retrieved from the main provider. Update sync data accordingly.
|
||||
* Apply changes retrieved from the workspace provider. Update sync data accordingly.
|
||||
*/
|
||||
const applyChanges = (changes) => {
|
||||
const allItemsById = { ...store.getters.allItemsById };
|
||||
@ -138,10 +139,7 @@ const applyChanges = (changes) => {
|
||||
let getExistingItem;
|
||||
if (store.getters['workspace/currentWorkspaceIsGit']) {
|
||||
const itemsByGitPath = { ...store.getters.itemsByGitPath };
|
||||
getExistingItem = (existingSyncData) => {
|
||||
const items = existingSyncData && itemsByGitPath[existingSyncData.id];
|
||||
return items ? items[0] : null;
|
||||
};
|
||||
getExistingItem = existingSyncData => existingSyncData && itemsByGitPath[existingSyncData.id];
|
||||
} else {
|
||||
getExistingItem = existingSyncData => existingSyncData && allItemsById[existingSyncData.itemId];
|
||||
}
|
||||
@ -476,6 +474,13 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// If content is to be created, schedule a restart to create the file as well
|
||||
if (provider === workspaceProvider &&
|
||||
!store.getters['data/syncDataByItemId'][fileId]
|
||||
) {
|
||||
syncContext.restartSkipContents = true;
|
||||
}
|
||||
|
||||
// Upload merged content
|
||||
const item = {
|
||||
...mergedContent,
|
||||
@ -491,13 +496,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
||||
utils.serializeObject(syncLocationToStore)
|
||||
) {
|
||||
store.commit('syncLocation/patchItem', syncLocationToStore);
|
||||
}
|
||||
|
||||
// If content was just created, restart sync to create the file as well
|
||||
if (provider === workspaceProvider &&
|
||||
!store.getters['data/syncDataByItemId'][fileId]
|
||||
) {
|
||||
syncContext.restartSkipContents = true;
|
||||
workspaceSvc.ensureUniqueLocations();
|
||||
}
|
||||
};
|
||||
|
||||
@ -668,16 +667,13 @@ const syncWorkspace = async (skipContents = false) => {
|
||||
|
||||
if (!changedItem) return false;
|
||||
|
||||
const resultSyncData = await workspaceProvider
|
||||
.saveWorkspaceItem({
|
||||
// Use deepCopy to freeze objects
|
||||
item: utils.deepCopy(changedItem),
|
||||
syncData: utils.deepCopy(syncDataToUpdate),
|
||||
ifNotTooLate,
|
||||
});
|
||||
store.dispatch('data/patchSyncDataById', {
|
||||
[resultSyncData.id]: resultSyncData,
|
||||
});
|
||||
updateSyncData(await workspaceProvider.saveWorkspaceItem({
|
||||
// Use deepCopy to freeze objects
|
||||
item: utils.deepCopy(changedItem),
|
||||
syncData: utils.deepCopy(syncDataToUpdate),
|
||||
ifNotTooLate,
|
||||
}));
|
||||
|
||||
return true;
|
||||
}));
|
||||
|
||||
@ -819,7 +815,7 @@ const requestSync = () => {
|
||||
|
||||
// Determine if we have to clean files
|
||||
const fileHashesToClean = {};
|
||||
if (getLastStoredSyncActivity() + utils.cleanTrashAfter < Date.now()) {
|
||||
if (getLastStoredSyncActivity() + constants.cleanTrashAfter < Date.now()) {
|
||||
// Last synchronization happened 7 days ago
|
||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||
store.getters['file/items'].forEach((file) => {
|
||||
|
@ -1,13 +1,14 @@
|
||||
import googleHelper from './providers/helpers/googleHelper';
|
||||
import githubHelper from './providers/helpers/githubHelper';
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
import dropboxHelper from './providers/helpers/dropboxHelper';
|
||||
import constants from '../data/constants';
|
||||
|
||||
const promised = {};
|
||||
|
||||
const parseUserId = (userId) => {
|
||||
const prefix = userId[2] === ':' && userId.slice(0, 2);
|
||||
const type = prefix && utils.userIdPrefixes[prefix];
|
||||
const type = prefix && constants.userIdPrefixes[prefix];
|
||||
return type ? [type, userId.slice(3)] : ['google', userId];
|
||||
};
|
||||
|
||||
@ -17,7 +18,7 @@ export default {
|
||||
store.commit('userInfo/addItem', { id, name, imageUrl });
|
||||
},
|
||||
async getInfo(userId) {
|
||||
if (!promised[userId]) {
|
||||
if (userId && !promised[userId]) {
|
||||
const [type, sub] = parseUserId(userId);
|
||||
|
||||
// Try to find a token with this sub
|
||||
@ -33,6 +34,17 @@ export default {
|
||||
if (!store.state.offline) {
|
||||
promised[userId] = true;
|
||||
switch (type) {
|
||||
case 'dropbox': {
|
||||
const dropboxToken = Object.values(store.getters['data/dropboxTokensBySub'])[0];
|
||||
try {
|
||||
await dropboxHelper.getAccount(dropboxToken, sub);
|
||||
} catch (err) {
|
||||
if (!token || err.status !== 404) {
|
||||
promised[userId] = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'github':
|
||||
try {
|
||||
await githubHelper.getUser(sub);
|
||||
|
@ -1,8 +1,7 @@
|
||||
import yaml from 'js-yaml';
|
||||
import '../libs/clunderscore';
|
||||
import presets from '../data/presets';
|
||||
|
||||
const origin = `${window.location.protocol}//${window.location.host}`;
|
||||
import constants from '../data/constants';
|
||||
|
||||
// For utils.uid()
|
||||
const uidLength = 16;
|
||||
@ -16,7 +15,18 @@ const parseQueryParams = (params) => {
|
||||
const result = {};
|
||||
params.split('&').forEach((param) => {
|
||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||
if (key) {
|
||||
if (key && value != null) {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// For utils.setQueryParams()
|
||||
const filterParams = (params = {}) => {
|
||||
const result = {};
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (key && value != null) {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
@ -67,12 +77,9 @@ Object.keys(presets).forEach((key) => {
|
||||
|
||||
export default {
|
||||
computedPresets,
|
||||
cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
origin,
|
||||
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
||||
queryParams: parseQueryParams(window.location.hash.slice(1)),
|
||||
setQueryParams(params = {}) {
|
||||
this.queryParams = params;
|
||||
this.queryParams = filterParams(params);
|
||||
const serializedParams = Object.entries(this.queryParams).map(([key, value]) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
|
||||
const hash = `#${serializedParams}`;
|
||||
@ -80,39 +87,17 @@ export default {
|
||||
window.location.replace(hash);
|
||||
}
|
||||
},
|
||||
types: [
|
||||
'contentState',
|
||||
'syncedContent',
|
||||
'content',
|
||||
'file',
|
||||
'folder',
|
||||
'syncLocation',
|
||||
'publishLocation',
|
||||
'data',
|
||||
],
|
||||
localStorageDataIds: [
|
||||
'workspaces',
|
||||
'settings',
|
||||
'layoutSettings',
|
||||
'tokens',
|
||||
],
|
||||
userIdPrefixes: {
|
||||
go: 'google',
|
||||
gh: 'github',
|
||||
},
|
||||
textMaxLength: 250000,
|
||||
sanitizeText(text) {
|
||||
const result = `${text || ''}`.slice(0, this.textMaxLength);
|
||||
const result = `${text || ''}`.slice(0, constants.textMaxLength);
|
||||
// last char must be a `\n`.
|
||||
return `${result}\n`.replace(/\n\n$/, '\n');
|
||||
},
|
||||
defaultName: 'Untitled',
|
||||
sanitizeName(name) {
|
||||
return `${name || ''}`
|
||||
// Replace `/`, control characters and other kind of spaces with a space
|
||||
.replace(/[/\x00-\x1F\x7f-\xa0\s]+/g, ' ').trim() // eslint-disable-line no-control-regex
|
||||
// Keep only 250 characters
|
||||
.slice(0, 250) || this.defaultName;
|
||||
.slice(0, 250) || constants.defaultName;
|
||||
},
|
||||
deepCopy,
|
||||
serializeObject(obj) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import store from '../store';
|
||||
import utils from './utils';
|
||||
import constants from '../data/constants';
|
||||
|
||||
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
||||
|
||||
@ -35,7 +36,7 @@ export default {
|
||||
// Show warning dialogs
|
||||
if (!background) {
|
||||
// If name is being stripped
|
||||
if (item.name !== utils.defaultName && item.name !== name) {
|
||||
if (item.name !== constants.defaultName && item.name !== name) {
|
||||
await store.dispatch('modal/open', {
|
||||
type: 'stripName',
|
||||
item,
|
||||
@ -83,7 +84,7 @@ export default {
|
||||
|
||||
// Show warning dialogs
|
||||
// If name has been stripped
|
||||
if (sanitizedName !== utils.defaultName && sanitizedName !== item.name) {
|
||||
if (sanitizedName !== constants.defaultName && sanitizedName !== item.name) {
|
||||
await store.dispatch('modal/open', {
|
||||
type: 'stripName',
|
||||
item,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../data/emptyContent';
|
||||
import empty from '../data/empties/emptyContent';
|
||||
import utils from '../services/utils';
|
||||
import cledit from '../services/editor/cledit';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../data/emptyContentState';
|
||||
import empty from '../data/empties/emptyContentState';
|
||||
|
||||
const module = moduleTemplate(empty, true);
|
||||
|
||||
|
@ -1,14 +1,15 @@
|
||||
import Vue from 'vue';
|
||||
import yaml from 'js-yaml';
|
||||
import utils from '../services/utils';
|
||||
import defaultWorkspaces from '../data/defaultWorkspaces';
|
||||
import defaultSettings from '../data/defaultSettings.yml';
|
||||
import defaultLocalSettings from '../data/defaultLocalSettings';
|
||||
import defaultLayoutSettings from '../data/defaultLayoutSettings';
|
||||
import plainHtmlTemplate from '../data/plainHtmlTemplate.html';
|
||||
import styledHtmlTemplate from '../data/styledHtmlTemplate.html';
|
||||
import styledHtmlWithTocTemplate from '../data/styledHtmlWithTocTemplate.html';
|
||||
import jekyllSiteTemplate from '../data/jekyllSiteTemplate.html';
|
||||
import defaultWorkspaces from '../data/defaults/defaultWorkspaces';
|
||||
import defaultSettings from '../data/defaults/defaultSettings.yml';
|
||||
import defaultLocalSettings from '../data/defaults/defaultLocalSettings';
|
||||
import defaultLayoutSettings from '../data/defaults/defaultLayoutSettings';
|
||||
import plainHtmlTemplate from '../data/templates/plainHtmlTemplate.html';
|
||||
import styledHtmlTemplate from '../data/templates/styledHtmlTemplate.html';
|
||||
import styledHtmlWithTocTemplate from '../data/templates/styledHtmlWithTocTemplate.html';
|
||||
import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
|
||||
import constants from '../data/constants';
|
||||
|
||||
const itemTemplate = (id, data = {}) => ({
|
||||
id,
|
||||
@ -33,7 +34,7 @@ const empty = (id) => {
|
||||
};
|
||||
|
||||
// Item IDs that will be stored in the localStorage
|
||||
const lsItemIdSet = new Set(utils.localStorageDataIds);
|
||||
const lsItemIdSet = new Set(constants.localStorageDataIds);
|
||||
|
||||
// Getter/setter/patcher factories
|
||||
const getter = id => state => ((lsItemIdSet.has(id)
|
||||
@ -58,13 +59,13 @@ const layoutSettingsToggler = propertyName => ({ getters, dispatch }, value) =>
|
||||
[propertyName]: value === undefined ? !getters.layoutSettings[propertyName] : value,
|
||||
});
|
||||
const notEnoughSpace = (getters) => {
|
||||
const constants = getters['layout/constants'];
|
||||
const layoutConstants = getters['layout/constants'];
|
||||
const showGutter = getters['discussion/currentDiscussion'];
|
||||
return document.body.clientWidth < constants.editorMinWidth +
|
||||
constants.explorerWidth +
|
||||
constants.sideBarWidth +
|
||||
constants.buttonBarWidth +
|
||||
(showGutter ? constants.gutterWidth : 0);
|
||||
return document.body.clientWidth < layoutConstants.editorMinWidth +
|
||||
layoutConstants.explorerWidth +
|
||||
layoutConstants.sideBarWidth +
|
||||
layoutConstants.buttonBarWidth +
|
||||
(showGutter ? layoutConstants.gutterWidth : 0);
|
||||
};
|
||||
|
||||
// For templates
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import emptyFile from '../data/emptyFile';
|
||||
import emptyFolder from '../data/emptyFolder';
|
||||
import emptyFile from '../data/empties/emptyFile';
|
||||
import emptyFolder from '../data/empties/emptyFolder';
|
||||
|
||||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../data/emptyFile';
|
||||
import empty from '../data/empties/emptyFile';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../data/emptyFolder';
|
||||
import empty from '../data/empties/emptyFolder';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
||||
|
@ -19,8 +19,9 @@ import syncedContent from './syncedContent';
|
||||
import userInfo from './userInfo';
|
||||
import workspace from './workspace';
|
||||
import locationTemplate from './locationTemplate';
|
||||
import emptyPublishLocation from '../data/emptyPublishLocation';
|
||||
import emptySyncLocation from '../data/emptySyncLocation';
|
||||
import emptyPublishLocation from '../data/empties/emptyPublishLocation';
|
||||
import emptySyncLocation from '../data/empties/emptySyncLocation';
|
||||
import constants from '../data/constants';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@ -77,7 +78,7 @@ const store = new Vuex.Store({
|
||||
getters: {
|
||||
allItemsById: (state) => {
|
||||
const result = {};
|
||||
utils.types.forEach(type => Object.assign(result, state[type].itemsById));
|
||||
constants.types.forEach(type => Object.assign(result, state[type].itemsById));
|
||||
return result;
|
||||
},
|
||||
pathsByItemId: (state, getters) => {
|
||||
@ -86,7 +87,7 @@ const store = new Vuex.Store({
|
||||
const getPath = (item) => {
|
||||
let itemPath = result[item.id];
|
||||
if (!itemPath) {
|
||||
if (item.parendId === 'trash') {
|
||||
if (item.parentId === 'trash') {
|
||||
itemPath = `.stackedit-trash/${item.name}`;
|
||||
} else {
|
||||
let { name } = item;
|
||||
@ -150,14 +151,19 @@ const store = new Vuex.Store({
|
||||
});
|
||||
return result;
|
||||
},
|
||||
itemIdsByGitPath: (state, { gitPathsByItemId }) => {
|
||||
const result = {};
|
||||
Object.entries(gitPathsByItemId).forEach(([id, path]) => {
|
||||
result[path] = id;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
itemsByGitPath: (state, { allItemsById, gitPathsByItemId }) => {
|
||||
const result = {};
|
||||
Object.entries(gitPathsByItemId).forEach(([id, path]) => {
|
||||
const item = allItemsById[id];
|
||||
if (item) {
|
||||
const items = result[path] || [];
|
||||
items.push(item);
|
||||
result[path] = items;
|
||||
result[path] = item;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import providerRegistry from '../services/providers/common/providerRegistry';
|
||||
import utils from '../services/utils';
|
||||
|
||||
const addToGroup = (groups, item) => {
|
||||
const list = groups[item.fileId];
|
||||
@ -54,7 +55,7 @@ export default (empty) => {
|
||||
const provider = providerRegistry.providersById[location.providerId];
|
||||
return {
|
||||
...location,
|
||||
description: provider.getLocationDescription(location),
|
||||
description: utils.sanitizeName(provider.getLocationDescription(location)),
|
||||
url: provider.getLocationUrl(location),
|
||||
};
|
||||
});
|
||||
@ -74,7 +75,8 @@ export default (empty) => {
|
||||
id: 'main',
|
||||
providerId: workspaceProvider.id,
|
||||
fileId,
|
||||
description: workspaceProvider.getSyncDataDescription(fileSyncData, contentSyncData),
|
||||
description: utils.sanitizeName(workspaceProvider
|
||||
.getSyncDataDescription(fileSyncData, contentSyncData)),
|
||||
url: workspaceProvider.getSyncDataUrl(fileSyncData, contentSyncData),
|
||||
}, ...current];
|
||||
},
|
||||
|
@ -1,3 +1,5 @@
|
||||
import providerRegistry from '../services/providers/common/providerRegistry';
|
||||
|
||||
const defaultTimeout = 5000;
|
||||
|
||||
export default {
|
||||
@ -34,7 +36,8 @@ export default {
|
||||
} else if (error.status) {
|
||||
const location = rootState.queue.currentLocation;
|
||||
if (location.providerId) {
|
||||
item.content = `HTTP error ${error.status} on ${location.providerId} location.`;
|
||||
const provider = providerRegistry.providersById[location.providerId];
|
||||
item.content = `HTTP error ${error.status} on ${provider.name} location.`;
|
||||
} else {
|
||||
item.content = `HTTP error ${error.status}.`;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../data/emptySyncedContent';
|
||||
import empty from '../data/empties/emptySyncedContent';
|
||||
|
||||
const module = moduleTemplate(empty, true);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import utils from '../services/utils';
|
||||
import providerRegistry from '../services/providers/common/providerRegistry';
|
||||
import constants from '../data/constants';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
@ -22,14 +23,14 @@ export default {
|
||||
Object.entries(rootGetters['data/workspaces']).forEach(([id, workspace]) => {
|
||||
const sanitizedWorkspace = {
|
||||
id,
|
||||
providerId: mainWorkspaceToken && 'googleDriveAppData',
|
||||
providerId: 'googleDriveAppData',
|
||||
sub: mainWorkspaceToken && mainWorkspaceToken.sub,
|
||||
...workspace,
|
||||
};
|
||||
// Filter workspaces that don't have a provider
|
||||
const workspaceProvider = providerRegistry.providersById[sanitizedWorkspace.providerId];
|
||||
if (workspaceProvider) {
|
||||
// Rebuild the url with the current hostname
|
||||
// Build the url with the current hostname
|
||||
const params = workspaceProvider.getWorkspaceParams(sanitizedWorkspace);
|
||||
sanitizedWorkspace.url = utils.addQueryParams('app', params, true);
|
||||
sanitizedWorkspace.locationUrl = workspaceProvider
|
||||
@ -81,7 +82,7 @@ export default {
|
||||
if (!loginToken) {
|
||||
return null;
|
||||
}
|
||||
const prefix = utils.someResult(Object.entries(utils.userIdPrefixes), ([key, value]) => {
|
||||
const prefix = utils.someResult(Object.entries(constants.userIdPrefixes), ([key, value]) => {
|
||||
if (rootGetters[`data/${value}TokensBySub`][loginToken.sub]) {
|
||||
return key;
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
color: inherit;
|
||||
height: 2.5rem;
|
||||
height: 2.4rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
Loading…
Reference in New Issue
Block a user