Temporary folder (part 2)

This commit is contained in:
benweet 2018-03-14 00:42:26 +00:00
parent 53f2076585
commit 88cf11972f
13 changed files with 206 additions and 221 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -1,7 +1,7 @@
<template>
<div class="button-bar">
<div class="button-bar__inner button-bar__inner--top">
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
<icon-navigation-bar></icon-navigation-bar>
</button>
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
@ -26,12 +26,17 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { mapState, mapGetters, mapActions } from 'vuex';
export default {
computed: mapGetters('data', [
'layoutSettings',
]),
computed: {
...mapState([
'light',
]),
...mapGetters('data', [
'layoutSettings',
]),
},
methods: mapActions('data', [
'toggleNavigationBar',
'toggleEditor',

View File

@ -19,7 +19,7 @@
<icon-close></icon-close>
</button>
</div>
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" tabindex="0">
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" tabindex="0" @keyup.delete="deleteItem()">
<explorer-node :node="rootNode" :depth="0"></explorer-node>
</div>
</div>

View File

@ -169,7 +169,7 @@ export default {
perform: () => this.newItem(false),
}, {
name: 'New folder',
disabled: !this.node.isFolder || this.node.isTrash,
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
perform: () => this.newItem(true),
}, {
type: 'separator',
@ -179,7 +179,6 @@ export default {
perform: () => this.setEditingId(this.node.item.id),
}, {
name: 'Delete',
disabled: this.node.isTrash || this.node.item.parentId === 'trash',
perform: () => this.deleteItem(),
}],
})

View File

@ -1,6 +1,6 @@
<template>
<div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab">
<component :is="currentModalComponent" v-if="currentModalComponent"></component>
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
<modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="config.content"></div>
<div class="modal__button-bar">
@ -56,8 +56,6 @@ const getTabbables = container => container.querySelectorAll('a[href], button, .
// Filter enabled and visible element
.cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable'));
const kebabCase = str => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, match => `-${match.toLowerCase()}`);
export default {
components: {
ModalInner,
@ -102,11 +100,15 @@ export default {
'config',
]),
currentModalComponent() {
if (!this.condig.modal) {
return '';
if (this.config.type) {
let componentName = this.config.type[0].toUpperCase();
componentName += this.config.type.slice(1);
componentName += 'Modal';
if (this.$options.components[componentName]) {
return componentName;
}
}
return `${kebabCase(this.config.modal)}-modal`;
return null;
},
},
methods: {

View File

@ -108,9 +108,7 @@ export default {
}
this.titleFakeElt.textContent = this.title;
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
return width < this.styles.titleMaxWidth
? width
: this.styles.titleMaxWidth;
return Math.min(width, this.styles.titleMaxWidth);
},
titleScrolling() {
const result = this.titleHover && !this.titleFocus;
@ -220,7 +218,7 @@ export default {
float: left;
&.navigation-bar__inner--button {
margin-right: 15px;
margin-right: 12px;
}
}

View File

@ -25,7 +25,7 @@ export default {
]),
showSponsorButton() {
const type = this.$store.getters['modal/config'].type;
return !this.$store.getters.isSponsor && type !== 'sponsor' && type !== 'signInForSponsorship';
return !this.$store.getters.isSponsor && type !== 'SponsorModal' && type !== 'signInForSponsorship';
},
},
methods: {

View File

@ -5,7 +5,6 @@ import welcomeFile from '../data/welcomeFile.md';
const dbVersion = 1;
const dbStoreName = 'objects';
const fileIdToOpen = utils.queryParams.fileId;
const exportWorkspace = utils.queryParams.exportWorkspace;
const resetApp = utils.queryParams.reset;
const deleteMarkerMaxAge = 1000;
@ -100,151 +99,6 @@ const localDbSvc = {
hashMap,
connection: null,
/**
* Create the connection and start syncing.
*/
init() {
return Promise.resolve()
.then(() => {
// Reset the app if reset flag was passed
if (resetApp) {
return Promise.all(
Object.keys(store.getters['data/workspaces'])
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
)
.then(() => utils.localStorageDataIds.forEach((id) => {
// Clean data stored in localStorage
localStorage.removeItem(`data/${id}`);
}))
.then(() => {
location.reload();
throw new Error('reload');
});
}
// Create the connection
this.connection = new Connection();
// Load the DB
return localDbSvc.sync();
})
.then(() => {
// If exportWorkspace parameter was provided
if (exportWorkspace) {
const backup = JSON.stringify(store.getters.allItemMap);
const blob = new Blob([backup], {
type: 'text/plain;charset=utf-8',
});
FileSaver.saveAs(blob, 'StackEdit workspace.json');
return;
}
// Save welcome file content hash if not done already
const hash = utils.hash(welcomeFile);
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
if (!welcomeFileHashes[hash]) {
store.dispatch('data/patchLocalSettings', {
welcomeFileHashes: {
...welcomeFileHashes,
[hash]: 1,
},
});
}
// If app was last opened 7 days ago and synchronization is off
if (!store.getters['workspace/syncToken'] &&
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
) {
// Clean files
store.getters['file/items']
.filter(file => file.parentId === 'trash') // If file is in the trash
.forEach(file => store.dispatch('deleteFile', file.id));
}
// Enable sponsorship
if (utils.queryParams.paymentSuccess) {
location.hash = ''; // PaymentSuccess param is always on its own
store.dispatch('modal/paymentSuccess');
const sponsorToken = store.getters['workspace/sponsorToken'];
// Force check sponsorship after a few seconds
const currentDate = Date.now();
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
store.dispatch('data/setGoogleToken', {
...sponsorToken,
expiresOn: currentDate - checkSponsorshipAfter,
});
}
}
// Sync local DB periodically
utils.setInterval(() => localDbSvc.sync(), 1000);
if (fileIdToOpen) {
store.commit('file/setCurrentId', fileIdToOpen);
}
// watch current file changing
store.watch(
() => store.getters['file/current'].id,
() => {
// See if currentFile is real, ie it has an ID
const currentFile = store.getters['file/current'];
// If current file has no ID, get the most recent file
if (!currentFile.id) {
const recentFile = store.getters['file/lastOpened'];
// Set it as the current file
if (recentFile.id) {
store.commit('file/setCurrentId', recentFile.id);
} else {
// If still no ID, create a new file
store.dispatch('createFile', {
name: 'Welcome file',
text: welcomeFile,
})
// Set it as the current file
.then(newFile => store.commit('file/setCurrentId', newFile.id));
}
} else {
Promise.resolve()
// Load contentState from DB
.then(() => localDbSvc.loadContentState(currentFile.id))
// Load syncedContent from DB
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
// Load content from DB
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
.then(
() => {
// Set last opened file
store.dispatch('data/setLastOpenedId', currentFile.id);
// Cancel new discussion
store.commit('discussion/setCurrentDiscussionId');
// Open the gutter if file contains discussions
store.commit('discussion/setCurrentDiscussionId',
store.getters['discussion/nextDiscussionId']);
// Add the fileId to the queryParams
utils.setQueryParams({
...utils.queryParams,
fileId: currentFile.id,
});
},
(err) => {
// Failure (content is not available), go back to previous file
const lastOpenedFile = store.getters['file/lastOpened'];
store.commit('file/setCurrentId', lastOpenedFile.id);
throw err;
},
)
.catch((err) => {
console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err);
});
}
}, {
immediate: true,
});
});
},
/**
* Sync data items stored in the localStorage.
*/
@ -485,6 +339,142 @@ const localDbSvc = {
localStorage.removeItem(`${id}/lastWindowFocus`);
});
},
/**
* Create the connection and start syncing.
*/
init() {
return Promise.resolve()
.then(() => {
// Reset the app if reset flag was passed
if (resetApp) {
return Promise.all(
Object.keys(store.getters['data/workspaces'])
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
)
.then(() => utils.localStorageDataIds.forEach((id) => {
// Clean data stored in localStorage
localStorage.removeItem(`data/${id}`);
}))
.then(() => {
location.reload();
throw new Error('reload');
});
}
// Create the connection
this.connection = new Connection();
// Load the DB
return localDbSvc.sync();
})
.then(() => {
// If exportWorkspace parameter was provided
if (exportWorkspace) {
const backup = JSON.stringify(store.getters.allItemMap);
const blob = new Blob([backup], {
type: 'text/plain;charset=utf-8',
});
FileSaver.saveAs(blob, 'StackEdit workspace.json');
return;
}
// Save welcome file content hash if not done already
const hash = utils.hash(welcomeFile);
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
if (!welcomeFileHashes[hash]) {
store.dispatch('data/patchLocalSettings', {
welcomeFileHashes: {
...welcomeFileHashes,
[hash]: 1,
},
});
}
// If app was last opened 7 days ago and synchronization is off
if (!store.getters['workspace/syncToken'] &&
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
) {
// Clean files
store.getters['file/items']
.filter(file => file.parentId === 'trash') // If file is in the trash
.forEach(file => store.dispatch('deleteFile', file.id));
}
// Enable sponsorship
if (utils.queryParams.paymentSuccess) {
location.hash = ''; // PaymentSuccess param is always on its own
store.dispatch('modal/paymentSuccess');
const sponsorToken = store.getters['workspace/sponsorToken'];
// Force check sponsorship after a few seconds
const currentDate = Date.now();
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
store.dispatch('data/setGoogleToken', {
...sponsorToken,
expiresOn: currentDate - checkSponsorshipAfter,
});
}
}
// Sync local DB periodically
utils.setInterval(() => localDbSvc.sync(), 1000);
// watch current file changing
store.watch(
() => store.getters['file/current'].id,
() => {
// See if currentFile is real, ie it has an ID
const currentFile = store.getters['file/current'];
// If current file has no ID, get the most recent file
if (!currentFile.id) {
const recentFile = store.getters['file/lastOpened'];
// Set it as the current file
if (recentFile.id) {
store.commit('file/setCurrentId', recentFile.id);
} else {
// If still no ID, create a new file
store.dispatch('createFile', {
name: 'Welcome file',
text: welcomeFile,
})
// Set it as the current file
.then(newFile => store.commit('file/setCurrentId', newFile.id));
}
} else {
Promise.resolve()
// Load contentState from DB
.then(() => localDbSvc.loadContentState(currentFile.id))
// Load syncedContent from DB
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
// Load content from DB
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
.then(
() => {
// Set last opened file
store.dispatch('data/setLastOpenedId', currentFile.id);
// Cancel new discussion
store.commit('discussion/setCurrentDiscussionId');
// Open the gutter if file contains discussions
store.commit('discussion/setCurrentDiscussionId',
store.getters['discussion/nextDiscussionId']);
},
(err) => {
// Failure (content is not available), go back to previous file
const lastOpenedFile = store.getters['file/lastOpened'];
store.commit('file/setCurrentId', lastOpenedFile.id);
throw err;
},
)
.catch((err) => {
console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err);
});
}
}, {
immediate: true,
});
});
},
};
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)

View File

@ -4,16 +4,17 @@ import utils from './utils';
import editorSvc from './editorSvc';
const origin = utils.queryParams.origin;
const existingFileId = utils.queryParams.fileId;
const fileName = utils.queryParams.fileName;
const contentText = utils.queryParams.contentText;
const contentProperties = utils.queryParams.contentProperties;
export default {
closed: false,
close() {
if (origin && window.parent) {
if (!this.closed && origin && window.parent) {
window.parent.postMessage({ type: 'close' }, origin);
}
this.closed = true;
},
init() {
if (!origin || !window.parent) {
@ -21,64 +22,39 @@ export default {
}
store.commit('setLight', true);
return Promise.resolve()
.then(() => {
const file = store.state.file.itemMap[existingFileId];
if (file) {
// If file exists, check that the origin site has created it
const fileCreation = store.getters['data/fileCreations'][file.id];
if (fileCreation && fileCreation.origin === origin) {
return file;
}
}
// Create a new temp file
return store.dispatch('createFile', {
name: fileName,
text: contentText,
properties: contentProperties,
parentId: 'temp',
});
})
return store.dispatch('createFile', {
name: fileName,
text: contentText,
properties: contentProperties,
parentId: 'temp',
})
.then((file) => {
const fileItemMap = store.state.file.itemMap;
// Sanitize file creations
const fileCreations = {};
Object.entries(store.getters['data/fileCreations']).forEach(([id, fileCreation]) => {
if (fileItemMap[id]) {
fileCreations[id] = fileCreation;
const lastCreated = {};
Object.entries(store.getters['data/lastCreated']).forEach(([id, createdOn]) => {
if (fileItemMap[id] && fileItemMap[id].parentId === 'temp') {
lastCreated[id] = createdOn;
}
});
// Track file creation from the origin site
fileCreations[file.id] = {
// Track file creation from other site
lastCreated[file.id] = {
created: Date.now(),
origin,
};
// List temp files
const tempFileCreations = [];
Object.entries(fileCreations).forEach(([id, fileCreation]) => {
if (fileItemMap[id].parentId === 'temp') {
tempFileCreations.push({
id,
created: fileCreation.created,
});
}
});
// Keep only the last 10 temp files
tempFileCreations
.sort((fileCreation1, fileCreation2) => fileCreation2.created - fileCreation1.created)
// Keep only the last 10 temp files created by other sites
Object.entries(lastCreated)
.sort(([, createdOn1], [, createdOn2]) => createdOn2 - createdOn1)
.splice(10)
.forEach((fileCreation) => {
delete fileCreations[fileCreation.id];
store.dispatch('deleteFile', fileCreation.id);
.forEach(([id]) => {
delete lastCreated[id];
store.dispatch('deleteFile', id);
});
// Store file creations and open the file
store.dispatch('data/setFileCreations', fileCreations);
store.dispatch('data/setLastCreated', lastCreated);
store.commit('file/setCurrentId', file.id);
const onChange = cledit.Utils.debounce(() => {
@ -86,7 +62,7 @@ export default {
if (currentFile.id !== file.id) {
// Close editor if file has changed for some reason
this.close();
} else if (editorSvc.previewCtx.html != null) {
} else if (!this.closed && editorSvc.previewCtx.html != null) {
const content = store.getters['content/current'];
const properties = utils.computeProperties(content.properties);
window.parent.postMessage({

View File

@ -170,7 +170,7 @@ export default {
...getters.templates,
...additionalTemplates,
}),
fileCreations: getter('fileCreations'),
lastCreated: getter('lastCreated'),
lastOpened: getter('lastOpened'),
lastOpenedIds: (state, getters, rootState) => {
const lastOpened = {
@ -262,7 +262,7 @@ export default {
});
commit('setItem', itemTemplate('templates', dataToCommit));
},
setFileCreations: setter('fileCreations'),
setLastCreated: setter('lastCreated'),
setLastOpenedId: ({ getters, commit, dispatch, rootState }, fileId) => {
const lastOpened = { ...getters.lastOpened };
lastOpened[fileId] = Date.now();

View File

@ -69,17 +69,26 @@ export default {
},
getters: {
nodeStructure: (state, getters, rootState, rootGetters) => {
const rootNode = new Node(emptyFolder(), [], true, true);
// Create Trash node
const trashFolderNode = new Node(emptyFolder(), [], true);
trashFolderNode.item.id = 'trash';
trashFolderNode.item.name = 'Trash';
trashFolderNode.noDrag = true;
trashFolderNode.isTrash = true;
trashFolderNode.parentNode = rootNode;
// Create Temp node
const tempFolderNode = new Node(emptyFolder(), [], true);
tempFolderNode.item.id = 'temp';
tempFolderNode.item.name = 'Temp';
tempFolderNode.noDrag = true;
tempFolderNode.noDrop = true;
tempFolderNode.isTemp = true;
tempFolderNode.parentNode = rootNode;
// Fill nodeMap with all file and folder nodes
const nodeMap = {
trash: trashFolderNode,
temp: tempFolderNode,
@ -96,7 +105,8 @@ export default {
];
nodeMap[item.id] = new Node(item, locations);
});
const rootNode = new Node(emptyFolder(), [], true, true);
// Build the tree
Object.entries(nodeMap).forEach(([, node]) => {
let parentNode = nodeMap[node.item.parentId];
if (!parentNode || !parentNode.isFolder) {
@ -110,8 +120,11 @@ export default {
} else {
parentNode.files.push(node);
}
node.parentNode = parentNode;
});
rootNode.sortChildren();
// Add Trash and Temp nodes
rootNode.folders.unshift(tempFolderNode);
tempFolderNode.files.forEach((node) => {
node.noDrop = true;
@ -119,7 +132,8 @@ export default {
if (trashFolderNode.files.length) {
rootNode.folders.unshift(trashFolderNode);
}
// Add a fake file at the end of the root folder to allow drag and drop into it.
// Add a fake file at the end of the root folder to allow drag and drop into it
rootNode.files.push(fakeFileNode);
return {
nodeMap,

View File

@ -1,7 +1,7 @@
const minPadding = 20;
const editorTopPadding = 10;
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
const navigationBarLeftButtonWidth = 38 + 4 + 15;
const navigationBarLeftButtonWidth = 38 + 4 + 12;
const navigationBarRightButtonWidth = 38 + 8;
const navigationBarSpinnerWidth = 24 + 8 + 5; // 5 for left margin
const navigationBarLocationWidth = 20;
@ -23,7 +23,8 @@ const constants = {
function computeStyles(state, getters, layoutSettings = getters['data/layoutSettings'], styles = {
showNavigationBar: layoutSettings.showNavigationBar
|| !layoutSettings.showEditor
|| state.content.revisionContent,
|| state.content.revisionContent
|| state.light,
showStatusBar: layoutSettings.showStatusBar,
showEditor: layoutSettings.showEditor,
showSidePreview: layoutSettings.showSidePreview && layoutSettings.showEditor,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB