store renamings

This commit is contained in:
Benoit Schweblin 2018-06-21 20:16:33 +01:00
parent e05e7717eb
commit 7a87015af1
63 changed files with 672 additions and 594 deletions

View File

@ -1,15 +1,15 @@
<template> <template>
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu"> <div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--folder': node.isFolder, 'explorer-node--open': isOpen, 'explorer-node--trash': node.isTrash, 'explorer-node--temp': node.isTemp, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent> <div class="explorer-node__item-editor" v-if="isEditing" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName"> <input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
</div> </div>
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()"> <div class="explorer-node__item" v-else :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
{{node.item.name}} {{node.item.name}}
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider> <icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
</div> </div>
<div class="explorer-node__children" v-if="node.isFolder && isOpen"> <div class="explorer-node__children" v-if="node.isFolder && isOpen">
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node> <explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{paddingLeft: childLeftPadding}"> <div v-if="newChild" class="explorer-node__new-child" :class="{'explorer-node__new-child--folder': newChild.isFolder}" :style="{paddingLeft: childLeftPadding}">
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc="submitNewChild(true)" v-model.trim="newChildName"> <input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc="submitNewChild(true)" v-model.trim="newChildName">
</div> </div>
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node> <explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
@ -227,18 +227,26 @@ $item-font-size: 14px;
} }
} }
.explorer-node__item--folder, .explorer-node--trash,
.explorer-node__item-editor--folder, .explorer-node--temp {
color: rgba(0, 0, 0, 0.5);
}
.explorer-node--folder > .explorer-node__item,
.explorer-node--folder > .explorer-node__item-editor,
.explorer-node__new-child--folder { .explorer-node__new-child--folder {
&::before { &::before {
content: '▹'; content: '▹';
position: absolute; position: absolute;
margin-left: -13px; margin-left: -13px;
.explorer-node--open > & {
content: '▾';
} }
} }
.explorer-node--folder.explorer-node--open > .explorer-node__item,
.explorer-node--folder.explorer-node--open > .explorer-node__item-editor {
&::before {
content: '▾';
}
} }
$new-child-height: 25px; $new-child-height: 25px;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="modal" v-if="config" @keydown.esc="onEscape" @keydown.tab="onTab"> <div class="modal" v-if="config" @keydown.esc="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
<component v-if="currentModalComponent" :is="currentModalComponent"></component> <component v-if="currentModalComponent" :is="currentModalComponent"></component>
<modal-inner v-else aria-label="Dialog"> <modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div> <div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
@ -138,8 +138,8 @@ export default {
const isFocusIn = evt.type === 'focusin'; const isFocusIn = evt.type === 'focusin';
if (evt.target.parentNode && evt.target.parentNode.parentNode) { if (evt.target.parentNode && evt.target.parentNode.parentNode) {
// Focus effect // Focus effect
if (evt.target.parentNode.classList.contains('form-entry__field') && if (evt.target.parentNode.classList.contains('form-entry__field')
evt.target.parentNode.parentNode.classList.contains('form-entry')) { && evt.target.parentNode.parentNode.classList.contains('form-entry')) {
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn); evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn);
} }
} }
@ -159,19 +159,15 @@ export default {
mounted() { mounted() {
this.$watch( this.$watch(
() => this.config, () => this.config,
() => { (isOpen) => {
if (this.$el) { if (isOpen) {
window.addEventListener('focusin', this.onFocusInOut);
window.addEventListener('focusout', this.onFocusInOut);
const tabbables = getTabbables(this.$el); const tabbables = getTabbables(this.$el);
if (tabbables[0]) { if (tabbables[0]) {
tabbables[0].focus(); tabbables[0].focus();
} }
} else {
window.removeEventListener('focusin', this.onFocusInOut);
window.removeEventListener('focusout', this.onFocusInOut);
} }
}, },
{ immediate: true },
); );
}, },
}; };

View File

@ -10,7 +10,7 @@ export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
url() { url() {
const userInfo = this.$store.state.userInfo.itemMap[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}')`;
}, },
}, },

View File

@ -9,7 +9,7 @@ export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
name() { name() {
const userInfo = this.$store.state.userInfo.itemMap[this.userId]; const userInfo = this.$store.state.userInfo.itemsById[this.userId];
return userInfo ? userInfo.name : 'Someone'; return userInfo ? userInfo.name : 'Someone';
}, },
}, },

View File

@ -89,7 +89,7 @@ export default {
async templates() { async templates() {
try { try {
const { templates } = await this.$store.dispatch('modal/open', 'templates'); const { templates } = await this.$store.dispatch('modal/open', 'templates');
this.$store.dispatch('data/setTemplates', templates); this.$store.dispatch('data/setTemplatesById', templates);
} catch (e) { } catch (e) {
// Cancel // Cancel
} }

View File

@ -146,22 +146,22 @@ export default {
return this.$store.getters['file/current'].name; return this.$store.getters['file/current'].name;
}, },
googleDriveTokens() { googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive); return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
}, },
dropboxTokens() { dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokens']); return tokensToArray(this.$store.getters['data/dropboxTokensBySub']);
}, },
githubTokens() { githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokens']); return tokensToArray(this.$store.getters['data/githubTokensBySub']);
}, },
wordpressTokens() { wordpressTokens() {
return tokensToArray(this.$store.getters['data/wordpressTokens']); return tokensToArray(this.$store.getters['data/wordpressTokensBySub']);
}, },
bloggerTokens() { bloggerTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isBlogger); return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isBlogger);
}, },
zendeskTokens() { zendeskTokens() {
return tokensToArray(this.$store.getters['data/zendeskTokens']); return tokensToArray(this.$store.getters['data/zendeskTokensBySub']);
}, },
noToken() { noToken() {
return !this.googleDriveTokens.length return !this.googleDriveTokens.length

View File

@ -122,13 +122,13 @@ export default {
return this.$store.getters['file/current'].name; return this.$store.getters['file/current'].name;
}, },
googleDriveTokens() { googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive); return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
}, },
dropboxTokens() { dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokens']); return tokensToArray(this.$store.getters['data/dropboxTokensBySub']);
}, },
githubTokens() { githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokens']); return tokensToArray(this.$store.getters['data/githubTokensBySub']);
}, },
noToken() { noToken() {
return !this.googleDriveTokens.length return !this.googleDriveTokens.length

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<div class="workspace" v-for="(workspace, id) in sanitizedWorkspaces" :key="id"> <div class="workspace" v-for="(workspace, id) in sanitizedWorkspacesById" :key="id">
<menu-entry :href="workspace.url" target="_blank"> <menu-entry :href="workspace.url" target="_blank">
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider> <icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div> <div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div>
@ -37,7 +37,7 @@ export default {
}, },
computed: { computed: {
...mapGetters('data', [ ...mapGetters('data', [
'sanitizedWorkspaces', 'sanitizedWorkspaceById',
]), ]),
...mapGetters('workspace', [ ...mapGetters('workspace', [
'currentWorkspace', 'currentWorkspace',

View File

@ -4,7 +4,7 @@
<p>Please choose a template for your <b>HTML export</b>.</p> <p>Please choose a template for your <b>HTML export</b>.</p>
<form-entry label="Template"> <form-entry label="Template">
<select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()"> <select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -41,7 +41,7 @@ export default modalTemplate({
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
const html = await exportSvc.applyTemplate( const html = await exportSvc.applyTemplate(
currentFile.id, currentFile.id,
this.allTemplates[selectedTemplate], this.allTemplatesById[selectedTemplate],
); );
this.result = html; this.result = html;
}, 10); }, 10);
@ -60,7 +60,7 @@ export default modalTemplate({
const { config } = this; const { config } = this;
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
config.resolve(); config.resolve();
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]); exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
}, },
}, },
}); });

View File

@ -36,7 +36,7 @@ export default modalTemplate({
}), }),
computed: { computed: {
googlePhotosTokens() { googlePhotosTokens() {
const googleTokens = this.$store.getters['data/googleTokens']; const googleTokens = this.$store.getters['data/googleTokensBySub'];
return Object.entries(googleTokens) return Object.entries(googleTokens)
.map(([, token]) => token) .map(([, token]) => token)
.filter(token => token.isPhotos) .filter(token => token.isPhotos)

View File

@ -4,7 +4,7 @@
<p>Please choose a template for your <b>PDF export</b>.</p> <p>Please choose a template for your <b>PDF export</b>.</p>
<form-entry label="Template"> <form-entry label="Template">
<select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()"> <select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -45,7 +45,7 @@ export default modalTemplate({
sponsorSvc.getToken(), sponsorSvc.getToken(),
exportSvc.applyTemplate( exportSvc.applyTemplate(
currentFile.id, currentFile.id,
this.allTemplates[this.selectedTemplate], this.allTemplatesById[this.selectedTemplate],
true, true,
), ),
])); ]));

View File

@ -91,11 +91,11 @@ export default {
}, },
created() { created() {
this.$watch( this.$watch(
() => this.$store.getters['data/allTemplates'], () => this.$store.getters['data/allTemplatesById'],
(allTemplates) => { (allTemplatesById) => {
const templates = {}; const templates = {};
// Sort templates by name // Sort templates by name
Object.entries(allTemplates) Object.entries(allTemplatesById)
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name)) .sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
.forEach(([id, template]) => { .forEach(([id, template]) => {
const templateClone = utils.deepCopy(template); const templateClone = utils.deepCopy(template);

View File

@ -1,7 +1,7 @@
<template> <template>
<modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces"> <modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces">
<div class="modal__content"> <div class="modal__content">
<div class="workspace-entry flex flex--row flex--align-center" v-for="(workspace, id) in sanitizedWorkspaces" :key="id"> <div class="workspace-entry flex flex--row flex--align-center" v-for="(workspace, id) in sanitizedWorkspacesById" :key="id">
<div class="workspace-entry__icon flex flex--column flex--center"> <div class="workspace-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="workspace.providerId"></icon-provider> <icon-provider :provider-id="workspace.providerId"></icon-provider>
</div> </div>
@ -48,8 +48,8 @@ export default {
'config', 'config',
]), ]),
...mapGetters('data', [ ...mapGetters('data', [
'workspaces', 'workspacesById',
'sanitizedWorkspaces', 'sanitizedWorkspacesById',
]), ]),
...mapGetters('workspace', [ ...mapGetters('workspace', [
'mainWorkspace', 'mainWorkspace',
@ -59,12 +59,12 @@ export default {
methods: { methods: {
edit(id) { edit(id) {
this.editedId = id; this.editedId = id;
this.editingName = this.workspaces[id].name; this.editingName = this.workspacesById[id].name;
}, },
submitEdit(cancel) { submitEdit(cancel) {
const workspace = this.workspaces[this.editedId]; const workspace = this.workspacesById[this.editedId];
if (workspace && !cancel && this.editingName) { if (workspace && !cancel && this.editingName) {
this.$store.dispatch('data/patchWorkspaces', { this.$store.dispatch('data/patchWorkspacesById', {
[this.editedId]: { [this.editedId]: {
...workspace, ...workspace,
name: this.editingName, name: this.editingName,

View File

@ -5,7 +5,7 @@
<icon-close></icon-close> <icon-close></icon-close>
</button> </button>
<div class="modal__sponsor-button" v-if="showSponsorButton"> <div class="modal__sponsor-button" v-if="showSponsorButton">
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>. Please consider StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>, please consider
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5. <a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
</div> </div>
<slot></slot> <slot></slot>
@ -51,11 +51,11 @@ export default {
.modal__close-button { .modal__close-button {
position: absolute; position: absolute;
top: 8px; top: 7px;
right: 8px; right: 7px;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
width: 30px; width: 28px;
height: 30px; height: 28px;
padding: 2px; padding: 2px;
&:active, &:active,

View File

@ -52,15 +52,15 @@ export default (desc) => {
}, },
}; };
if (key === 'selectedTemplate') { if (key === 'selectedTemplate') {
component.computed.allTemplates = () => { component.computed.allTemplatesById = () => {
const allTemplates = store.getters['data/allTemplates']; const allTemplatesById = store.getters['data/allTemplatesById'];
const sortedTemplates = {}; const sortedTemplatesById = {};
Object.entries(allTemplates) Object.entries(allTemplatesById)
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name)) .sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
.forEach(([templateId, template]) => { .forEach(([templateId, template]) => {
sortedTemplates[templateId] = template; sortedTemplatesById[templateId] = template;
}); });
return sortedTemplates; return sortedTemplatesById;
}; };
// Make use of `function` to have `this` bound to the component // Make use of `function` to have `this` bound to the component
component.methods.configureTemplates = async function () { // eslint-disable-line func-names component.methods.configureTemplates = async function () { // eslint-disable-line func-names
@ -68,7 +68,7 @@ export default (desc) => {
type: 'templates', type: 'templates',
selectedId: this.selectedTemplate, selectedId: this.selectedTemplate,
}); });
store.dispatch('data/setTemplates', templates); store.dispatch('data/setTemplatesById', templates);
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
[id]: selectedId, [id]: selectedId,
}); });

View File

@ -16,7 +16,7 @@
</form-entry> </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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -16,7 +16,7 @@
</form-entry> </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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -45,7 +45,7 @@ export default modalTemplate({
name: this.name, name: this.name,
password: this.password, password: this.password,
}; };
this.$store.dispatch('data/setCouchdbToken', token); this.$store.dispatch('data/addCouchdbToken', token);
this.config.resolve(); this.config.resolve();
} }
}, },

View File

@ -14,7 +14,7 @@
</form-entry> </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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -23,7 +23,7 @@
</form-entry> </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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -8,7 +8,7 @@
<div class="form-entry"> <div class="form-entry">
<div class="form-entry__checkbox"> <div class="form-entry__checkbox">
<label> <label>
<input type="checkbox" v-model="repoFullAccess"> Grant access to my <b>private repositories</b> <input type="checkbox" v-model="repoFullAccess"> Grant access to your private repositories
</label> </label>
</div> </div>
</div> </div>

View File

@ -26,7 +26,7 @@
</form-entry> </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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -34,7 +34,7 @@
</div> </div>
<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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -17,7 +17,7 @@
</form-entry> </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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -22,7 +22,7 @@
</form-entry> </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 allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>

View File

@ -1,6 +1,6 @@
export default () => ({ export default () => ({
main: { main: {
name: 'Main workspace', name: 'Main workspace',
// The rest will be filled by the data/sanitizedWorkspaces getter // The rest will be filled by the data/sanitizedWorkspacesById getter
}, },
}); });

View File

@ -82,7 +82,7 @@ export default {
if (doClose) { if (doClose) {
// Close the current file by opening the last opened, not deleted one // Close the current file by opening the last opened, not deleted one
store.getters['data/lastOpenedIds'].some((id) => { store.getters['data/lastOpenedIds'].some((id) => {
const file = store.state.file.itemMap[id]; const file = store.state.file.itemsById[id];
if (file.parentId === 'trash') { if (file.parentId === 'trash') {
return false; return false;
} }

View File

@ -46,7 +46,7 @@ export default {
value: '{{{files.0.content.text}}}', value: '{{{files.0.content.text}}}',
helpers: '', helpers: '',
}, pdf = false) { }, pdf = false) {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemsById[fileId];
const content = await localDbSvc.loadItem(`${fileId}/content`); const content = await localDbSvc.loadItem(`${fileId}/content`);
const properties = utils.computeProperties(content.properties); const properties = utils.computeProperties(content.properties);
const options = extensionSvc.getOptions(properties); const options = extensionSvc.getOptions(properties);
@ -114,7 +114,7 @@ export default {
* Export a file to disk. * Export a file to disk.
*/ */
async exportToDisk(fileId, type, template) { async exportToDisk(fileId, type, template) {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemsById[fileId];
const html = await this.applyTemplate(fileId, template); const html = await this.applyTemplate(fileId, template);
const blob = new Blob([html], { const blob = new Blob([html], {
type: 'text/plain;charset=utf-8', type: 'text/plain;charset=utf-8',

View File

@ -43,9 +43,9 @@ export default {
// Check if there is already a file with that path // Check if there is already a file with that path
if (workspaceUniquePaths) { if (workspaceUniquePaths) {
const parentPath = store.getters.itemPaths[item.parentId] || ''; const parentPath = store.getters.pathsByItemId[item.parentId] || '';
const path = parentPath + item.name; const path = parentPath + item.name;
if (store.getters.pathItems[path]) { if (store.getters.itemsByPath[path]) {
await store.dispatch('modal/open', { await store.dispatch('modal/open', {
type: 'pathConflict', type: 'pathConflict',
item, item,
@ -62,7 +62,7 @@ export default {
} }
// Return the new file item // Return the new file item
return store.state.file.itemMap[id]; return store.state.file.itemsById[id];
}, },
/** /**
@ -88,12 +88,13 @@ export default {
item, item,
}); });
} }
// Check if there is a path conflict // Check if there is a path conflict
if (store.getters['workspace/hasUniquePaths']) { if (store.getters['workspace/hasUniquePaths']) {
const parentPath = store.getters.itemPaths[item.parentId] || ''; const parentPath = store.getters.pathsByItemId[item.parentId] || '';
const path = parentPath + sanitizedName; const path = parentPath + sanitizedName;
const pathItems = store.getters.pathItems[path] || []; const items = store.getters.itemsByPath[path] || [];
if (pathItems.some(itemWithSamePath => itemWithSamePath.id !== id)) { if (items.some(itemWithSamePath => itemWithSamePath.id !== id)) {
await store.dispatch('modal/open', { await store.dispatch('modal/open', {
type: 'pathConflict', type: 'pathConflict',
item, item,
@ -112,7 +113,7 @@ export default {
*/ */
setOrPatchItem(patch) { setOrPatchItem(patch) {
const item = { const item = {
...store.getters.allItemMap[patch.id] || patch, ...store.getters.allItemsById[patch.id] || patch,
}; };
if (!item.id) { if (!item.id) {
return null; return null;
@ -136,7 +137,7 @@ export default {
this.makePathUnique(item.id); this.makePathUnique(item.id);
} }
return store.getters.allItemMap[item.id]; return store.getters.allItemsById[item.id];
}, },
/** /**
@ -160,12 +161,15 @@ export default {
}, },
/** /**
* Ensure two files/folders don't have the same path if the workspace doesn't support it. * Ensure two files/folders don't have the same path if the workspace doesn't allow it.
*/ */
ensureUniquePaths() { ensureUniquePaths(idsToKeep = {}) {
if (store.getters['workspace/hasUniquePaths']) { if (store.getters['workspace/hasUniquePaths']) {
if (Object.keys(store.getters.itemPaths).some(id => this.makePathUnique(id))) { if (Object.keys(store.getters.pathsByItemId)
this.ensureUniquePaths(); .some(id => !idsToKeep[id] && this.makePathUnique(id))
) {
// Just changed one item path, restart
this.ensureUniquePaths(idsToKeep);
} }
} }
}, },
@ -175,13 +179,13 @@ export default {
* Add a prefix to its name and return true otherwise. * Add a prefix to its name and return true otherwise.
*/ */
makePathUnique(id) { makePathUnique(id) {
const { pathItems, allItemMap, itemPaths } = store.getters; const { itemsByPath, allItemsById, pathsByItemId } = store.getters;
const item = allItemMap[id]; const item = allItemsById[id];
if (!item) { if (!item) {
return false; return false;
} }
let path = itemPaths[id]; let path = pathsByItemId[id];
if (pathItems[path].length === 1) { if (itemsByPath[path].length === 1) {
return false; return false;
} }
const isFolder = item.type === 'folder'; const isFolder = item.type === 'folder';
@ -190,11 +194,11 @@ export default {
path = path.slice(0, -1); path = path.slice(0, -1);
} }
for (let suffix = 1; ; suffix += 1) { for (let suffix = 1; ; suffix += 1) {
let pathWithPrefix = `${path}.${suffix}`; let pathWithSuffix = `${path}.${suffix}`;
if (isFolder) { if (isFolder) {
pathWithPrefix += '/'; pathWithSuffix += '/';
} }
if (!pathItems[pathWithPrefix]) { if (!itemsByPath[pathWithSuffix]) {
store.commit(`${item.type}/patchItem`, { store.commit(`${item.type}/patchItem`, {
id: item.id, id: item.id,
name: `${item.name}.${suffix}`, name: `${item.name}.${suffix}`,

View File

@ -123,7 +123,7 @@ const localDbSvc = {
} }
// Write item if different from stored one // Write item if different from stored one
const item = store.state.data.lsItemMap[id]; const item = store.state.data.lsItemsById[id];
if (item && item.hash !== lsHashMap[id]) { if (item && item.hash !== lsHashMap[id]) {
localStorage.setItem(key, JSON.stringify(item)); localStorage.setItem(key, JSON.stringify(item));
lsHashMap[id] = item.hash; lsHashMap[id] = item.hash;
@ -178,7 +178,7 @@ const localDbSvc = {
changes.push(item); changes.push(item);
cursor.continue(); cursor.continue();
} else { } else {
const storeItemMap = { ...store.getters.allItemMap }; const storeItemMap = { ...store.getters.allItemsById };
changes.forEach((item) => { changes.forEach((item) => {
this.readDbItem(item, storeItemMap); this.readDbItem(item, storeItemMap);
// If item is an old delete marker, remove it from the DB // If item is an old delete marker, remove it from the DB
@ -213,7 +213,7 @@ const localDbSvc = {
checker = cb => (id) => { checker = cb => (id) => {
if (!storeItemMap[id]) { if (!storeItemMap[id]) {
const [fileId] = id.split('/'); const [fileId] = id.split('/');
if (!store.state.file.itemMap[fileId]) { if (!store.state.file.itemsById[fileId]) {
cb(id); cb(id);
} }
} }
@ -277,7 +277,7 @@ const localDbSvc = {
*/ */
async loadItem(id) { async loadItem(id) {
// Check if item is in the store // Check if item is in the store
const itemInStore = store.getters.allItemMap[id]; const itemInStore = store.getters.allItemsById[id];
if (itemInStore) { if (itemInStore) {
// Use deepCopy to freeze item // Use deepCopy to freeze item
return Promise.resolve(itemInStore); return Promise.resolve(itemInStore);
@ -326,11 +326,11 @@ const localDbSvc = {
* Drop the database and clean the localStorage for the specified workspaceId. * Drop the database and clean the localStorage for the specified workspaceId.
*/ */
async removeWorkspace(id) { async removeWorkspace(id) {
const workspaces = { const workspacesById = {
...store.getters['data/workspaces'], ...store.getters['data/workspacesById'],
}; };
delete workspaces[id]; delete workspacesById[id];
store.dispatch('data/setWorkspaces', workspaces); store.dispatch('data/setWorkspacesById', workspacesById);
this.syncLocalStorage(); this.syncLocalStorage();
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const dbName = getDbName(id); const dbName = getDbName(id);
@ -348,7 +348,7 @@ const localDbSvc = {
async init() { async init() {
// Reset the app if reset flag was passed // Reset the app if reset flag was passed
if (resetApp) { if (resetApp) {
await Promise.all(Object.keys(store.getters['data/workspaces']) await Promise.all(Object.keys(store.getters['data/workspacesById'])
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId))); .map(workspaceId => localDbSvc.removeWorkspace(workspaceId)));
utils.localStorageDataIds.forEach((id) => { utils.localStorageDataIds.forEach((id) => {
// Clean data stored in localStorage // Clean data stored in localStorage
@ -366,7 +366,7 @@ const localDbSvc = {
// If exportWorkspace parameter was provided // If exportWorkspace parameter was provided
if (exportWorkspace) { if (exportWorkspace) {
const backup = JSON.stringify(store.getters.allItemMap); const backup = JSON.stringify(store.getters.allItemsById);
const blob = new Blob([backup], { const blob = new Blob([backup], {
type: 'text/plain;charset=utf-8', type: 'text/plain;charset=utf-8',
}); });
@ -405,7 +405,7 @@ const localDbSvc = {
// Force check sponsorship after a few seconds // Force check sponsorship after a few seconds
const currentDate = Date.now(); const currentDate = Date.now();
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) { if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
store.dispatch('data/setGoogleToken', { store.dispatch('data/addGoogleToken', {
...sponsorToken, ...sponsorToken,
expiresOn: currentDate - checkSponsorshipAfter, expiresOn: currentDate - checkSponsorshipAfter,
}); });

View File

@ -5,7 +5,7 @@ import Provider from './common/Provider';
export default new Provider({ export default new Provider({
id: 'bloggerPage', id: 'bloggerPage',
getToken(location) { getToken(location) {
const token = store.getters['data/googleTokens'][location.sub]; const token = store.getters['data/googleTokensBySub'][location.sub];
return token && token.isBlogger ? token : null; return token && token.isBlogger ? token : null;
}, },
getUrl(location) { getUrl(location) {

View File

@ -5,7 +5,7 @@ import Provider from './common/Provider';
export default new Provider({ export default new Provider({
id: 'blogger', id: 'blogger',
getToken(location) { getToken(location) {
const token = store.getters['data/googleTokens'][location.sub]; const token = store.getters['data/googleTokensBySub'][location.sub];
return token && token.isBlogger ? token : null; return token && token.isBlogger ? token : null;
}, },
getUrl(location) { getUrl(location) {

View File

@ -7,6 +7,9 @@ import fileSvc from '../../fileSvc';
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/; const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
export default class Provider { export default class Provider {
prepareChanges = changes => changes
onChangesApplied = () => {}
constructor(props) { constructor(props) {
Object.assign(this, props); Object.assign(this, props);
providerRegistry.register(this); providerRegistry.register(this);
@ -41,7 +44,7 @@ export default class Provider {
* Parse content serialized with serializeContent() * Parse content serialized with serializeContent()
*/ */
static parseContent(serializedContent, id) { static parseContent(serializedContent, id) {
const result = utils.deepCopy(store.state.content.itemMap[id]) || emptyContent(id); const result = utils.deepCopy(store.state.content.itemsById[id]) || emptyContent(id);
result.text = utils.sanitizeText(serializedContent); result.text = utils.sanitizeText(serializedContent);
result.history = []; result.history = [];
const extractedData = dataExtractor.exec(serializedContent); const extractedData = dataExtractor.exec(serializedContent);
@ -82,7 +85,7 @@ export default class Provider {
const location = utils.search(allLocations, criteria); const location = utils.search(allLocations, criteria);
if (location) { if (location) {
// Found one, open it if it exists // Found one, open it if it exists
const item = store.state.file.itemMap[location.fileId]; const item = store.state.file.itemsById[location.fileId];
if (item) { if (item) {
store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
// If file is in the trash, restore it // If file is in the trash, restore it

View File

@ -17,12 +17,12 @@ export default new Provider({
dbUrl, dbUrl,
}; };
const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspaceId = utils.makeWorkspaceId(workspaceParams);
const getToken = () => store.getters['data/couchdbTokens'][workspaceId]; const getToken = () => store.getters['data/couchdbTokensBySub'][workspaceId];
const getWorkspace = () => store.getters['data/sanitizedWorkspaces'][workspaceId]; const getWorkspace = () => store.getters['data/sanitizedWorkspacesById'][workspaceId];
if (!getToken()) { if (!getToken()) {
// Create token // Create token
store.dispatch('data/setCouchdbToken', { store.dispatch('data/addCouchdbToken', {
sub: workspaceId, sub: workspaceId,
dbUrl, dbUrl,
}); });
@ -38,7 +38,7 @@ export default new Provider({
} catch (e) { } catch (e) {
throw new Error(`${dbUrl} is not accessible. Make sure you have the proper permissions.`); throw new Error(`${dbUrl} is not accessible. Make sure you have the proper permissions.`);
} }
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspacesById', {
[workspaceId]: { [workspaceId]: {
id: workspaceId, id: workspaceId,
name: db.db_name, name: db.db_name,
@ -52,7 +52,7 @@ export default new Provider({
// Fix the URL hash // Fix the URL hash
utils.setQueryParams(workspaceParams); utils.setQueryParams(workspaceParams);
if (workspace.url !== window.location.href) { if (workspace.url !== window.location.href) {
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspacesById', {
[workspace.id]: { [workspace.id]: {
...workspace, ...workspace,
url: window.location.href, url: window.location.href,
@ -91,7 +91,7 @@ export default new Provider({
syncLastSeq, syncLastSeq,
}); });
}, },
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 } = couchdbHelper.uploadDocument({
token: syncToken, token: syncToken,
@ -108,24 +108,24 @@ export default new Provider({
rev, rev,
}; };
}, },
removeWorkspaceItem(syncData) { removeWorkspaceItem({ syncData }) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return couchdbHelper.removeDocument(syncToken, syncData.id, syncData.rev); return couchdbHelper.removeDocument(syncToken, syncData.id, syncData.rev);
}, },
async downloadWorkspaceContent(token, syncData) { async downloadWorkspaceContent({ token, contentSyncData }) {
const body = await couchdbHelper.retrieveDocumentWithAttachments(token, syncData.id); const body = await couchdbHelper.retrieveDocumentWithAttachments(token, contentSyncData.id);
const rev = body._rev; // eslint-disable-line no-underscore-dangle const rev = body._rev; // eslint-disable-line no-underscore-dangle
const item = Provider.parseContent(body.attachments.data, body.item.id); const content = Provider.parseContent(body.attachments.data, body.item.id);
return { return {
item, content,
syncData: { contentSyncData: {
...syncData, ...contentSyncData,
hash: item.hash, hash: content.hash,
rev, rev,
}, },
}; };
}, },
async downloadWorkspaceData(token, dataId, syncData) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
} }
@ -142,30 +142,32 @@ export default new Provider({
}, },
}; };
}, },
async uploadWorkspaceContent(token, item, syncData) { async uploadWorkspaceContent({ token, content, contentSyncData }) {
const res = await couchdbHelper.uploadDocument({ const res = await couchdbHelper.uploadDocument({
token, token,
item: { item: {
id: item.id, id: content.id,
type: item.type, type: content.type,
hash: item.hash, hash: content.hash,
}, },
data: Provider.serializeContent(item), data: Provider.serializeContent(content),
dataType: 'text/plain', dataType: 'text/plain',
documentId: syncData && syncData.id, documentId: contentSyncData && contentSyncData.id,
rev: syncData && syncData.rev, rev: contentSyncData && contentSyncData.rev,
}); });
// Return new sync data // Return new sync data
return { return {
contentSyncData: {
id: res.id, id: res.id,
itemId: item.id, itemId: content.id,
type: item.type, type: content.type,
hash: item.hash, hash: content.hash,
rev: res.rev, rev: res.rev,
},
}; };
}, },
async uploadWorkspaceData(token, item, syncData) { async uploadWorkspaceData({ token, item, syncData }) {
const res = await couchdbHelper.uploadDocument({ const res = await couchdbHelper.uploadDocument({
token, token,
item: { item: {
@ -181,11 +183,13 @@ export default new Provider({
// Return new sync data // Return new sync data
return { return {
syncData: {
id: res.id, id: res.id,
itemId: item.id, itemId: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
rev: res.rev, rev: res.rev,
},
}; };
}, },
async listRevisions(token, fileId) { async listRevisions(token, fileId) {

View File

@ -20,7 +20,7 @@ const makePathRelative = (token, path) => {
export default new Provider({ export default new Provider({
id: 'dropbox', id: 'dropbox',
getToken(location) { getToken(location) {
return store.getters['data/dropboxTokens'][location.sub]; return store.getters['data/dropboxTokensBySub'][location.sub];
}, },
getUrl(location) { getUrl(location) {
const pathComponents = location.path.split('/').map(encodeURIComponent); const pathComponents = location.path.split('/').map(encodeURIComponent);

View File

@ -6,7 +6,7 @@ import utils from '../utils';
export default new Provider({ export default new Provider({
id: 'gist', id: 'gist',
getToken(location) { getToken(location) {
return store.getters['data/githubTokens'][location.sub]; return store.getters['data/githubTokensBySub'][location.sub];
}, },
getUrl(location) { getUrl(location) {
return `https://gist.github.com/${location.gistId}`; return `https://gist.github.com/${location.gistId}`;
@ -23,7 +23,7 @@ export default new Provider({
return Provider.parseContent(content, `${syncLocation.fileId}/content`); return Provider.parseContent(content, `${syncLocation.fileId}/content`);
}, },
async uploadContent(token, content, syncLocation) { async uploadContent(token, content, syncLocation) {
const file = store.state.file.itemMap[syncLocation.fileId]; const file = store.state.file.itemsById[syncLocation.fileId];
const description = utils.sanitizeName(file && file.name); const description = utils.sanitizeName(file && file.name);
const gist = await githubHelper.uploadGist({ const gist = await githubHelper.uploadGist({
...syncLocation, ...syncLocation,

View File

@ -9,7 +9,7 @@ const savedSha = {};
export default new Provider({ export default new Provider({
id: 'github', id: 'github',
getToken(location) { getToken(location) {
return store.getters['data/githubTokens'][location.sub]; return store.getters['data/githubTokensBySub'][location.sub];
}, },
getUrl(location) { getUrl(location) {
return `https://github.com/${encodeURIComponent(location.owner)}/${encodeURIComponent(location.repo)}/blob/${encodeURIComponent(location.branch)}/${encodeURIComponent(location.path)}`; return `https://github.com/${encodeURIComponent(location.owner)}/${encodeURIComponent(location.repo)}/blob/${encodeURIComponent(location.branch)}/${encodeURIComponent(location.path)}`;
@ -20,12 +20,12 @@ export default new Provider({
}, },
async downloadContent(token, syncLocation) { async downloadContent(token, syncLocation) {
try { try {
const { sha, content } = await githubHelper.downloadFile({ const { sha, data } = await githubHelper.downloadFile({
...syncLocation, ...syncLocation,
token, token,
}); });
savedSha[syncLocation.id] = sha; savedSha[syncLocation.id] = sha;
return Provider.parseContent(content, `${syncLocation.fileId}/content`); return Provider.parseContent(data, `${syncLocation.fileId}/content`);
} catch (e) { } catch (e) {
// Ignore error, upload is going to fail anyway // Ignore error, upload is going to fail anyway
return null; return null;

View File

@ -28,7 +28,6 @@ const endsWith = (str, suffix) => str.slice(-suffix.length) === suffix;
export default new Provider({ export default new Provider({
id: 'githubWorkspace', id: 'githubWorkspace',
isGit: true,
getToken() { getToken() {
return store.getters['workspace/syncToken']; return store.getters['workspace/syncToken'];
}, },
@ -47,13 +46,13 @@ export default new Provider({
workspaceParams.path = path; workspaceParams.path = path;
} }
const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspaceId = utils.makeWorkspaceId(workspaceParams);
let workspace = store.getters['data/sanitizedWorkspaces'][workspaceId]; let workspace = store.getters['data/sanitizedWorkspacesById'][workspaceId];
// See if we already have a token // See if we already have a token
let token; let token;
if (workspace) { if (workspace) {
// Token sub is in the workspace // Token sub is in the workspace
token = store.getters['data/githubTokens'][workspace.sub]; token = store.getters['data/githubTokensBySub'][workspace.sub];
} }
if (!token) { if (!token) {
await store.dispatch('modal/open', { type: 'githubAccount' }); await store.dispatch('modal/open', { type: 'githubAccount' });
@ -74,27 +73,27 @@ export default new Provider({
// Fix the URL hash // Fix the URL hash
utils.setQueryParams(workspaceParams); utils.setQueryParams(workspaceParams);
if (workspace.url !== window.location.href) { if (workspace.url !== window.location.href) {
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspacesById', {
[workspaceId]: { [workspaceId]: {
...workspace, ...workspace,
url: window.location.href, url: window.location.href,
}, },
}); });
} }
return store.getters['data/sanitizedWorkspaces'][workspaceId]; return store.getters['data/sanitizedWorkspacesById'][workspaceId];
}, },
async getChanges() { getChanges() {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const { owner, repo, branch } = getWorkspaceWithOwner(); const { owner, repo, branch } = getWorkspaceWithOwner();
const tree = await githubHelper.getTree({ return githubHelper.getTree({
token: syncToken, token: syncToken,
owner, owner,
repo, repo,
branch, branch,
}); });
},
prepareChanges(tree) {
const workspacePath = store.getters['workspace/currentWorkspace'].path || ''; const workspacePath = store.getters['workspace/currentWorkspace'].path || '';
const syncDataByPath = store.getters['data/syncData'];
const syncDataByItemId = store.getters['data/syncDataByItemId'];
// Store all blobs sha // Store all blobs sha
treeShaMap = Object.create(null); treeShaMap = Object.create(null);
@ -136,9 +135,20 @@ export default new Provider({
const changes = []; const changes = [];
const pathIds = {}; const pathIds = {};
const syncDataToKeep = Object.create(null); const syncDataToKeep = Object.create(null);
const syncDataByPath = store.getters['data/syncDataById'];
const { itemsByGitPath } = store.getters;
const getId = (path) => { const getId = (path) => {
const syncData = syncDataByPath[path]; const existingItem = itemsByGitPath[path];
const id = syncData ? syncData.itemId : utils.uid(); // Use the item ID only if the item was already synced
if (existingItem && syncDataByPath[path]) {
pathIds[path] = existingItem.id;
return existingItem.id;
}
// Generate a new ID
let id = utils.uid();
if (path[0] === '/') {
id += '/content';
}
pathIds[path] = id; pathIds[path] = id;
return id; return id;
}; };
@ -146,9 +156,8 @@ export default new Provider({
// 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]) => {
const id = getId(path);
const item = utils.addItemHash({ const item = utils.addItemHash({
id, id: getId(path),
type: 'folder', type: 'folder',
name: path.slice(parentPath.length, -1), name: path.slice(parentPath.length, -1),
parentId: pathIds[parentPath] || null, parentId: pathIds[parentPath] || null,
@ -158,18 +167,22 @@ export default new Provider({
item, item,
syncData: { syncData: {
id: path, id: path,
itemId: id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}, },
}); });
}); });
// File creations/updates // File/content creations/updates
Object.entries(treeFileMap).forEach(([path, parentPath]) => { Object.entries(treeFileMap).forEach(([path, parentPath]) => {
const id = getId(path); // Look for content sync data as it's created before file sync data
const contentPath = `/${path}`;
const contentId = getId(contentPath);
// File creations/updates
const [fileId] = contentId.split('/');
const item = utils.addItemHash({ const item = utils.addItemHash({
id, 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: pathIds[parentPath] || null,
@ -179,31 +192,31 @@ export default new Provider({
item, item,
syncData: { syncData: {
id: path, id: path,
itemId: id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}, },
}); });
// Content creations/updates // Content creations/updates
const contentSyncData = syncDataByItemId[`${id}/content`]; const contentSyncData = syncDataByPath[contentPath];
if (contentSyncData) { if (contentSyncData) {
syncDataToKeep[contentSyncData.id] = true; syncDataToKeep[path] = true;
syncDataToKeep[contentPath] = true;
} }
if (!contentSyncData || contentSyncData.sha !== treeShaMap[path]) { if (!contentSyncData || contentSyncData.sha !== treeShaMap[path]) {
const type = 'content';
// Use `/` as a prefix to get a unique syncData id // Use `/` as a prefix to get a unique syncData id
changes.push({ changes.push({
syncDataId: `/${path}`, syncDataId: contentPath,
item: { item: {
id: `${id}/content`, id: contentId,
type: 'content', type,
// Need a truthy value to force saving sync data // Need a truthy value to force downloading the content
hash: 1, hash: 1,
}, },
syncData: { syncData: {
id: `/${path}`, id: contentPath,
itemId: `${id}/content`, type,
type: 'content',
// Need a truthy value to force downloading the content // Need a truthy value to force downloading the content
hash: 1, hash: 1,
}, },
@ -212,35 +225,34 @@ export default new Provider({
}); });
// Data creations/updates // Data creations/updates
const syncDataByItemId = store.getters['data/syncDataByItemId'];
Object.keys(treeDataMap).forEach((path) => { Object.keys(treeDataMap).forEach((path) => {
try {
// 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) {
pathIds[path] = id; pathIds[path] = id;
const syncData = syncDataByItemId[id]; const syncData = syncDataByItemId[id];
if (syncData) { if (syncData) {
syncDataToKeep[syncData.id] = true; syncDataToKeep[syncData.id] = true;
} }
if (!syncData || syncData.sha !== treeShaMap[path]) { if (!syncData || syncData.sha !== treeShaMap[path]) {
const type = 'data';
changes.push({ changes.push({
syncDataId: path, syncDataId: path,
item: { item: {
id, id,
type: 'data', type,
// Need a truthy value to force saving sync data // Need a truthy value to force saving sync data
hash: 1, hash: 1,
}, },
syncData: { syncData: {
id: path, id: path,
itemId: id, type,
type: 'data',
// Need a truthy value to force downloading the content // Need a truthy value to force downloading the content
hash: 1, hash: 1,
}, },
}); });
} }
} catch (e) {
// Ignore parsing errors
} }
}); });
@ -255,12 +267,18 @@ export default new Provider({
pathMatcher: /^([\s\S]+)\.([\w-]+)\.publish$/, pathMatcher: /^([\s\S]+)\.([\w-]+)\.publish$/,
}] }]
.forEach(({ type, map, pathMatcher }) => Object.keys(map).forEach((path) => { .forEach(({ type, map, pathMatcher }) => Object.keys(map).forEach((path) => {
try { const [, filePath, data] = path.match(pathMatcher) || [];
const [, filePath, data] = path.match(pathMatcher); 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 = pathIds[`${filePath}.md`];
if (fileId) { if (fileId) {
const id = getId(path); // Reuse existing ID or create a new one
const existingItem = itemsByGitPath[path];
const id = existingItem
? 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)),
id, id,
@ -272,14 +290,11 @@ export default new Provider({
item, item,
syncData: { syncData: {
id: path, id: path,
itemId: id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}, },
}); });
} }
} catch (e) {
// Ignore parsing errors
} }
})); }));
@ -292,10 +307,9 @@ export default new Provider({
return changes; return changes;
}, },
async saveWorkspaceItem(item) { async saveWorkspaceItem({ item }) {
const syncData = { const syncData = {
id: store.getters.itemGitPaths[item.id], id: store.getters.gitPathsByItemId[item.id],
itemId: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}; };
@ -316,7 +330,7 @@ export default new Provider({
}); });
return syncData; return syncData;
}, },
async removeWorkspaceItem(syncData) { async removeWorkspaceItem({ syncData }) {
if (treeShaMap[syncData.id]) { if (treeShaMap[syncData.id]) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
await githubHelper.removeFile({ await githubHelper.removeFile({
@ -327,41 +341,40 @@ export default new Provider({
}); });
} }
}, },
async downloadWorkspaceContent(token, contentSyncData) { async downloadWorkspaceContent({
const [fileId] = contentSyncData.itemId.split('/'); token,
const path = store.getters.itemGitPaths[fileId]; contentId,
const syncData = store.getters['data/syncData'][path]; contentSyncData,
if (!syncData) { fileSyncData,
return {}; }) {
} const { sha, data } = await githubHelper.downloadFile({
const { sha, content } = await githubHelper.downloadFile({
...getWorkspaceWithOwner(), ...getWorkspaceWithOwner(),
token, token,
path: getAbsolutePath(syncData), path: getAbsolutePath(fileSyncData),
}); });
treeShaMap[path] = sha; treeShaMap[fileSyncData.id] = sha;
const item = Provider.parseContent(content, `${fileId}/content`); const content = Provider.parseContent(data, contentId);
return { return {
item, content,
syncData: { contentSyncData: {
...contentSyncData, ...contentSyncData,
hash: item.hash, hash: content.hash,
sha, sha,
}, },
}; };
}, },
async downloadWorkspaceData(token, dataId, syncData) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
} }
const { sha, content } = await githubHelper.downloadFile({ const { sha, data } = await githubHelper.downloadFile({
...getWorkspaceWithOwner(), ...getWorkspaceWithOwner(),
token, token,
path: getAbsolutePath(syncData), path: getAbsolutePath(syncData),
}); });
treeShaMap[syncData.id] = sha; treeShaMap[syncData.id] = sha;
const item = JSON.parse(content); const item = JSON.parse(data);
return { return {
item, item,
syncData: { syncData: {
@ -371,32 +384,36 @@ export default new Provider({
}, },
}; };
}, },
async uploadWorkspaceContent(token, item) { async uploadWorkspaceContent({ token, content, file }) {
const [fileId] = item.id.split('/'); const path = store.getters.gitPathsByItemId[file.id];
const path = store.getters.itemGitPaths[fileId];
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
const res = await githubHelper.uploadFile({ const res = await githubHelper.uploadFile({
...getWorkspaceWithOwner(), ...getWorkspaceWithOwner(),
token, token,
path: absolutePath, path: absolutePath,
content: Provider.serializeContent(item), content: Provider.serializeContent(content),
sha: treeShaMap[path], sha: treeShaMap[path],
}); });
// Return new sync data // Return new sync data
return { return {
id: store.getters.itemGitPaths[item.id], contentSyncData: {
itemId: item.id, id: store.getters.gitPathsByItemId[content.id],
type: item.type, type: content.type,
hash: item.hash, hash: content.hash,
sha: res.content.sha, sha: res.content.sha,
},
fileSyncData: {
id: path,
type: 'file',
hash: file.hash,
},
}; };
}, },
async uploadWorkspaceData(token, item) { async uploadWorkspaceData({ token, item }) {
const path = store.getters.itemGitPaths[item.id]; const path = store.getters.gitPathsByItemId[item.id];
const syncData = { const syncData = {
id: path, id: path,
itemId: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}; };
@ -409,8 +426,10 @@ export default new Provider({
}); });
return { return {
syncData: {
...syncData, ...syncData,
sha: res.content.sha, sha: res.content.sha,
},
}; };
}, },
onSyncEnd() { onSyncEnd() {
@ -458,12 +477,12 @@ export default new Provider({
}, },
async getRevisionContent(token, fileId, revisionId) { async getRevisionContent(token, fileId, revisionId) {
const syncData = Provider.getContentSyncData(fileId); const syncData = Provider.getContentSyncData(fileId);
const { content } = await githubHelper.downloadFile({ const { data } = await githubHelper.downloadFile({
...getWorkspaceWithOwner(), ...getWorkspaceWithOwner(),
token, token,
branch: revisionId, branch: revisionId,
path: getAbsolutePath(syncData), path: getAbsolutePath(syncData),
}); });
return Provider.parseContent(content, `${fileId}/content`); return Provider.parseContent(data, `${fileId}/content`);
}, },
}); });

View File

@ -15,7 +15,7 @@ export default new Provider({
// Remove the URL hash // Remove the URL hash
utils.setQueryParams(); utils.setQueryParams();
// Return the main workspace // Return the main workspace
return store.getters['data/workspaces'].main; return store.getters['data/workspacesById'].main;
}, },
async getChanges() { async getChanges() {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
@ -48,7 +48,7 @@ export default new Provider({
syncStartPageToken, syncStartPageToken,
}); });
}, },
async saveWorkspaceItem(item, syncData, ifNotTooLate) { async saveWorkspaceItem({ item, syncData, ifNotTooLate }) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
const file = await googleHelper.uploadAppDataFile({ const file = await googleHelper.uploadAppDataFile({
token: syncToken, token: syncToken,
@ -64,22 +64,22 @@ export default new Provider({
hash: item.hash, hash: item.hash,
}; };
}, },
removeWorkspaceItem(syncData, ifNotTooLate) { removeWorkspaceItem({ syncData, ifNotTooLate }) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
return googleHelper.removeAppDataFile(syncToken, syncData.id, ifNotTooLate); return googleHelper.removeAppDataFile(syncToken, syncData.id, ifNotTooLate);
}, },
async downloadWorkspaceContent(token, syncData) { async downloadWorkspaceContent({ token, contentSyncData }) {
const data = await googleHelper.downloadAppDataFile(token, syncData.id); const data = await googleHelper.downloadAppDataFile(token, contentSyncData.id);
const item = utils.addItemHash(JSON.parse(data)); const content = utils.addItemHash(JSON.parse(data));
return { return {
item, content,
syncData: { contentSyncData: {
...syncData, ...contentSyncData,
hash: item.hash, hash: content.hash,
}, },
}; };
}, },
async downloadWorkspaceData(token, dataId, syncData) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
} }
@ -94,28 +94,40 @@ export default new Provider({
}, },
}; };
}, },
async uploadWorkspaceContent(token, item, syncData, ifNotTooLate) { async uploadWorkspaceContent({
const file = await googleHelper.uploadAppDataFile({ token,
content,
contentSyncData,
ifNotTooLate,
}) {
const gdriveFile = await googleHelper.uploadAppDataFile({
token, token,
name: JSON.stringify({ name: JSON.stringify({
id: item.id, id: content.id,
type: item.type, type: content.type,
hash: item.hash, hash: content.hash,
}), }),
media: JSON.stringify(item), media: JSON.stringify(content),
fileId: syncData && syncData.id, fileId: contentSyncData && contentSyncData.id,
ifNotTooLate, ifNotTooLate,
}); });
// Return new sync data // Return new sync data
return { return {
id: file.id, contentSyncData: {
itemId: item.id, id: gdriveFile.id,
type: item.type, itemId: content.id,
hash: item.hash, type: content.type,
hash: content.hash,
},
}; };
}, },
async uploadWorkspaceData(token, item, syncData, ifNotTooLate) { async uploadWorkspaceData({
token,
item,
syncData,
ifNotTooLate,
}) {
const file = await googleHelper.uploadAppDataFile({ const file = await googleHelper.uploadAppDataFile({
token, token,
name: JSON.stringify({ name: JSON.stringify({
@ -130,10 +142,12 @@ export default new Provider({
// Return new sync data // Return new sync data
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,
},
}; };
}, },
async listRevisions(token, fileId) { async listRevisions(token, fileId) {

View File

@ -7,7 +7,7 @@ import fileSvc from '../fileSvc';
export default new Provider({ export default new Provider({
id: 'googleDrive', id: 'googleDrive',
getToken(location) { getToken(location) {
const token = store.getters['data/googleTokens'][location.sub]; const token = store.getters['data/googleTokensBySub'][location.sub];
return token && token.isDrive ? token : null; return token && token.isDrive ? token : null;
}, },
getUrl(location) { getUrl(location) {
@ -21,7 +21,7 @@ export default new Provider({
const state = googleHelper.driveState || {}; const state = googleHelper.driveState || {};
if (state.userId) { if (state.userId) {
// Try to find the token corresponding to the user ID // Try to find the token corresponding to the user ID
let token = store.getters['data/googleTokens'][state.userId]; let token = store.getters['data/googleTokensBySub'][state.userId];
// If not found or not enough permission, popup an OAuth2 window // If not found or not enough permission, popup an OAuth2 window
if (!token || !token.isDrive) { if (!token || !token.isDrive) {
await store.dispatch('modal/open', { type: 'googleDriveAccount' }); await store.dispatch('modal/open', { type: 'googleDriveAccount' });
@ -42,7 +42,7 @@ export default new Provider({
folderId, folderId,
}; };
const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspaceId = utils.makeWorkspaceId(workspaceParams);
const workspace = store.getters['data/sanitizedWorkspaces'][workspaceId]; const workspace = store.getters['data/sanitizedWorkspacesById'][workspaceId];
// If we have the workspace, open it by changing the current URL // If we have the workspace, open it by changing the current URL
if (workspace) { if (workspace) {
utils.setQueryParams(workspaceParams); utils.setQueryParams(workspaceParams);
@ -83,7 +83,7 @@ export default new Provider({
}, },
async performAction() { async performAction() {
const state = googleHelper.driveState || {}; const state = googleHelper.driveState || {};
const token = store.getters['data/googleTokens'][state.userId]; const token = store.getters['data/googleTokensBySub'][state.userId];
switch (token && state.action) { switch (token && state.action) {
case 'create': { case 'create': {
const file = await fileSvc.createFile({}, true); const file = await fileSvc.createFile({}, true);
@ -106,7 +106,7 @@ export default new Provider({
return Provider.parseContent(content, `${syncLocation.fileId}/content`); return Provider.parseContent(content, `${syncLocation.fileId}/content`);
}, },
async uploadContent(token, content, syncLocation, ifNotTooLate) { async uploadContent(token, content, syncLocation, ifNotTooLate) {
const file = store.state.file.itemMap[syncLocation.fileId]; const file = store.state.file.itemsById[syncLocation.fileId];
const name = utils.sanitizeName(file && file.name); const name = utils.sanitizeName(file && file.name);
const parents = []; const parents = [];
if (syncLocation.driveParentId) { if (syncLocation.driveParentId) {

View File

@ -22,7 +22,7 @@ export default new Provider({
&& utils.makeWorkspaceId(makeWorkspaceParams(folderId)); && utils.makeWorkspaceId(makeWorkspaceParams(folderId));
const getWorkspace = folderId => const getWorkspace = folderId =>
store.getters['data/sanitizedWorkspaces'][makeWorkspaceId(folderId)]; store.getters['data/sanitizedWorkspacesById'][makeWorkspaceId(folderId)];
const initFolder = async (token, folder) => { const initFolder = async (token, folder) => {
const appProperties = { const appProperties = {
@ -68,7 +68,7 @@ export default new Provider({
// Update workspace in the store // Update workspace in the store
const workspaceId = makeWorkspaceId(folder.id); const workspaceId = makeWorkspaceId(folder.id);
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspacesById', {
[workspaceId]: { [workspaceId]: {
id: workspaceId, id: workspaceId,
sub: token.sub, sub: token.sub,
@ -86,7 +86,7 @@ export default new Provider({
// Token sub is in the workspace or in the url if workspace is about to be created // Token sub is in the workspace or in the url if workspace is about to be created
const { sub } = getWorkspace(utils.queryParams.folderId) || utils.queryParams; const { sub } = getWorkspace(utils.queryParams.folderId) || utils.queryParams;
// See if we already have a token // See if we already have a token
let token = store.getters['data/googleTokens'][sub]; let token = store.getters['data/googleTokensBySub'][sub];
// If no token has been found, popup an authorize window and get one // If no token has been found, popup an authorize window and get one
if (!token || !token.isDrive || !token.driveFullAccess) { if (!token || !token.isDrive || !token.driveFullAccess) {
await store.dispatch('modal/open', 'workspaceGoogleRedirection'); await store.dispatch('modal/open', 'workspaceGoogleRedirection');
@ -130,14 +130,14 @@ export default new Provider({
// Fix the URL hash // Fix the URL hash
utils.setQueryParams(makeWorkspaceParams(workspace.folderId)); utils.setQueryParams(makeWorkspaceParams(workspace.folderId));
if (workspace.url !== window.location.href) { if (workspace.url !== window.location.href) {
store.dispatch('data/patchWorkspaces', { store.dispatch('data/patchWorkspacesById', {
[workspace.id]: { [workspace.id]: {
...workspace, ...workspace,
url: window.location.href, url: window.location.href,
}, },
}); });
} }
return store.getters['data/sanitizedWorkspaces'][workspace.id]; return store.getters['data/sanitizedWorkspacesById'][workspace.id];
}, },
async performAction() { async performAction() {
const state = googleHelper.driveState || {}; const state = googleHelper.driveState || {};
@ -145,21 +145,21 @@ export default new Provider({
switch (token && state.action) { switch (token && state.action) {
case 'create': { case 'create': {
const driveFolder = googleHelper.driveActionFolder; const driveFolder = googleHelper.driveActionFolder;
let syncData = store.getters['data/syncData'][driveFolder.id]; let syncData = store.getters['data/syncDataById'][driveFolder.id];
if (!syncData && driveFolder.appProperties.id) { if (!syncData && driveFolder.appProperties.id) {
// Create folder if not already synced // Create folder if not already synced
store.commit('folder/setItem', { store.commit('folder/setItem', {
id: driveFolder.appProperties.id, id: driveFolder.appProperties.id,
name: driveFolder.name, name: driveFolder.name,
}); });
const item = store.state.folder.itemMap[driveFolder.appProperties.id]; const item = store.state.folder.itemsById[driveFolder.appProperties.id];
syncData = { syncData = {
id: driveFolder.id, id: driveFolder.id,
itemId: item.id, itemId: item.id,
type: item.type, type: item.type,
hash: item.hash, hash: item.hash,
}; };
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncDataById', {
[syncData.id]: syncData, [syncData.id]: syncData,
}); });
} }
@ -173,7 +173,7 @@ export default new Provider({
case 'open': { case 'open': {
// open first file only // open first file only
const firstFile = googleHelper.driveActionFiles[0]; const firstFile = googleHelper.driveActionFiles[0];
const syncData = store.getters['data/syncData'][firstFile.id]; const syncData = store.getters['data/syncDataById'][firstFile.id];
if (!syncData) { if (!syncData) {
fileIdToOpen = firstFile.id; fileIdToOpen = firstFile.id;
} else { } else {
@ -191,6 +191,10 @@ export default new Provider({
const { changes, startPageToken } = await googleHelper const { changes, startPageToken } = await googleHelper
.getChanges(syncToken, lastStartPageToken, false, workspace.teamDriveId); .getChanges(syncToken, lastStartPageToken, false, workspace.teamDriveId);
syncStartPageToken = startPageToken;
return changes;
},
prepareChanges(changes) {
// Collect possible parent IDs // Collect possible parent IDs
const parentIds = {}; const parentIds = {};
Object.entries(store.getters['data/syncDataByItemId']).forEach(([id, syncData]) => { Object.entries(store.getters['data/syncDataByItemId']).forEach(([id, syncData]) => {
@ -204,6 +208,7 @@ export default new Provider({
}); });
// Collect changes // Collect changes
const workspace = store.getters['workspace/currentWorkspace'];
const result = []; const result = [];
changes.forEach((change) => { changes.forEach((change) => {
// Ignore changes on StackEdit own folders // Ignore changes on StackEdit own folders
@ -259,21 +264,23 @@ export default new Provider({
if (type === 'file') { if (type === 'file') {
// create a fake change as a file content change // create a fake change as a file content change
const id = `${appProperties.id}/content`;
const syncDataId = `${change.fileId}/content`;
contentChange = { contentChange = {
item: { item: {
id: `${appProperties.id}/content`, id,
type: 'content', type: 'content',
// Need a truthy value to force saving sync data // Need a truthy value to force saving sync data
hash: 1, hash: 1,
}, },
syncData: { syncData: {
id: `${change.fileId}/content`, id: syncDataId,
itemId: `${appProperties.id}/content`, itemId: id,
type: 'content', type: 'content',
// Need a truthy value to force downloading the content // Need a truthy value to force downloading the content
hash: 1, hash: 1,
}, },
syncDataId: `${change.fileId}/content`, syncDataId,
}; };
} }
} }
@ -288,7 +295,7 @@ export default new Provider({
}; };
} else { } else {
// Item was removed // Item was removed
const syncData = store.getters['data/syncData'][change.fileId]; const syncData = store.getters['data/syncDataById'][change.fileId];
if (syncData && syncData.type === 'file') { if (syncData && syncData.type === 'file') {
// create a fake change as a file content change // create a fake change as a file content change
contentChange = { contentChange = {
@ -304,7 +311,7 @@ export default new Provider({
result.push(contentChange); result.push(contentChange);
} }
}); });
syncStartPageToken = startPageToken;
return result; return result;
}, },
onChangesApplied() { onChangesApplied() {
@ -312,7 +319,7 @@ export default new Provider({
syncStartPageToken, syncStartPageToken,
}); });
}, },
async saveWorkspaceItem(item, syncData, ifNotTooLate) { async saveWorkspaceItem({ item, syncData, ifNotTooLate }) {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
let file; let file;
@ -363,40 +370,35 @@ export default new Provider({
hash: item.hash, hash: item.hash,
}; };
}, },
async removeWorkspaceItem(syncData, ifNotTooLate) { async removeWorkspaceItem({ syncData, ifNotTooLate }) {
// Ignore content deletion // Ignore content deletion
if (syncData.type !== 'content') { if (syncData.type !== 'content') {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
await googleHelper.removeFile(syncToken, syncData.id, ifNotTooLate); await googleHelper.removeFile(syncToken, syncData.id, ifNotTooLate);
} }
}, },
async downloadWorkspaceContent(token, contentSyncData) { async downloadWorkspaceContent({ token, contentSyncData, fileSyncData }) {
const [fileId] = contentSyncData.itemId.split('/'); const data = await googleHelper.downloadFile(token, fileSyncData.id);
const syncData = store.getters['data/syncDataByItemId'][fileId]; const content = Provider.parseContent(data, contentSyncData.itemId);
if (!syncData) {
return {};
}
const content = await googleHelper.downloadFile(token, syncData.id);
const item = Provider.parseContent(content, contentSyncData.itemId);
// Open the file requested by action if it wasn't synced yet // Open the file requested by action if it wasn't synced yet
if (fileIdToOpen && fileIdToOpen === syncData.id) { if (fileIdToOpen && fileIdToOpen === fileSyncData.id) {
fileIdToOpen = null; fileIdToOpen = null;
// Open the file once downloaded content has been stored // Open the file once downloaded content has been stored
setTimeout(() => { setTimeout(() => {
store.commit('file/setCurrentId', fileId); store.commit('file/setCurrentId', fileSyncData.itemId);
}, 10); }, 10);
} }
return { return {
item, content,
syncData: { contentSyncData: {
...contentSyncData, ...contentSyncData,
hash: item.hash, hash: content.hash,
}, },
}; };
}, },
async downloadWorkspaceData(token, dataId, syncData) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
} }
@ -411,57 +413,66 @@ export default new Provider({
}, },
}; };
}, },
async uploadWorkspaceContent(token, content, contentSyncData, ifNotTooLate) { async uploadWorkspaceContent({
const [fileId] = content.id.split('/'); token,
const syncData = store.getters['data/syncDataByItemId'][fileId]; content,
let file; file,
fileSyncData,
ifNotTooLate,
}) {
let gdriveFile;
let newFileSyncData;
if (syncData) { if (fileSyncData) {
// Only update file media // Only update file media
file = await googleHelper.uploadFile({ gdriveFile = await googleHelper.uploadFile({
token, token,
media: Provider.serializeContent(content), media: Provider.serializeContent(content),
fileId: syncData.id, fileId: fileSyncData.id,
ifNotTooLate, ifNotTooLate,
}); });
} else { } else {
// Create file with media // Create file with media
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
// Use deepCopy to freeze objects const parentSyncData = store.getters['data/syncDataByItemId'][file.parentId];
const item = utils.deepCopy(store.state.file.itemMap[fileId]); gdriveFile = await googleHelper.uploadFile({
const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId];
file = await googleHelper.uploadFile({
token, token,
name: item.name, name: file.name,
parents: [parentSyncData ? parentSyncData.id : workspace.folderId], parents: [parentSyncData ? parentSyncData.id : workspace.folderId],
appProperties: { appProperties: {
id: item.id, id: file.id,
folderId: workspace.folderId, folderId: workspace.folderId,
}, },
media: Provider.serializeContent(content), media: Provider.serializeContent(content),
ifNotTooLate, ifNotTooLate,
}); });
// Create file syncData // Create file sync data
store.dispatch('data/patchSyncData', { newFileSyncData = {
[file.id]: { id: gdriveFile.id,
id: file.id, itemId: file.id,
itemId: item.id, type: file.type,
type: item.type, hash: file.hash,
hash: item.hash, };
},
});
} }
// Return new sync data // Return new sync data
return { return {
id: `${file.id}/content`, contentSyncData: {
id: `${gdriveFile.id}/content`,
itemId: content.id, itemId: content.id,
type: content.type, type: content.type,
hash: content.hash, hash: content.hash,
},
fileSyncData: newFileSyncData,
}; };
}, },
async uploadWorkspaceData(token, item, syncData, ifNotTooLate) { async uploadWorkspaceData({
token,
item,
syncData,
ifNotTooLate,
}) {
const workspace = store.getters['workspace/currentWorkspace']; const workspace = store.getters['workspace/currentWorkspace'];
const file = await googleHelper.uploadFile({ const file = await googleHelper.uploadFile({
token, token,
@ -482,10 +493,12 @@ export default new Provider({
// Return new sync data // Return new sync data
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,
},
}; };
}, },
async listRevisions(token, fileId) { async listRevisions(token, fileId) {

View File

@ -4,7 +4,7 @@ import store from '../../../store';
const request = async (token, options = {}) => { const request = async (token, options = {}) => {
const baseUrl = `${token.dbUrl}/`; const baseUrl = `${token.dbUrl}/`;
const getLastToken = () => store.getters['data/couchdbTokens'][token.sub]; const getLastToken = () => store.getters['data/couchdbTokensBySub'][token.sub];
const assertUnauthorized = (err) => { const assertUnauthorized = (err) => {
if (err.status !== 401) { if (err.status !== 401) {

View File

@ -53,8 +53,8 @@ export default {
fullAccess, fullAccess,
}; };
// Add token to dropboxTokens // Add token to dropbox tokens
store.dispatch('data/setDropboxToken', token); store.dispatch('data/addDropboxToken', token);
return token; return token;
}, },
addAccount(fullAccess = false) { addAccount(fullAccess = false) {

View File

@ -76,8 +76,8 @@ export default {
repoFullAccess: scopes.indexOf('repo') !== -1, repoFullAccess: scopes.indexOf('repo') !== -1,
}; };
// Add token to githubTokens // Add token to github tokens
store.dispatch('data/setGithubToken', token); store.dispatch('data/addGithubToken', token);
return token; return token;
}, },
async addAccount(repoFullAccess = false) { async addAccount(repoFullAccess = false) {
@ -204,7 +204,7 @@ export default {
}); });
return { return {
sha, sha,
content: utils.decodeBase64(content), data: utils.decodeBase64(content),
}; };
}, },

View File

@ -52,7 +52,7 @@ export default {
const { reason } = (((err.body || {}).error || {}).errors || [])[0] || {}; const { reason } = (((err.body || {}).error || {}).errors || [])[0] || {};
if (reason === 'authError') { if (reason === 'authError') {
// Mark the token as revoked and get a new one // Mark the token as revoked and get a new one
store.dispatch('data/setGoogleToken', { store.dispatch('data/addGoogleToken', {
...token, ...token,
expiresOn: 0, expiresOn: 0,
}); });
@ -101,7 +101,7 @@ export default {
} }
// Build token object including scopes and sub // Build token object including scopes and sub
const existingToken = store.getters['data/googleTokens'][body.sub]; const existingToken = store.getters['data/googleTokensBySub'][body.sub];
const token = { const token = {
scopes, scopes,
accessToken, accessToken,
@ -158,13 +158,13 @@ export default {
} }
} }
// Add token to googleTokens // Add token to google tokens
store.dispatch('data/setGoogleToken', token); store.dispatch('data/addGoogleToken', token);
return token; return token;
}, },
async refreshToken(token, scopes = []) { async refreshToken(token, scopes = []) {
const { sub } = token; const { sub } = token;
const lastToken = store.getters['data/googleTokens'][sub]; const lastToken = store.getters['data/googleTokensBySub'][sub];
const mergedScopes = [...new Set([ const mergedScopes = [...new Set([
...scopes, ...scopes,
...lastToken.scopes, ...lastToken.scopes,

View File

@ -44,13 +44,13 @@ export default {
name: body.display_name, name: body.display_name,
sub: `${body.ID}`, sub: `${body.ID}`,
}; };
// Add token to wordpressTokens // Add token to wordpress tokens
store.dispatch('data/setWordpressToken', token); store.dispatch('data/addWordpressToken', token);
return token; return token;
}, },
async refreshToken(token) { async refreshToken(token) {
const { sub } = token; const { sub } = token;
const lastToken = store.getters['data/wordpressTokens'][sub]; const lastToken = store.getters['data/wordpressTokensBySub'][sub];
if (lastToken.expiresOn > Date.now() + tokenExpirationMargin) { if (lastToken.expiresOn > Date.now() + tokenExpirationMargin) {
return lastToken; return lastToken;

View File

@ -45,8 +45,8 @@ export default {
sub: uniqueSub, sub: uniqueSub,
}; };
// Add token to zendeskTokens // Add token to zendesk tokens
store.dispatch('data/setZendeskToken', token); store.dispatch('data/addZendeskToken', token);
return token; return token;
}, },
addAccount(subdomain, clientId) { addAccount(subdomain, clientId) {

View File

@ -5,7 +5,7 @@ import Provider from './common/Provider';
export default new Provider({ export default new Provider({
id: 'wordpress', id: 'wordpress',
getToken(location) { getToken(location) {
return store.getters['data/wordpressTokens'][location.sub]; return store.getters['data/wordpressTokensBySub'][location.sub];
}, },
getUrl(location) { getUrl(location) {
return `https://wordpress.com/post/${location.siteId}/${location.postId}`; return `https://wordpress.com/post/${location.siteId}/${location.postId}`;

View File

@ -5,7 +5,7 @@ import Provider from './common/Provider';
export default new Provider({ export default new Provider({
id: 'zendesk', id: 'zendesk',
getToken(location) { getToken(location) {
return store.getters['data/zendeskTokens'][location.sub]; return store.getters['data/zendeskTokensBySub'][location.sub];
}, },
getUrl(location) { getUrl(location) {
const token = this.getToken(location); const token = this.getToken(location);

View File

@ -40,10 +40,10 @@ const ensureDate = (value, defaultValue) => {
const publish = async (publishLocation) => { const publish = async (publishLocation) => {
const { fileId } = publishLocation; const { fileId } = publishLocation;
const template = store.getters['data/allTemplates'][publishLocation.templateId]; const template = store.getters['data/allTemplatesById'][publishLocation.templateId];
const html = await exportSvc.applyTemplate(fileId, template); const html = await exportSvc.applyTemplate(fileId, template);
const content = await localDbSvc.loadItem(`${fileId}/content`); const content = await localDbSvc.loadItem(`${fileId}/content`);
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemsById[fileId];
const properties = utils.computeProperties(content.properties); const properties = utils.computeProperties(content.properties);
const provider = providerRegistry.providers[publishLocation.providerId]; const provider = providerRegistry.providers[publishLocation.providerId];
const token = provider.getToken(publishLocation); const token = provider.getToken(publishLocation);
@ -90,7 +90,7 @@ const publishFile = async (fileId) => {
}, },
}); });
}); });
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemsById[fileId];
store.dispatch('notification/info', `"${file.name}" was published to ${counter} location(s).`); store.dispatch('notification/info', `"${file.name}" was published to ${counter} location(s).`);
} finally { } finally {
await localDbSvc.unloadContents(); await localDbSvc.unloadContents();

View File

@ -108,7 +108,7 @@ const upgradeSyncedContent = (syncedContent) => {
const cleanSyncedContent = (syncedContent) => { const cleanSyncedContent = (syncedContent) => {
// Clean syncHistory from removed syncLocations // Clean syncHistory from removed syncLocations
Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => { Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => {
if (syncLocationId !== 'main' && !store.state.syncLocation.itemMap[syncLocationId]) { if (syncLocationId !== 'main' && !store.state.syncLocation.itemsById[syncLocationId]) {
delete syncedContent.syncHistory[syncLocationId]; delete syncedContent.syncHistory[syncLocationId];
} }
}); });
@ -129,28 +129,43 @@ const cleanSyncedContent = (syncedContent) => {
* Apply changes retrieved from the main provider. Update sync data accordingly. * Apply changes retrieved from the main provider. Update sync data accordingly.
*/ */
const applyChanges = (changes) => { const applyChanges = (changes) => {
const storeItemMap = { ...store.getters.allItemMap }; const allItemsById = { ...store.getters.allItemsById };
const syncData = { ...store.getters['data/syncData'] }; const syncDataById = { ...store.getters['data/syncDataById'] };
let getExistingItem;
if (workspaceProvider.isGit) {
const { itemsByGitPath } = store.getters;
getExistingItem = (existingSyncData) => {
const items = existingSyncData && itemsByGitPath[existingSyncData.id];
return items ? items[0] : null;
};
} else {
getExistingItem = existingSyncData => existingSyncData && allItemsById[existingSyncData.itemId];
}
const idsToKeep = {};
let saveSyncData = false; let saveSyncData = false;
// Process each change
changes.forEach((change) => { changes.forEach((change) => {
const existingSyncData = syncData[change.syncDataId]; const existingSyncData = syncDataById[change.syncDataId];
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId]; const existingItem = getExistingItem(existingSyncData);
// If item was removed
if (!change.item && existingSyncData) { if (!change.item && existingSyncData) {
// Item was removed if (syncDataById[change.syncDataId]) {
if (syncData[change.syncDataId]) { delete syncDataById[change.syncDataId];
delete syncData[change.syncDataId];
saveSyncData = true; saveSyncData = true;
} }
if (existingItem) { if (existingItem) {
// Remove object from the store // Remove object from the store
store.commit(`${existingItem.type}/deleteItem`, existingItem.id); store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
delete storeItemMap[existingItem.id]; delete allItemsById[existingItem.id];
} }
// If item was modified
} else if (change.item && change.item.hash) { } else if (change.item && change.item.hash) {
// Item was modifed idsToKeep[change.item.id] = true;
if ((existingSyncData || {}).hash !== change.syncData.hash) { if ((existingSyncData || {}).hash !== change.syncData.hash) {
syncData[change.syncDataId] = change.syncData; syncDataById[change.syncDataId] = change.syncData;
saveSyncData = true; saveSyncData = true;
} }
if ( if (
@ -162,13 +177,14 @@ const applyChanges = (changes) => {
&& change.item.type !== 'content' && change.item.type !== 'data' && change.item.type !== 'content' && change.item.type !== 'data'
) { ) {
store.commit(`${change.item.type}/setItem`, change.item); store.commit(`${change.item.type}/setItem`, change.item);
storeItemMap[change.item.id] = change.item; allItemsById[change.item.id] = change.item;
} }
} }
}); });
if (saveSyncData) { if (saveSyncData) {
store.dispatch('data/setSyncData', syncData); store.dispatch('data/setSyncDataById', syncDataById);
fileSvc.ensureUniquePaths(idsToKeep);
} }
}; };
@ -192,7 +208,7 @@ const createSyncLocation = (syncLocation) => {
history: [content.hash], history: [content.hash],
}, syncLocation); }, syncLocation);
await localDbSvc.loadSyncedContent(fileId); await localDbSvc.loadSyncedContent(fileId);
const newSyncedContent = utils.deepCopy(upgradeSyncedContent(store.state.syncedContent.itemMap[`${fileId}/syncedContent`])); const newSyncedContent = utils.deepCopy(upgradeSyncedContent(store.state.syncedContent.itemsById[`${fileId}/syncedContent`]));
const newSyncHistoryItem = []; const newSyncHistoryItem = [];
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem; newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
newSyncHistoryItem[LAST_SEEN] = content.hash; newSyncHistoryItem[LAST_SEEN] = content.hash;
@ -220,7 +236,7 @@ const tooLateChecker = (timeout) => {
}; };
/** /**
* Return true if file is in the temp folder or it's a welcome file. * Return true if file is in the temp folder or is a welcome file.
*/ */
const isTempFile = (fileId) => { const isTempFile = (fileId) => {
const contentId = `${fileId}/content`; const contentId = `${fileId}/content`;
@ -228,8 +244,8 @@ const isTempFile = (fileId) => {
// If file has already been synced, it's not a temp file // If file has already been synced, it's not a temp file
return false; return false;
} }
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemsById[fileId];
const content = store.state.content.itemMap[contentId]; const content = store.state.content.itemsById[contentId];
if (!file || !content) { if (!file || !content) {
return false; return false;
} }
@ -251,6 +267,24 @@ const isTempFile = (fileId) => {
return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions; return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
}; };
/**
* Patch sync data if some have changed in the result.
*/
const updateSyncData = (result) => {
['syncData', 'contentSyncData', 'fileSyncData'].forEach((field) => {
const syncData = result[field];
if (syncData) {
const oldSyncData = store.getters['data/syncDataById'][syncData.id];
if (utils.serializeObject(oldSyncData) !== utils.serializeObject(syncData)) {
store.dispatch('data/patchSyncDataById', {
[syncData.id]: syncData,
});
}
}
});
return result;
};
class SyncContext { class SyncContext {
restart = false; restart = false;
attempted = {}; attempted = {};
@ -270,8 +304,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
// Item may not exist if content has not been downloaded yet // Item may not exist if content has not been downloaded yet
} }
const getContent = () => store.state.content.itemMap[contentId]; const getSyncedContent = () => upgradeSyncedContent(store.state.syncedContent.itemsById[`${fileId}/syncedContent`]);
const getSyncedContent = () => upgradeSyncedContent(store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId]; const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
try { try {
@ -303,57 +336,45 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
} }
// On workspace provider, call downloadWorkspaceContent // On workspace provider, call downloadWorkspaceContent
const oldSyncData = provider.isGit const oldContentSyncData = store.getters['data/syncDataByItemId'][contentId];
? store.getters['data/syncData'][store.getters.itemGitPaths[contentId]] const oldFileSyncData = store.getters['data/syncDataByItemId'][fileId];
: store.getters['data/syncDataByItemId'][contentId]; if (!oldContentSyncData || !oldFileSyncData) {
if (!oldSyncData) {
return null;
}
const { item, syncData } = await provider.downloadWorkspaceContent(token, oldSyncData);
if (!item) {
return null; return null;
} }
// Update sync data if changed const { content } = updateSyncData(await provider.downloadWorkspaceContent({
if (syncData token,
&& utils.serializeObject(oldSyncData) !== utils.serializeObject(syncData) contentId,
) { contentSyncData: oldContentSyncData,
store.dispatch('data/patchSyncData', { fileSyncData: oldFileSyncData,
[syncData.id]: syncData, }));
});
} // Return the downloaded content
return item; return content;
}; };
const uploadContent = async (item, ifNotTooLate) => { const uploadContent = async (content, ifNotTooLate) => {
// On simple provider, call simply uploadContent // On simple provider, call simply uploadContent
if (syncLocation.id !== 'main') { if (syncLocation.id !== 'main') {
return provider.uploadContent(token, item, syncLocation, ifNotTooLate); return provider.uploadContent(token, content, syncLocation, ifNotTooLate);
} }
// On workspace provider, call uploadWorkspaceContent // On workspace provider, call uploadWorkspaceContent
const oldSyncData = provider.isGit const oldContentSyncData = store.getters['data/syncDataByItemId'][contentId];
? store.getters['data/syncData'][store.getters.itemGitPaths[contentId]] if (oldContentSyncData && oldContentSyncData.hash === content.hash) {
: store.getters['data/syncDataByItemId'][contentId];
if (oldSyncData && oldSyncData.hash === item.hash) {
return syncLocation; return syncLocation;
} }
const oldFileSyncData = store.getters['data/syncDataByItemId'][fileId];
const syncData = await provider.uploadWorkspaceContent( updateSyncData(await provider.uploadWorkspaceContent({
token, token,
item, content,
oldSyncData, // Use deepCopy to freeze item
file: utils.deepCopy(store.state.file.itemsById[fileId]),
contentSyncData: oldContentSyncData,
fileSyncData: oldFileSyncData,
ifNotTooLate, ifNotTooLate,
); }));
// Update sync data if changed
if (syncData
&& utils.serializeObject(oldSyncData) !== utils.serializeObject(syncData)
) {
store.dispatch('data/patchSyncData', {
[syncData.id]: syncData,
});
}
// Return syncLocation // Return syncLocation
return syncLocation; return syncLocation;
@ -366,7 +387,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
// Merge content // Merge content
let mergedContent; let mergedContent;
const clientContent = utils.deepCopy(getContent()); const clientContent = utils.deepCopy(store.state.content.itemsById[contentId]);
if (!clientContent) { if (!clientContent) {
mergedContent = utils.deepCopy(serverContent || null); mergedContent = utils.deepCopy(serverContent || null);
} else if (!serverContent // If sync location has not been created yet } else if (!serverContent // If sync location has not been created yet
@ -401,7 +422,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
}); });
// Retrieve content with its new hash value and freeze it // Retrieve content with its new hash value and freeze it
mergedContent = utils.deepCopy(getContent()); mergedContent = utils.deepCopy(store.state.content.itemsById[contentId]);
// Make merged content history // Make merged content history
const mergedContentHistory = serverContent ? serverContent.history.slice() : []; const mergedContentHistory = serverContent ? serverContent.history.slice() : [];
@ -504,8 +525,8 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
* Sync a data item, typically settings, workspaces and templates. * Sync a data item, typically settings, workspaces and templates.
*/ */
const syncDataItem = async (dataId) => { const syncDataItem = async (dataId) => {
const getItem = () => store.state.data.itemMap[dataId] const getItem = () => store.state.data.itemsById[dataId]
|| store.state.data.lsItemMap[dataId]; || store.state.data.lsItemsById[dataId];
const oldItem = getItem(); const oldItem = getItem();
const oldSyncData = store.getters['data/syncDataByItemId'][dataId]; const oldSyncData = store.getters['data/syncDataByItemId'][dataId];
@ -515,23 +536,13 @@ const syncDataItem = async (dataId) => {
} }
const token = workspaceProvider.getToken(); const token = workspaceProvider.getToken();
const { item, syncData } = await workspaceProvider.downloadWorkspaceData( const { item } = updateSyncData(await workspaceProvider.downloadWorkspaceData({
token, token,
dataId, syncData: oldSyncData,
oldSyncData, }));
);
// Update sync data if changed
if (syncData
&& utils.serializeObject(oldSyncData) !== utils.serializeObject(syncData)
) {
store.dispatch('data/patchSyncData', {
[syncData.id]: syncData,
});
}
const serverItem = item; const serverItem = item;
const dataSyncData = store.getters['data/dataSyncData'][dataId]; const dataSyncData = store.getters['data/dataSyncDataById'][dataId];
let mergedItem = (() => { let mergedItem = (() => {
const clientItem = utils.deepCopy(getItem()); const clientItem = utils.deepCopy(getItem());
if (!clientItem) { if (!clientItem) {
@ -575,24 +586,15 @@ const syncDataItem = async (dataId) => {
} }
// Upload merged data item // Upload merged data item
const newSyncData = await workspaceProvider.uploadWorkspaceData( updateSyncData(await workspaceProvider.uploadWorkspaceData({
token, token,
mergedItem, item: mergedItem,
syncData, syncData: store.getters['data/syncDataByItemId'][dataId],
tooLateChecker(restartContentSyncAfter), ifNotTooLate: tooLateChecker(restartContentSyncAfter),
); }));
// Update sync data if changed
if (newSyncData
&& utils.serializeObject(syncData) !== utils.serializeObject(newSyncData)
) {
store.dispatch('data/patchSyncData', {
[newSyncData.id]: newSyncData,
});
}
// Update data sync data // Update data sync data
store.dispatch('data/patchDataSyncData', { store.dispatch('data/patchDataSyncDataById', {
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]), [dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
}); });
}; };
@ -617,11 +619,10 @@ const syncWorkspace = async () => {
} }
const changes = await workspaceProvider.getChanges(); const changes = await workspaceProvider.getChanges();
// Apply changes // Apply changes
applyChanges(changes); applyChanges(workspaceProvider.prepareChanges(changes));
if (workspaceProvider.onChangesApplied) {
workspaceProvider.onChangesApplied(); workspaceProvider.onChangesApplied();
}
// Prevent from sending items too long after changes have been retrieved // Prevent from sending items too long after changes have been retrieved
const ifNotTooLate = tooLateChecker(restartSyncAfter); const ifNotTooLate = tooLateChecker(restartSyncAfter);
@ -629,33 +630,25 @@ const syncWorkspace = async () => {
// Called until no item to save // Called until no item to save
const saveNextItem = () => ifNotTooLate(async () => { const saveNextItem = () => ifNotTooLate(async () => {
const storeItemMap = { const storeItemMap = {
...store.state.file.itemMap, ...store.state.file.itemsById,
...store.state.folder.itemMap, ...store.state.folder.itemsById,
...store.state.syncLocation.itemMap, ...store.state.syncLocation.itemsById,
...store.state.publishLocation.itemMap, ...store.state.publishLocation.itemsById,
// Deal with contents and data later // Deal with contents and data later
}; };
let getSyncData;
if (workspaceProvider.isGit) {
const syncData = store.getters['data/syncData'];
getSyncData = id => syncData[store.getters.itemGitPaths[id]];
} else {
const syncDataByItemId = store.getters['data/syncDataByItemId']; const syncDataByItemId = store.getters['data/syncDataByItemId'];
getSyncData = id => syncDataByItemId[id];
}
const [changedItem, syncDataToUpdate] = utils.someResult( const [changedItem, syncDataToUpdate] = utils.someResult(
Object.entries(storeItemMap), Object.entries(storeItemMap),
([id, item]) => { ([id, item]) => {
const existingSyncData = getSyncData(id); const syncData = syncDataByItemId[id];
if ((!existingSyncData || existingSyncData.hash !== item.hash) if ((!syncData || syncData.hash !== item.hash)
// Add file/folder if parent has been added // Add file/folder if parent has been added
&& (!storeItemMap[item.parentId] || getSyncData(item.parentId)) && (!storeItemMap[item.parentId] || syncDataByItemId[item.parentId])
// Add file if content has been added // Add file if content has been added
&& (item.type !== 'file' || getSyncData(`${id}/content`)) && (item.type !== 'file' || syncDataByItemId[`${id}/content`])
) { ) {
return [item, existingSyncData]; return [item, syncData];
} }
return null; return null;
}, },
@ -663,13 +656,13 @@ const syncWorkspace = async () => {
if (changedItem) { if (changedItem) {
const resultSyncData = await workspaceProvider const resultSyncData = await workspaceProvider
.saveWorkspaceItem( .saveWorkspaceItem({
// Use deepCopy to freeze objects // Use deepCopy to freeze objects
utils.deepCopy(changedItem), item: utils.deepCopy(changedItem),
utils.deepCopy(syncDataToUpdate), syncData: utils.deepCopy(syncDataToUpdate),
ifNotTooLate, ifNotTooLate,
); });
store.dispatch('data/patchSyncData', { store.dispatch('data/patchSyncDataById', {
[resultSyncData.id]: resultSyncData, [resultSyncData.id]: resultSyncData,
}); });
await saveNextItem(); await saveNextItem();
@ -682,38 +675,40 @@ const syncWorkspace = async () => {
let getItem; let getItem;
let getFileItem; let getFileItem;
if (workspaceProvider.isGit) { if (workspaceProvider.isGit) {
const { gitPathItems } = store.getters; const { itemsByGitPath } = store.getters;
getItem = syncData => gitPathItems[syncData.id]; getItem = syncData => itemsByGitPath[syncData.id];
getFileItem = syncData => gitPathItems[syncData.id.slice(1)]; // Remove leading / getFileItem = syncData => itemsByGitPath[syncData.id.slice(1)]; // Remove leading /
} else { } else {
const { allItemMap } = store.getters; const { allItemsById } = store.getters;
getItem = syncData => allItemMap[syncData.itemId]; getItem = syncData => allItemsById[syncData.itemId];
getFileItem = syncData => allItemMap[syncData.itemId.split('/')[0]]; getFileItem = syncData => allItemsById[syncData.itemId.split('/')[0]];
} }
const syncData = store.getters['data/syncData']; const syncDataById = store.getters['data/syncDataById'];
const syncDataToRemove = utils.deepCopy(utils.someResult( const syncDataToRemove = utils.deepCopy(utils.someResult(
Object.values(syncData), Object.values(syncDataById),
(existingSyncData) => { (syncData) => {
if (!getItem(existingSyncData) if (!getItem(syncData)
// We don't want to delete data items, especially on first sync // We don't want to delete data items, especially on first sync
&& existingSyncData.type !== 'data' && syncData.type !== 'data'
// Remove content only if file has been removed // Remove content only if file has been removed
&& (existingSyncData.type !== 'content' && (syncData.type !== 'content'
|| !getFileItem(existingSyncData)) || !getFileItem(syncData))
) { ) {
return existingSyncData; return syncData;
} }
return null; return null;
}, },
)); ));
if (syncDataToRemove) { if (syncDataToRemove) {
// Use deepCopy to freeze objects await workspaceProvider.removeWorkspaceItem({
await workspaceProvider.removeWorkspaceItem(syncDataToRemove, ifNotTooLate); syncData: syncDataToRemove,
const syncDataCopy = { ...store.getters['data/syncData'] }; ifNotTooLate,
});
const syncDataCopy = { ...store.getters['data/syncDataById'] };
delete syncDataCopy[syncDataToRemove.id]; delete syncDataCopy[syncDataToRemove.id];
store.dispatch('data/setSyncData', syncDataCopy); store.dispatch('data/setSyncDataById', syncDataCopy);
await removeNextItem(); await removeNextItem();
} }
}); });
@ -727,33 +722,38 @@ const syncWorkspace = async () => {
await syncDataItem('templates'); await syncDataItem('templates');
const getOneFileIdToSync = () => { const getOneFileIdToSync = () => {
const contentIds = [...new Set([
...Object.keys(localDbSvc.hashMap.content),
...store.getters['file/items'].map(file => `${file.id}/content`),
])];
const contentMap = store.state.content.itemMap;
const syncDataById = store.getters['data/syncData'];
let getSyncData; let getSyncData;
if (workspaceProvider.isGit) { if (workspaceProvider.isGit) {
const { itemGitPaths } = store.getters; const { gitPathsByItemId } = store.getters;
getSyncData = contentId => syncDataById[itemGitPaths[contentId]]; const syncDataById = store.getters['data/syncDataById'];
// Use file git path as content may not exist or not be loaded
getSyncData = fileId => syncDataById[`/${gitPathsByItemId[fileId]}`];
} else { } else {
const syncDataByItemId = store.getters['data/syncDataByItemId']; const syncDataByItemId = store.getters['data/syncDataByItemId'];
getSyncData = contentId => syncDataByItemId[contentId]; getSyncData = (fileId, contentId) => syncDataByItemId[contentId];
} }
return utils.someResult(contentIds, (contentId) => { // Collect all [fileId, contentId]
// Get content hash from itemMap or from localDbSvc if not loaded const ids = [
...Object.keys(localDbSvc.hashMap.content)
.map(contentId => [contentId.split('/')[0], contentId]),
...store.getters['file/items']
.map(file => [file.id, `${file.id}/content`]),
];
// Find the first content out of sync
const contentMap = store.state.content.itemsById;
return utils.someResult(ids, ([fileId, contentId]) => {
// Get content hash from itemsById or from localDbSvc if not loaded
const loadedContent = contentMap[contentId]; const loadedContent = contentMap[contentId];
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId]; const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
const syncData = getSyncData(contentId); const syncData = getSyncData(fileId, contentId);
if ( if (
// Sync if content syncing was not attempted yet // Sync if content syncing was not attempted yet
!syncContext.attempted[contentId] && !syncContext.attempted[contentId] &&
// And if syncData does not exist or if content hash and syncData hash are inconsistent // And if syncData does not exist or if content hash and syncData hash are inconsistent
(!syncData || syncData.hash !== hash) (!syncData || syncData.hash !== hash)
) { ) {
const [fileId] = contentId.split('/');
return fileId; return fileId;
} }
return null; return null;
@ -843,7 +843,7 @@ const requestSync = () => {
// Clean files // Clean files
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => { Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
const file = store.state.file.itemMap[fileId]; const file = store.state.file.itemsById[fileId];
if (file && file.hash === fileHash) { if (file && file.hash === fileHash) {
fileSvc.deleteFile(fileId); fileSvc.deleteFile(fileId);
} }

View File

@ -38,12 +38,11 @@ export default {
parentId: 'temp', parentId: 'temp',
}, true); }, true);
const fileItemMap = store.state.file.itemMap;
// Sanitize file creations // Sanitize file creations
const lastCreated = {}; const lastCreated = {};
const fileItemsById = store.state.file.itemsById;
Object.entries(store.getters['data/lastCreated']).forEach(([id, createdOn]) => { Object.entries(store.getters['data/lastCreated']).forEach(([id, createdOn]) => {
if (fileItemMap[id] && fileItemMap[id].parentId === 'temp') { if (fileItemsById[id] && fileItemsById[id].parentId === 'temp') {
lastCreated[id] = createdOn; lastCreated[id] = createdOn;
} }
}); });

View File

@ -21,7 +21,7 @@ export default {
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
const token = store.getters[`data/${type}Tokens`][sub]; const token = store.getters[`data/${type}TokensBySub`][sub];
if (token) { if (token) {
store.commit('userInfo/addItem', { store.commit('userInfo/addItem', {
id: userId, id: userId,

View File

@ -31,11 +31,11 @@ module.mutations = {
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: ({ itemMap, revisionContent }, getters, rootState, rootGetters) => { current: ({ itemsById, revisionContent }, getters, rootState, rootGetters) => {
if (revisionContent) { if (revisionContent) {
return revisionContent; return revisionContent;
} }
return itemMap[`${rootGetters['file/current'].id}/content`] || empty(); return itemsById[`${rootGetters['file/current'].id}/content`] || empty();
}, },
currentChangeTrigger: (state, getters) => { currentChangeTrigger: (state, getters) => {
const { current } = getters; const { current } = getters;
@ -63,7 +63,7 @@ module.actions = {
}, },
setRevisionContent({ state, rootGetters, commit }, value) { setRevisionContent({ state, rootGetters, commit }, value) {
const currentFile = rootGetters['file/current']; const currentFile = rootGetters['file/current'];
const currentContent = state.itemMap[`${currentFile.id}/content`]; const currentContent = state.itemsById[`${currentFile.id}/content`];
if (currentContent) { if (currentContent) {
const diffs = diffMatchPatch.diff_main(currentContent.text, value.text); const diffs = diffMatchPatch.diff_main(currentContent.text, value.text);
diffMatchPatch.diff_cleanupSemantic(diffs); diffMatchPatch.diff_cleanupSemantic(diffs);

View File

@ -5,8 +5,8 @@ const module = moduleTemplate(empty, true);
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: ({ itemMap }, getters, rootState, rootGetters) => current: ({ itemsById }, getters, rootState, rootGetters) =>
itemMap[`${rootGetters['file/current'].id}/contentState`] || empty(), itemsById[`${rootGetters['file/current'].id}/contentState`] || empty(),
}; };
module.actions = { module.actions = {

View File

@ -37,13 +37,13 @@ const lsItemIdSet = new Set(utils.localStorageDataIds);
// Getter/setter/patcher factories // Getter/setter/patcher factories
const getter = id => state => ((lsItemIdSet.has(id) const getter = id => state => ((lsItemIdSet.has(id)
? state.lsItemMap ? state.lsItemsById
: state.itemMap)[id] || {}).data || empty(id).data; : state.itemsById)[id] || {}).data || empty(id).data;
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data)); const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
const patcher = id => ({ state, commit }, data) => { const patcher = id => ({ state, commit }, data) => {
const item = Object.assign(empty(id), (lsItemIdSet.has(id) const item = Object.assign(empty(id), (lsItemIdSet.has(id)
? state.lsItemMap ? state.lsItemsById
: state.itemMap)[id]); : state.itemsById)[id]);
commit('setItem', { commit('setItem', {
...empty(id), ...empty(id),
data: typeof data === 'object' ? { data: typeof data === 'object' ? {
@ -83,10 +83,10 @@ const additionalTemplates = {
}; };
// For tokens // For tokens
const tokenSetter = providerId => ({ getters, dispatch }, token) => { const tokenAdder = providerId => ({ getters, dispatch }, token) => {
dispatch('patchTokens', { dispatch('patchTokensByProviderId', {
[providerId]: { [providerId]: {
...getters[`${providerId}Tokens`], ...getters[`${providerId}TokensBySub`],
[token.sub]: token, [token.sub]: token,
}, },
}); });
@ -99,12 +99,12 @@ export default {
namespaced: true, namespaced: true,
state: { state: {
// Data items stored in the DB // Data items stored in the DB
itemMap: {}, itemsById: {},
// Data items stored in the localStorage // Data items stored in the localStorage
lsItemMap: {}, lsItemsById: {},
}, },
mutations: { mutations: {
setItem: ({ itemMap, lsItemMap }, value) => { setItem: ({ itemsById, lsItemsById }, value) => {
// Create an empty item and override its data field // Create an empty item and override its data field
const emptyItem = empty(value.id); const emptyItem = empty(value.id);
const data = typeof value.data === 'object' const data = typeof value.data === 'object'
@ -117,20 +117,20 @@ export default {
data, data,
}); });
// Store item in itemMap or lsItemMap if its stored in the localStorage // Store item in itemsById or lsItemsById if its stored in the localStorage
Vue.set(lsItemIdSet.has(item.id) ? lsItemMap : itemMap, item.id, item); Vue.set(lsItemIdSet.has(item.id) ? lsItemsById : itemsById, item.id, item);
}, },
deleteItem({ itemMap }, id) { deleteItem({ itemsById }, id) {
// Only used by localDbSvc to clean itemMap from object moved to localStorage // Only used by localDbSvc to clean itemsById from object moved to localStorage
Vue.delete(itemMap, id); Vue.delete(itemsById, id);
}, },
}, },
getters: { getters: {
workspaces: getter('workspaces'), workspacesById: getter('workspaces'),
sanitizedWorkspaces: (state, { workspaces }, rootState, rootGetters) => { sanitizedWorkspacesById: (state, { workspacesById }, rootState, rootGetters) => {
const sanitizedWorkspaces = {}; const sanitizedWorkspacesById = {};
const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken']; const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken'];
Object.entries(workspaces).forEach(([id, workspace]) => { Object.entries(workspacesById).forEach(([id, workspace]) => {
const sanitizedWorkspace = { const sanitizedWorkspace = {
id, id,
providerId: mainWorkspaceToken && 'googleDriveAppData', providerId: mainWorkspaceToken && 'googleDriveAppData',
@ -141,9 +141,9 @@ export default {
urlParser.href = workspace.url || 'app'; urlParser.href = workspace.url || 'app';
const params = utils.parseQueryParams(urlParser.hash.slice(1)); const params = utils.parseQueryParams(urlParser.hash.slice(1));
sanitizedWorkspace.url = utils.addQueryParams('app', params, true); sanitizedWorkspace.url = utils.addQueryParams('app', params, true);
sanitizedWorkspaces[id] = sanitizedWorkspace; sanitizedWorkspacesById[id] = sanitizedWorkspace;
}); });
return sanitizedWorkspaces; return sanitizedWorkspacesById;
}, },
settings: getter('settings'), settings: getter('settings'),
computedSettings: (state, { settings }) => { computedSettings: (state, { settings }) => {
@ -170,9 +170,9 @@ export default {
}, },
localSettings: getter('localSettings'), localSettings: getter('localSettings'),
layoutSettings: getter('layoutSettings'), layoutSettings: getter('layoutSettings'),
templates: getter('templates'), templatesById: getter('templates'),
allTemplates: (state, { templates }) => ({ allTemplatesById: (state, { templatesById }) => ({
...templates, ...templatesById,
...additionalTemplates, ...additionalTemplates,
}), }),
lastCreated: getter('lastCreated'), lastCreated: getter('lastCreated'),
@ -186,42 +186,51 @@ export default {
result[currentFileId] = Date.now(); result[currentFileId] = Date.now();
} }
return Object.keys(result) return Object.keys(result)
.filter(id => rootState.file.itemMap[id]) .filter(id => rootState.file.itemsById[id])
.sort((id1, id2) => result[id2] - result[id1]) .sort((id1, id2) => result[id2] - result[id1])
.slice(0, 20); .slice(0, 20);
}, },
syncData: getter('syncData'), syncDataById: getter('syncData'),
syncDataByItemId: (state, { syncData }) => { syncDataByItemId: (state, { syncDataById }, rootState, rootGetters) => {
const result = {}; const result = {};
Object.entries(syncData).forEach(([, value]) => { if (rootGetters['workspace/currentWorkspaceIsGit']) {
result[value.itemId] = value; Object.entries(rootGetters.gitPathsByItemId).forEach(([id, path]) => {
const syncDataEntry = syncDataById[path];
if (syncDataEntry) {
result[id] = syncDataEntry;
}
}); });
} else {
Object.entries(syncDataById).forEach(([, syncDataEntry]) => {
result[syncDataEntry.itemId] = syncDataEntry;
});
}
return result; return result;
}, },
syncDataByType: (state, { syncData }) => { syncDataByType: (state, { syncDataById }) => {
const result = {}; const result = {};
utils.types.forEach((type) => { utils.types.forEach((type) => {
result[type] = {}; result[type] = {};
}); });
Object.entries(syncData).forEach(([, item]) => { Object.entries(syncDataById).forEach(([, item]) => {
if (result[item.type]) { if (result[item.type]) {
result[item.type][item.itemId] = item; result[item.type][item.itemId] = item;
} }
}); });
return result; return result;
}, },
dataSyncData: getter('dataSyncData'), dataSyncDataById: getter('dataSyncData'),
tokens: getter('tokens'), tokensByProviderId: getter('tokens'),
googleTokens: (state, { tokens }) => tokens.google || {}, googleTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.google || {},
couchdbTokens: (state, { tokens }) => tokens.couchdb || {}, couchdbTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.couchdb || {},
dropboxTokens: (state, { tokens }) => tokens.dropbox || {}, dropboxTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.dropbox || {},
githubTokens: (state, { tokens }) => tokens.github || {}, githubTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.github || {},
wordpressTokens: (state, { tokens }) => tokens.wordpress || {}, wordpressTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.wordpress || {},
zendeskTokens: (state, { tokens }) => tokens.zendesk || {}, zendeskTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.zendesk || {},
}, },
actions: { actions: {
setWorkspaces: setter('workspaces'), setWorkspacesById: setter('workspaces'),
patchWorkspaces: patcher('workspaces'), patchWorkspacesById: patcher('workspaces'),
setSettings: setter('settings'), setSettings: setter('settings'),
patchLocalSettings: patcher('localSettings'), patchLocalSettings: patcher('localSettings'),
patchLayoutSettings: patcher('layoutSettings'), patchLayoutSettings: patcher('layoutSettings'),
@ -257,15 +266,15 @@ export default {
setSideBarPanel: ({ dispatch }, value) => dispatch('patchLayoutSettings', { setSideBarPanel: ({ dispatch }, value) => dispatch('patchLayoutSettings', {
sideBarPanel: value === undefined ? 'menu' : value, sideBarPanel: value === undefined ? 'menu' : value,
}), }),
setTemplates: ({ commit }, data) => { setTemplatesById: ({ commit }, templatesById) => {
const dataToCommit = { const templatesToCommit = {
...data, ...templatesById,
}; };
// We don't store additional templates // We don't store additional templates
Object.keys(additionalTemplates).forEach((id) => { Object.keys(additionalTemplates).forEach((id) => {
delete dataToCommit[id]; delete templatesToCommit[id];
}); });
commit('setItem', itemTemplate('templates', dataToCommit)); commit('setItem', itemTemplate('templates', templatesToCommit));
}, },
setLastCreated: setter('lastCreated'), setLastCreated: setter('lastCreated'),
setLastOpenedId: ({ getters, commit, rootState }, fileId) => { setLastOpenedId: ({ getters, commit, rootState }, fileId) => {
@ -274,21 +283,21 @@ export default {
// Remove entries that don't exist anymore // Remove entries that don't exist anymore
const cleanedLastOpened = {}; const cleanedLastOpened = {};
Object.entries(lastOpened).forEach(([id, value]) => { Object.entries(lastOpened).forEach(([id, value]) => {
if (rootState.file.itemMap[id]) { if (rootState.file.itemsById[id]) {
cleanedLastOpened[id] = value; cleanedLastOpened[id] = value;
} }
}); });
commit('setItem', itemTemplate('lastOpened', cleanedLastOpened)); commit('setItem', itemTemplate('lastOpened', cleanedLastOpened));
}, },
setSyncData: setter('syncData'), setSyncDataById: setter('syncData'),
patchSyncData: patcher('syncData'), patchSyncDataById: patcher('syncData'),
patchDataSyncData: patcher('dataSyncData'), patchDataSyncDataById: patcher('dataSyncData'),
patchTokens: patcher('tokens'), patchTokensByProviderId: patcher('tokens'),
setGoogleToken: tokenSetter('google'), addGoogleToken: tokenAdder('google'),
setCouchdbToken: tokenSetter('couchdb'), addCouchdbToken: tokenAdder('couchdb'),
setDropboxToken: tokenSetter('dropbox'), addDropboxToken: tokenAdder('dropbox'),
setGithubToken: tokenSetter('github'), addGithubToken: tokenAdder('github'),
setWordpressToken: tokenSetter('wordpress'), addWordpressToken: tokenAdder('wordpress'),
setZendeskToken: tokenSetter('zendesk'), addZendeskToken: tokenAdder('zendesk'),
}, },
}; };

View File

@ -10,10 +10,10 @@ module.state = {
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: ({ itemMap, currentId }) => itemMap[currentId] || empty(), current: ({ itemsById, currentId }) => itemsById[currentId] || empty(),
isCurrentTemp: (state, { current }) => current.parentId === 'temp', isCurrentTemp: (state, { current }) => current.parentId === 'temp',
lastOpened: ({ itemMap }, { items }, rootState, rootGetters) => lastOpened: ({ itemsById }, { items }, rootState, rootGetters) =>
itemMap[rootGetters['data/lastOpenedIds'][0]] || items[0] || empty(), itemsById[rootGetters['data/lastOpenedIds'][0]] || items[0] || empty(),
}; };
module.mutations = { module.mutations = {

View File

@ -75,14 +75,14 @@ const store = new Vuex.Store({
}, },
}, },
getters: { getters: {
allItemMap: (state) => { allItemsById: (state) => {
const result = {}; const result = {};
utils.types.forEach(type => Object.assign(result, state[type].itemMap)); utils.types.forEach(type => Object.assign(result, state[type].itemsById));
return result; return result;
}, },
itemPaths: (state, getters) => { pathsByItemId: (state, getters) => {
const result = {}; const result = {};
const folderMap = state.folder.itemMap; const foldersById = state.folder.itemsById;
const getPath = (item) => { const getPath = (item) => {
let itemPath = result[item.id]; let itemPath = result[item.id];
if (!itemPath) { if (!itemPath) {
@ -90,10 +90,10 @@ const store = new Vuex.Store({
itemPath = `.stackedit-trash/${item.name}`; itemPath = `.stackedit-trash/${item.name}`;
} else { } else {
let { name } = item; let { name } = item;
if (folderMap[item.id]) { if (foldersById[item.id]) {
name += '/'; name += '/';
} }
const parentFolder = folderMap[item.parentId]; const parentFolder = foldersById[item.parentId];
if (parentFolder) { if (parentFolder) {
itemPath = getPath(parentFolder) + name; itemPath = getPath(parentFolder) + name;
} else { } else {
@ -104,33 +104,38 @@ const store = new Vuex.Store({
result[item.id] = itemPath; result[item.id] = itemPath;
return itemPath; return itemPath;
}; };
[ [
...getters['folder/items'], ...getters['folder/items'],
...getters['file/items'], ...getters['file/items'],
].forEach(item => getPath(item)); ].forEach(item => getPath(item));
return result; return result;
}, },
pathItems: (state, { allItemMap, itemPaths }) => { itemsByPath: (state, { allItemsById, pathsByItemId }) => {
const result = {}; const result = {};
Object.entries(itemPaths).forEach(([id, path]) => { Object.entries(pathsByItemId).forEach(([id, path]) => {
const items = result[path] || []; const items = result[path] || [];
items.push(allItemMap[id]); items.push(allItemsById[id]);
result[path] = items; result[path] = items;
}); });
return result; return result;
}, },
itemGitPaths: (state, { allItemMap, itemPaths }) => { gitPathsByItemId: (state, { allItemsById, pathsByItemId }) => {
const result = {}; const result = {};
Object.entries(allItemMap).forEach(([id, item]) => { Object.entries(allItemsById).forEach(([id, item]) => {
if (item.type === 'data') { if (item.type === 'data') {
result[id] = `.stackedit-data/${id}.json`; result[id] = `.stackedit-data/${id}.json`;
} else if (item.type === 'file') { } else if (item.type === 'file') {
result[id] = `${itemPaths[id]}.md`; const filePath = pathsByItemId[id];
result[id] = `${filePath}.md`;
result[`${id}/content`] = `/${filePath}.md`;
} else if (item.type === 'content') { } else if (item.type === 'content') {
const [fileId] = id.split('/'); const [fileId] = id.split('/');
result[id] = `/${itemPaths[fileId]}.md`; const filePath = pathsByItemId[fileId];
result[fileId] = `${filePath}.md`;
result[id] = `/${filePath}.md`;
} else if (item.type === 'folder') { } else if (item.type === 'folder') {
result[id] = itemPaths[id]; result[id] = pathsByItemId[id];
} else if (item.type === 'syncLocation' || item.type === 'publishLocation') { } else if (item.type === 'syncLocation' || item.type === 'publishLocation') {
// locations are stored as paths // locations are stored as paths
const encodedItem = utils.encodeBase64(utils.serializeObject({ const encodedItem = utils.encodeBase64(utils.serializeObject({
@ -140,17 +145,20 @@ const store = new Vuex.Store({
fileId: undefined, fileId: undefined,
}), true); }), true);
const extension = item.type === 'syncLocation' ? 'sync' : 'publish'; const extension = item.type === 'syncLocation' ? 'sync' : 'publish';
result[id] = `${itemPaths[item.fileId]}.${encodedItem}.${extension}`; result[id] = `${pathsByItemId[item.fileId]}.${encodedItem}.${extension}`;
} }
}); });
return result; return result;
}, },
gitPathItems: (state, { allItemMap, itemGitPaths }) => { itemsByGitPath: (state, { allItemsById, gitPathsByItemId }) => {
const result = {}; const result = {};
Object.entries(itemGitPaths).forEach(([id, path]) => { Object.entries(gitPathsByItemId).forEach(([id, path]) => {
const item = allItemsById[id];
if (item) {
const items = result[path] || []; const items = result[path] || [];
items.push(allItemMap[id]); items.push(item);
result[path] = items; result[path] = items;
}
}); });
return result; return result;
}, },

View File

@ -8,10 +8,10 @@ export default (empty, simpleHash = false) => {
return { return {
namespaced: true, namespaced: true,
state: { state: {
itemMap: {}, itemsById: {},
}, },
getters: { getters: {
items: ({ itemMap }) => Object.values(itemMap), items: ({ itemsById }) => Object.values(itemsById),
}, },
mutations: { mutations: {
setItem(state, value) { setItem(state, value) {
@ -19,20 +19,20 @@ export default (empty, simpleHash = false) => {
if (!item.hash || !simpleHash) { if (!item.hash || !simpleHash) {
item.hash = hashFunc(item); item.hash = hashFunc(item);
} }
Vue.set(state.itemMap, item.id, item); Vue.set(state.itemsById, item.id, item);
}, },
patchItem(state, patch) { patchItem(state, patch) {
const item = state.itemMap[patch.id]; const item = state.itemsById[patch.id];
if (item) { if (item) {
Object.assign(item, patch); Object.assign(item, patch);
item.hash = hashFunc(item); item.hash = hashFunc(item);
Vue.set(state.itemMap, item.id, item); Vue.set(state.itemsById, item.id, item);
return true; return true;
} }
return false; return false;
}, },
deleteItem(state, id) { deleteItem(state, id) {
Vue.delete(state.itemMap, id); Vue.delete(state.itemsById, id);
}, },
}, },
actions: {}, actions: {},

View File

@ -5,8 +5,8 @@ const module = moduleTemplate(empty, true);
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: ({ itemMap }, getters, rootState, rootGetters) => current: ({ itemsById }, getters, rootState, rootGetters) =>
itemMap[`${rootGetters['file/current'].id}/syncedContent`] || empty(), itemsById[`${rootGetters['file/current'].id}/syncedContent`] || empty(),
}; };
export default module; export default module;

View File

@ -3,11 +3,11 @@ import Vue from 'vue';
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
itemMap: {}, itemsById: {},
}, },
mutations: { mutations: {
addItem: ({ itemMap }, item) => { addItem: ({ itemsById }, item) => {
Vue.set(itemMap, item.id, item); Vue.set(itemsById, item.id, item);
}, },
}, },
}; };

View File

@ -16,13 +16,14 @@ export default {
}, },
getters: { getters: {
mainWorkspace: (state, getters, rootState, rootGetters) => { mainWorkspace: (state, getters, rootState, rootGetters) => {
const workspaces = rootGetters['data/sanitizedWorkspaces']; const sanitizedWorkspacesById = rootGetters['data/sanitizedWorkspacesById'];
return workspaces.main; return sanitizedWorkspacesById.main;
}, },
currentWorkspace: ({ currentWorkspaceId }, { mainWorkspace }, rootState, rootGetters) => { currentWorkspace: ({ currentWorkspaceId }, { mainWorkspace }, rootState, rootGetters) => {
const workspaces = rootGetters['data/sanitizedWorkspaces']; const sanitizedWorkspacesById = rootGetters['data/sanitizedWorkspacesById'];
return workspaces[currentWorkspaceId] || mainWorkspace; return sanitizedWorkspacesById[currentWorkspaceId] || mainWorkspace;
}, },
currentWorkspaceIsGit: (state, { currentWorkspace }) => currentWorkspace.providerId === 'githubWorkspace',
hasUniquePaths: (state, { currentWorkspace }) => hasUniquePaths: (state, { currentWorkspace }) =>
currentWorkspace.providerId === 'githubWorkspace', currentWorkspace.providerId === 'githubWorkspace',
lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`, lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`,

View File

@ -9,8 +9,8 @@ const select = (id) => {
store.commit('explorer/setSelectedId', id); store.commit('explorer/setSelectedId', id);
expect(store.getters['explorer/selectedNode'].item.id).toEqual(id); expect(store.getters['explorer/selectedNode'].item.id).toEqual(id);
}; };
const ensureExists = file => expect(store.getters.allItemMap).toHaveProperty(file.id); const ensureExists = file => expect(store.getters.allItemsById).toHaveProperty(file.id);
const ensureNotExists = file => expect(store.getters.allItemMap).not.toHaveProperty(file.id); const ensureNotExists = file => expect(store.getters.allItemsById).not.toHaveProperty(file.id);
describe('Explorer.vue', () => { describe('Explorer.vue', () => {
it('should create new files in the root folder', () => { it('should create new files in the root folder', () => {