2018-05-04 18:07:28 +00:00
|
|
|
import store from '../store';
|
|
|
|
import utils from './utils';
|
2018-07-17 19:58:40 +00:00
|
|
|
import constants from '../data/constants';
|
2018-05-04 18:07:28 +00:00
|
|
|
|
|
|
|
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
|
|
|
|
|
|
|
export default {
|
2018-07-03 23:41:24 +00:00
|
|
|
|
2018-05-04 18:07:28 +00:00
|
|
|
/**
|
|
|
|
* Create a file in the store with the specified fields.
|
|
|
|
*/
|
2018-05-13 13:27:33 +00:00
|
|
|
async createFile({
|
2018-05-06 00:46:33 +00:00
|
|
|
name,
|
|
|
|
parentId,
|
|
|
|
text,
|
|
|
|
properties,
|
|
|
|
discussions,
|
|
|
|
comments,
|
|
|
|
} = {}, background = false) {
|
2018-05-04 18:07:28 +00:00
|
|
|
const id = utils.uid();
|
2018-06-07 23:56:11 +00:00
|
|
|
const item = {
|
2018-05-04 18:07:28 +00:00
|
|
|
id,
|
2018-05-06 00:46:33 +00:00
|
|
|
name: utils.sanitizeName(name),
|
|
|
|
parentId: parentId || null,
|
2018-05-04 18:07:28 +00:00
|
|
|
};
|
|
|
|
const content = {
|
|
|
|
id: `${id}/content`,
|
2018-05-06 00:46:33 +00:00
|
|
|
text: utils.sanitizeText(text || store.getters['data/computedSettings'].newFileContent),
|
|
|
|
properties: utils
|
|
|
|
.sanitizeText(properties || store.getters['data/computedSettings'].newFileProperties),
|
|
|
|
discussions: discussions || {},
|
|
|
|
comments: comments || {},
|
2018-05-04 18:07:28 +00:00
|
|
|
};
|
2018-07-03 23:41:24 +00:00
|
|
|
const workspaceUniquePaths = store.getters['workspace/currentWorkspaceHasUniquePaths'];
|
2018-05-13 13:27:33 +00:00
|
|
|
|
|
|
|
// Show warning dialogs
|
|
|
|
if (!background) {
|
|
|
|
// If name is being stripped
|
2018-07-17 19:58:40 +00:00
|
|
|
if (item.name !== constants.defaultName && item.name !== name) {
|
2018-06-07 23:56:11 +00:00
|
|
|
await store.dispatch('modal/open', {
|
|
|
|
type: 'stripName',
|
|
|
|
item,
|
|
|
|
});
|
2018-05-13 13:27:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check if there is already a file with that path
|
|
|
|
if (workspaceUniquePaths) {
|
2018-06-21 19:16:33 +00:00
|
|
|
const parentPath = store.getters.pathsByItemId[item.parentId] || '';
|
2018-06-07 23:56:11 +00:00
|
|
|
const path = parentPath + item.name;
|
2018-06-21 19:16:33 +00:00
|
|
|
if (store.getters.itemsByPath[path]) {
|
2018-06-07 23:56:11 +00:00
|
|
|
await store.dispatch('modal/open', {
|
|
|
|
type: 'pathConflict',
|
|
|
|
item,
|
|
|
|
});
|
2018-05-13 13:27:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save file and content in the store
|
|
|
|
store.commit('content/setItem', content);
|
2018-06-07 23:56:11 +00:00
|
|
|
store.commit('file/setItem', item);
|
2018-05-04 18:07:28 +00:00
|
|
|
if (workspaceUniquePaths) {
|
2018-05-13 13:27:33 +00:00
|
|
|
this.makePathUnique(id);
|
2018-05-04 18:07:28 +00:00
|
|
|
}
|
|
|
|
|
2018-05-13 13:27:33 +00:00
|
|
|
// Return the new file item
|
2018-06-21 19:16:33 +00:00
|
|
|
return store.state.file.itemsById[id];
|
2018-05-04 18:07:28 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Make sanity checks and then create/update the folder/file in the store.
|
|
|
|
*/
|
2018-05-13 13:27:33 +00:00
|
|
|
async storeItem(item) {
|
2018-05-04 18:07:28 +00:00
|
|
|
const id = item.id || utils.uid();
|
|
|
|
const sanitizedName = utils.sanitizeName(item.name);
|
|
|
|
|
|
|
|
if (item.type === 'folder' && forbiddenFolderNameMatcher.exec(sanitizedName)) {
|
2018-06-07 23:56:11 +00:00
|
|
|
await store.dispatch('modal/open', {
|
|
|
|
type: 'unauthorizedName',
|
|
|
|
item,
|
|
|
|
});
|
2018-05-04 18:07:28 +00:00
|
|
|
throw new Error('Unauthorized name.');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Show warning dialogs
|
2018-05-13 13:27:33 +00:00
|
|
|
// If name has been stripped
|
2018-07-17 19:58:40 +00:00
|
|
|
if (sanitizedName !== constants.defaultName && sanitizedName !== item.name) {
|
2018-06-07 23:56:11 +00:00
|
|
|
await store.dispatch('modal/open', {
|
|
|
|
type: 'stripName',
|
|
|
|
item,
|
|
|
|
});
|
2018-05-13 13:27:33 +00:00
|
|
|
}
|
2018-06-21 19:16:33 +00:00
|
|
|
|
2018-05-13 13:27:33 +00:00
|
|
|
// Check if there is a path conflict
|
2018-07-03 23:41:24 +00:00
|
|
|
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
2018-06-21 19:16:33 +00:00
|
|
|
const parentPath = store.getters.pathsByItemId[item.parentId] || '';
|
2018-05-13 13:27:33 +00:00
|
|
|
const path = parentPath + sanitizedName;
|
2018-06-21 19:16:33 +00:00
|
|
|
const items = store.getters.itemsByPath[path] || [];
|
|
|
|
if (items.some(itemWithSamePath => itemWithSamePath.id !== id)) {
|
2018-06-07 23:56:11 +00:00
|
|
|
await store.dispatch('modal/open', {
|
|
|
|
type: 'pathConflict',
|
|
|
|
item,
|
|
|
|
});
|
2018-05-04 18:07:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-13 13:27:33 +00:00
|
|
|
return this.setOrPatchItem({
|
|
|
|
...item,
|
2018-05-04 18:07:28 +00:00
|
|
|
id,
|
|
|
|
});
|
2018-05-13 13:27:33 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create/update the folder/file in the store and make sure its path is unique.
|
|
|
|
*/
|
|
|
|
setOrPatchItem(patch) {
|
|
|
|
const item = {
|
2018-06-21 19:16:33 +00:00
|
|
|
...store.getters.allItemsById[patch.id] || patch,
|
2018-05-13 13:27:33 +00:00
|
|
|
};
|
|
|
|
if (!item.id) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (patch.parentId !== undefined) {
|
|
|
|
item.parentId = patch.parentId || null;
|
|
|
|
}
|
|
|
|
if (patch.name) {
|
|
|
|
const sanitizedName = utils.sanitizeName(patch.name);
|
|
|
|
if (item.type !== 'folder' || !forbiddenFolderNameMatcher.exec(sanitizedName)) {
|
|
|
|
item.name = sanitizedName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save item in the store
|
|
|
|
store.commit(`${item.type}/setItem`, item);
|
2018-05-04 18:07:28 +00:00
|
|
|
|
2018-08-19 12:23:33 +00:00
|
|
|
// Remove circular reference
|
|
|
|
this.removeCircularReference(item);
|
|
|
|
|
2018-05-04 18:07:28 +00:00
|
|
|
// Ensure path uniqueness
|
2018-07-03 23:41:24 +00:00
|
|
|
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
2018-05-13 13:27:33 +00:00
|
|
|
this.makePathUnique(item.id);
|
2018-05-04 18:07:28 +00:00
|
|
|
}
|
2018-05-13 13:27:33 +00:00
|
|
|
|
2018-06-21 19:16:33 +00:00
|
|
|
return store.getters.allItemsById[item.id];
|
2018-05-04 18:07:28 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete a file in the store and all its related items.
|
|
|
|
*/
|
|
|
|
deleteFile(fileId) {
|
|
|
|
// Delete the file
|
|
|
|
store.commit('file/deleteItem', fileId);
|
|
|
|
// Delete the content
|
|
|
|
store.commit('content/deleteItem', `${fileId}/content`);
|
|
|
|
// Delete the syncedContent
|
|
|
|
store.commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
|
|
|
|
// Delete the contentState
|
|
|
|
store.commit('contentState/deleteItem', `${fileId}/contentState`);
|
|
|
|
// Delete sync locations
|
|
|
|
(store.getters['syncLocation/groupedByFileId'][fileId] || [])
|
|
|
|
.forEach(item => store.commit('syncLocation/deleteItem', item.id));
|
|
|
|
// Delete publish locations
|
|
|
|
(store.getters['publishLocation/groupedByFileId'][fileId] || [])
|
|
|
|
.forEach(item => store.commit('publishLocation/deleteItem', item.id));
|
|
|
|
},
|
|
|
|
|
2018-08-19 12:23:33 +00:00
|
|
|
/**
|
|
|
|
* Sanitize the whole workspace.
|
|
|
|
*/
|
|
|
|
sanitizeWorkspace(idsToKeep) {
|
|
|
|
// Detect and remove circular references for all folders.
|
|
|
|
store.getters['folder/items'].forEach(folder => this.removeCircularReference(folder));
|
|
|
|
|
|
|
|
this.ensureUniquePaths(idsToKeep);
|
|
|
|
this.ensureUniqueLocations(idsToKeep);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Detect and remove circular reference for an item.
|
|
|
|
*/
|
|
|
|
removeCircularReference(item) {
|
|
|
|
const foldersById = store.state.folder.itemsById;
|
|
|
|
for (
|
|
|
|
let parentFolder = foldersById[item.parentId];
|
|
|
|
parentFolder;
|
|
|
|
parentFolder = foldersById[parentFolder.parentId]
|
|
|
|
) {
|
|
|
|
if (parentFolder.id === item.id) {
|
|
|
|
store.commit('folder/patchItem', {
|
|
|
|
id: item.id,
|
|
|
|
parentId: null,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-05-04 18:07:28 +00:00
|
|
|
/**
|
2018-06-21 19:16:33 +00:00
|
|
|
* Ensure two files/folders don't have the same path if the workspace doesn't allow it.
|
2018-05-04 18:07:28 +00:00
|
|
|
*/
|
2018-06-21 19:16:33 +00:00
|
|
|
ensureUniquePaths(idsToKeep = {}) {
|
2018-07-03 23:41:24 +00:00
|
|
|
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
2018-06-21 19:16:33 +00:00
|
|
|
if (Object.keys(store.getters.pathsByItemId)
|
|
|
|
.some(id => !idsToKeep[id] && this.makePathUnique(id))
|
|
|
|
) {
|
|
|
|
// Just changed one item path, restart
|
|
|
|
this.ensureUniquePaths(idsToKeep);
|
2018-05-04 18:07:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return false if the file/folder path is unique.
|
|
|
|
* Add a prefix to its name and return true otherwise.
|
|
|
|
*/
|
|
|
|
makePathUnique(id) {
|
2018-06-21 19:16:33 +00:00
|
|
|
const { itemsByPath, allItemsById, pathsByItemId } = store.getters;
|
|
|
|
const item = allItemsById[id];
|
2018-05-04 18:07:28 +00:00
|
|
|
if (!item) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-06-21 19:16:33 +00:00
|
|
|
let path = pathsByItemId[id];
|
|
|
|
if (itemsByPath[path].length === 1) {
|
2018-05-04 18:07:28 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const isFolder = item.type === 'folder';
|
|
|
|
if (isFolder) {
|
|
|
|
// Remove trailing slash
|
|
|
|
path = path.slice(0, -1);
|
|
|
|
}
|
|
|
|
for (let suffix = 1; ; suffix += 1) {
|
2018-06-21 19:16:33 +00:00
|
|
|
let pathWithSuffix = `${path}.${suffix}`;
|
2018-05-04 18:07:28 +00:00
|
|
|
if (isFolder) {
|
2018-06-21 19:16:33 +00:00
|
|
|
pathWithSuffix += '/';
|
2018-05-04 18:07:28 +00:00
|
|
|
}
|
2018-06-21 19:16:33 +00:00
|
|
|
if (!itemsByPath[pathWithSuffix]) {
|
2018-05-04 18:07:28 +00:00
|
|
|
store.commit(`${item.type}/patchItem`, {
|
|
|
|
id: item.id,
|
|
|
|
name: `${item.name}.${suffix}`,
|
|
|
|
});
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2018-07-03 23:41:24 +00:00
|
|
|
|
|
|
|
addSyncLocation(location) {
|
|
|
|
store.commit('syncLocation/setItem', {
|
|
|
|
...location,
|
|
|
|
id: utils.uid(),
|
|
|
|
});
|
|
|
|
// Sanitize the workspace
|
|
|
|
this.ensureUniqueLocations();
|
|
|
|
},
|
|
|
|
|
|
|
|
addPublishLocation(location) {
|
|
|
|
store.commit('publishLocation/setItem', {
|
|
|
|
...location,
|
|
|
|
id: utils.uid(),
|
|
|
|
});
|
|
|
|
// Sanitize the workspace
|
|
|
|
this.ensureUniqueLocations();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensure two sync/publish locations of the same file don't have the same hash.
|
|
|
|
*/
|
|
|
|
ensureUniqueLocations(idsToKeep = {}) {
|
|
|
|
['syncLocation', 'publishLocation'].forEach((type) => {
|
|
|
|
store.getters[`${type}/items`].forEach((item) => {
|
|
|
|
if (!idsToKeep[item.id]
|
|
|
|
&& store.getters[`${type}/groupedByFileIdAndHash`][item.fileId][item.hash].length > 1
|
|
|
|
) {
|
|
|
|
store.commit(`${item.type}/deleteItem`, item.id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Drop the database and clean the localStorage for the specified workspaceId.
|
|
|
|
*/
|
|
|
|
async removeWorkspace(id) {
|
|
|
|
// Remove from the store first as workspace tabs will reload.
|
|
|
|
// Workspace deletion will be persisted as soon as possible
|
|
|
|
// by the store.getters['data/workspaces'] watcher in localDbSvc.
|
|
|
|
store.dispatch('workspace/removeWorkspace', id);
|
|
|
|
|
|
|
|
// Drop the database
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
const dbName = utils.getDbName(id);
|
|
|
|
const request = indexedDB.deleteDatabase(dbName);
|
|
|
|
request.onerror = resolve; // Ignore errors
|
|
|
|
request.onsuccess = resolve;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Clean the local storage
|
|
|
|
localStorage.removeItem(`${id}/lastSyncActivity`);
|
|
|
|
localStorage.removeItem(`${id}/lastWindowFocus`);
|
|
|
|
},
|
2018-05-04 18:07:28 +00:00
|
|
|
};
|