Commit 40ea8633 authored by Phil Hughes's avatar Phil Hughes

moved temp file & folder creation into a web worker

it wasn't previously affecting the performance in any way, however this keeps all the logic of creating folders & files in the worker which we can just post to whenever we need to

[ci skip]
parent 45b9d60b
......@@ -33,6 +33,7 @@ export default {
v-for="file in tree.tree"
:key="file.key"
:file="file"
:level="0"
/>
</div>
</template>
<script>
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapState, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
......
<script>
import { mapActions } from 'vuex';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
......@@ -23,6 +24,10 @@
type: Object,
required: true,
},
level: {
type: Number,
required: true,
},
},
computed: {
isTree() {
......@@ -33,14 +38,14 @@
},
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
marginLeft: `${this.level * 16}px`,
};
},
fileClass() {
return {
'file-open': this.isBlob && this.file.opened,
'file-active': this.isBlob && this.file.active,
'folder': this.isTree,
folder: this.isTree,
};
},
},
......@@ -50,15 +55,15 @@
}
},
methods: {
clickFile(row) {
...mapActions([
'toggleTreeOpen',
]),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
this.$store.dispatch('toggleTreeOpen', {
endpoint: this.file.url,
tree: this.file,
});
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
this.$router.push(`/project${row.url}`);
this.$router.push(`/project${this.file.url}`);
},
},
};
......@@ -72,7 +77,7 @@
>
<div
class="file-name"
@click="clickFile(file)"
@click="clickFile"
role="button"
>
<span
......@@ -110,6 +115,7 @@
v-for="childFile in file.tree"
:key="childFile.key"
:file="childFile"
:level="level + 1"
/>
</template>
</div>
......
......@@ -44,7 +44,6 @@ export const createTempEntry = (
dispatch('createTempTree', {
projectId,
branchId,
parent: selectedParent,
name,
});
} else if (type === 'blob') {
......
......@@ -4,10 +4,9 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
findEntry,
setPageTitle,
createTemp,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.indexOf(path);
......@@ -106,37 +105,44 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
}
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
const path = parent.path !== undefined ? parent.path : '';
// We need to do the replacement otherwise the web_url + file.url duplicate
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
projectId,
branchId,
name: name.replace(`${path}/`, ''),
path,
type: 'blob',
level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
url: newUrl,
});
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) =>
new Promise((resolve) => {
const worker = new FilesDecoratorWorker();
const path = parent.path !== undefined ? `${parent.path}/` : '';
const entryPath = path ? `${path}/${name}` : name;
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
if (state.entries[entryPath]) {
return flash(`The name "${name}" is already taken in this directory.`, 'alert', document, null, false, true);
}
worker.addEventListener('message', ({ data }) => {
const { file } = data;
worker.terminate();
commit(types.CREATE_TMP_FILE, {
parent,
file,
data,
projectId,
branchId,
});
commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
router.push(`/project${file.url}`);
resolve();
});
return Promise.resolve(file);
};
worker.postMessage({
data: [entryPath],
projectId,
branchId,
tempFile: true,
content,
base64,
});
return null;
});
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
......
......@@ -3,10 +3,8 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
findEntry,
createTemp,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
......@@ -33,41 +31,30 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
export const createTempTree = (
{ state, commit, dispatch },
{ projectId, branchId, parent, name },
) => {
let selectedTree = parent;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
{ projectId, branchId, name },
) => new Promise((resolve) => {
const dirName = name.replace(new RegExp(`^${state.path}/`), '');
const worker = new FilesDecoratorWorker();
dirNames.forEach((dirName) => {
const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
worker.addEventListener('message', ({ data }) => {
worker.terminate();
if (!foundEntry) {
const path = selectedTree.path !== undefined ? selectedTree.path : '';
const tmpEntry = createTemp({
commit(types.CREATE_TMP_TREE, {
data,
projectId,
branchId,
name: dirName,
path,
type: 'tree',
level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
tree: [],
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
});
commit(types.CREATE_TMP_TREE, {
parent: selectedTree,
tmpEntry,
resolve();
});
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
router.push(`/project${tmpEntry.url}`);
selectedTree = tmpEntry;
} else {
selectedTree = foundEntry;
}
worker.postMessage({
data: [`${dirName}/`],
projectId,
branchId,
tempFile: true,
});
};
});
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
......
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
return state.canCommit &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
......
......@@ -65,8 +65,33 @@ export default {
changed: false,
});
},
[types.CREATE_TMP_FILE](state, { file, parent }) {
parent.tree.push(file);
[types.CREATE_TMP_FILE](state, { data, projectId, branchId }) {
Object.keys(data.entries).forEach((key) => {
const entry = data.entries[key];
Object.assign(state.entries, {
[key]: entry,
});
});
Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
// Object.assign(state.entries, {
// [file.path]: file,
// });
// if (parent.path) {
// // Add it as a child of the parent
// Object.assign(state.entries[parent.path], {
// tree: parent.tree.concat(file),
// });
// } else {
// // Add it the root
// Object.assign(parent, {
// tree: parent.tree.concat(file),
// });
// }
},
[types.ADD_FILE_TO_CHANGED](state, path) {
Object.assign(state, {
......
......@@ -30,8 +30,18 @@ export default {
lastCommitPath: url,
});
},
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
[types.CREATE_TMP_TREE](state, { data, projectId, branchId }) {
Object.keys(data.entries).forEach((key) => {
const entry = data.entries[key];
Object.assign(state.entries, {
[key]: entry,
});
});
Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
},
[types.REMOVE_ALL_CHANGES_FILES](state) {
Object.assign(state, {
......
export default () => ({
canCommit: false,
currentProjectId: '',
currentBranchId: '',
changedFiles: [],
endpoints: {},
isInitialRoot: false,
lastCommitMsg: '',
lastCommitPath: '',
loading: false,
onTopOfBranch: false,
openFiles: [],
selectedFile: null,
path: '',
parentTreeUrl: '',
trees: {},
projects: {},
......
import _ from 'underscore';
export const dataStructure = () => ({
id: '',
key: '',
......@@ -9,7 +7,6 @@ export const dataStructure = () => ({
name: '',
url: '',
path: '',
level: 0,
tempFile: false,
tree: [],
loading: false,
......@@ -24,7 +21,6 @@ export const dataStructure = () => ({
updatedAt: '',
author: '',
},
tree_url: '',
blamePath: '',
commitsPath: '',
permalink: '',
......@@ -50,7 +46,6 @@ export const decorateData = (entity) => {
type,
url,
name,
tree_url,
path,
renderError,
content = '',
......@@ -59,7 +54,6 @@ export const decorateData = (entity) => {
opened = false,
changed = false,
parentTreeUrl = '',
level = 0,
base64 = false,
file_lock,
......@@ -75,9 +69,7 @@ export const decorateData = (entity) => {
type,
name,
url,
tree_url,
path,
level,
tempFile,
opened,
active,
......@@ -92,32 +84,6 @@ export const decorateData = (entity) => {
};
};
/*
Takes the multi-dimensional tree and returns a flattened array.
This allows for the table to recursively render the table rows but keeps the data
structure nested to make it easier to add new files/directories.
*/
export const treeList = (state, treeId) => {
const baseTree = state.trees[treeId];
if (baseTree) {
const mapTree = arr => (!arr.tree || !arr.tree.length ?
[] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(baseTree.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
}
return [];
};
export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
export const getTreeEntry = (store, treeId, path) => {
const fileList = treeList(store.state, treeId);
return fileList ? fileList.find(file => file.path === path) : null;
};
export const findEntry = (tree, type, name, prop = 'name') => tree.find(
f => f.type === type && f[prop] === name,
);
......@@ -151,39 +117,6 @@ export const createTemp = ({
});
};
export const createOrMergeEntry = ({ projectId,
branchId,
entry,
type,
parentTreeUrl,
level,
state }) => {
if (state.changedFiles.length) {
const foundChangedFile = findEntry(state.changedFiles, type, entry.path, 'path');
if (foundChangedFile) {
return foundChangedFile;
}
}
if (state.openFiles.length) {
const foundOpenFile = findEntry(state.openFiles, type, entry.path, 'path');
if (foundOpenFile) {
return foundOpenFile;
}
}
return decorateData({
...entry,
projectId,
branchId,
type,
parentTreeUrl,
level,
});
};
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
......@@ -210,11 +143,6 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
export const sortTree = (sortedTree) => {
sortedTree.forEach((el) => {
Object.assign(el, {
tree: el && el.tree ? sortTree(el.tree) : [],
});
});
return sortedTree.sort(sortTreesByTypeAndName);
};
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
})).sort(sortTreesByTypeAndName);
......@@ -4,15 +4,16 @@ import {
} from '../utils';
self.addEventListener('message', (e) => {
const { data, projectId, branchId } = e.data;
const { data, projectId, branchId, tempFile = false } = e.data;
const treeList = [];
let file;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop();
const blobName = pathSplit.pop().trim();
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName, folderLevel) => {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
const folderPath = `${(parentFolder ? `${parentFolder.path}/` : '')}${folderName}`;
const foundEntry = acc[folderPath];
......@@ -25,9 +26,11 @@ self.addEventListener('message', (e) => {
name: folderName,
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}`,
level: parentFolder ? parentFolder.level + 1 : folderLevel,
type: 'tree',
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
});
Object.assign(acc, {
......@@ -49,17 +52,19 @@ self.addEventListener('message', (e) => {
}, []);
}
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
const file = decorateData({
file = decorateData({
projectId,
branchId,
id: path,
name: blobName,
path,
url: `/${projectId}/blob/${branchId}/${path}`,
level: fileFolder ? fileFolder.level + 1 : 0,
type: 'blob',
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
});
Object.assign(acc, {
......@@ -71,6 +76,7 @@ self.addEventListener('message', (e) => {
} else {
treeList.push(file);
}
}
return acc;
}, {});
......@@ -78,5 +84,6 @@ self.addEventListener('message', (e) => {
self.postMessage({
entries,
treeList: sortTree(treeList),
file,
});
});
......@@ -26,38 +26,6 @@ describe('Multi-file store getters', () => {
});
});
describe('canEditFile', () => {
beforeEach(() => {
localState.onTopOfBranch = true;
localState.canCommit = true;
localState.openFiles.push(file());
localState.openFiles[0].active = true;
});
it('returns true if user can commit and has open files', () => {
expect(getters.canEditFile(localState)).toBeTruthy();
});
it('returns false if user can commit and has no open files', () => {
localState.openFiles = [];
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user can commit and active file is binary', () => {
localState.openFiles[0].binary = true;
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user cant commit', () => {
localState.canCommit = false;
expect(getters.canEditFile(localState)).toBeFalsy();
});
});
describe('modifiedFiles', () => {
it('returns a list of modified files', () => {
localState.openFiles.push(file());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment