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