Commit ea109251 authored by Phil Hughes's avatar Phil Hughes

moved the bulk the data generation into a worker

[ci skip]
parent 6c938804
......@@ -31,13 +31,13 @@
},
},
computed: {
...mapState([
'changedFiles',
]),
...mapGetters([
'activeFile',
'openFilesMap',
]),
...mapGetters({
openFiles: 'openFilesMap',
changedFiles: 'changedFilesMap',
}),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
......@@ -65,7 +65,7 @@
v-if="activeFile"
>
<repo-tabs
:files="openFilesMap"
:files="openFiles"
/>
<repo-editor
class="multi-file-edit-pane-content"
......
<script>
import { mapState, mapActions } from 'vuex';
import { mapState, mapGetters, mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
......@@ -25,8 +25,10 @@
computed: {
...mapState([
'rightPanelCollapsed',
'changedFiles',
]),
...mapGetters({
changedFiles: 'changedFilesMap',
}),
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
......
......@@ -36,8 +36,10 @@ export default {
'currentBranchId',
'rightPanelCollapsed',
'lastCommitMsg',
'changedFiles',
]),
...mapGetters({
changedFiles: 'changedFilesMap',
}),
...mapState('commit', [
'commitMessage',
'submitCommitLoading',
......@@ -53,7 +55,6 @@ export default {
},
methods: {
...mapActions([
'getTreeData',
'setPanelCollapsedStatus',
]),
...mapActions('commit', [
......
......@@ -86,7 +86,7 @@ export default {
if (file.active) {
this.changeFileContent({
file,
path: file.path,
content: model.getModel().getValue(),
});
}
......
......@@ -67,7 +67,7 @@
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
@click.stop.prevent="closeFile(tab.path)"
:aria-label="closeLabel"
>
<icon
......
......@@ -7,15 +7,15 @@ import {
findEntry,
setPageTitle,
createTemp,
findIndexOfFile,
} from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, file) => {
const indexOfClosedFile = state.openFiles.indexOf(file.path);
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.indexOf(path);
const file = state.entries[path];
const fileWasActive = file.active;
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.SET_FILE_ACTIVE, { file, active: false });
commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false });
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
......@@ -29,16 +29,17 @@ export const closeFile = ({ commit, state, getters, dispatch }, file) => {
dispatch('getLastCommitData');
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
const file = state.entries[path];
const currentActiveFile = getters.activeFile;
if (file.active) return;
if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
commit(types.SET_FILE_ACTIVE, { path: currentActiveFile.path, active: false });
}
commit(types.SET_FILE_ACTIVE, { file, active: true });
commit(types.SET_FILE_ACTIVE, { path, active: true });
dispatch('scrollToTab');
// reset hash for line highlighting
......@@ -61,8 +62,8 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
})
.then((data) => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
......@@ -77,15 +78,16 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile
})
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ state, commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
const indexOfChangedFile = state.changedFiles.indexOf(path);
if (file.changed && indexOfChangedFile === -1) {
commit(types.ADD_FILE_TO_CHANGED, file);
} else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, file);
if (!file.changed && indexOfChangedFile === -1) {
commit(types.ADD_FILE_TO_CHANGED, path);
} else if (file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
};
......@@ -130,9 +132,9 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
parent,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.ADD_FILE_TO_CHANGED, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
router.push(`/project${file.url}`);
......@@ -144,6 +146,6 @@ export const discardFileChanges = ({ commit }, file) => {
commit(types.REMOVE_FILE_FROM_CHANGED, file);
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.TOGGLE_FILE_OPEN, file.path);
}
};
......@@ -9,55 +9,8 @@ import {
findEntry,
createTemp,
createOrMergeEntry,
decorateData,
} from '../utils';
export const getTreeData = (
{ commit, state, dispatch },
{ endpoint, tree = null, projectId, branch, force = false } = {},
) => new Promise((resolve, reject) => {
// We already have the base tree so we resolve immediately
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
resolve();
} else {
if (tree) commit(types.TOGGLE_LOADING, { entry: tree });
const selectedProject = state.projects[projectId];
// We are merging the web_url that we got on the project info with the endpoint
// we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
if (completeEndpoint && (!tree || !tree.tempFile)) {
service.getTreeData(completeEndpoint)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
dispatch('updateDirectoryData', { data, tree, projectId, branch, clearTree: false });
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
if (tree) commit(types.TOGGLE_LOADING, { entry: selectedTree });
const prevLastCommitPath = selectedTree.lastCommitPath;
if (prevLastCommitPath !== null) {
dispatch('getLastCommitData', selectedTree);
}
resolve(data);
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, { entry: tree });
reject(e);
});
} else {
resolve();
}
}
});
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, { tree }) => {
commit(types.TOGGLE_TREE_OPEN, tree);
......@@ -74,10 +27,10 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
visitUrl(row.url);
} else if (row.type === 'blob' && (row.opened || row.changed)) {
if (row.changed && !row.opened) {
commit(types.TOGGLE_FILE_OPEN, row);
commit(types.TOGGLE_FILE_OPEN, row.path);
}
dispatch('setFileActive', row);
dispatch('setFileActive', row.path);
} else {
dispatch('getFileData', row);
}
......@@ -146,47 +99,6 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const updateDirectoryData = (
{ commit, state },
{ data, tree, projectId, branch, clearTree = true },
) => {
if (!tree) {
const existingTree = state.trees[`${projectId}/${branch}`];
if (!existingTree) {
commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
}
}
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
const createEntry = (entry, type) => createOrMergeEntry({
projectId: `${projectId}`,
branchId: branch,
entry,
level,
type,
parentTreeUrl,
state,
});
let formattedData = [
...data.trees.map(t => createEntry(t, 'tree')),
...data.submodules.map(m => createEntry(m, 'submodule')),
...data.blobs.map(b => createEntry(b, 'blob')),
];
if (!clearTree && tree) {
const tempFiles = state.changedFiles.filter(f => f.tempFile && f.path === `${tree.path}/${f.name}`);
if (tempFiles.length) {
formattedData = formattedData.concat(tempFiles);
}
}
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
};
export const getFiles = (
{ state, commit, dispatch },
{ projectId, branchId } = {},
......@@ -199,73 +111,25 @@ export const getFiles = (
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then((data) => {
const treeList = [];
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop();
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName, folderLevel) => {
const folderPath = `${(pathAcc.length ? `${pathAcc[pathAcc.length - 1]}/` : '')}${folderName}`;
const foundEntry = state.entries[folderPath];
if (!foundEntry) {
const tree = decorateData({
projectId,
branchId,
id: folderPath,
name: folderName,
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}`,
level: folderLevel,
type: 'tree',
});
Object.assign(acc, {
[folderPath]: tree,
});
if (folderLevel === 0) {
treeList.push(tree.path);
}
}
return pathAcc;
}, []);
}
const fileFolder = acc[pathSplit.join('/')];
const file = decorateData({
projectId,
branchId,
id: path,
name: blobName,
path,
url: `/${projectId}/blob/${branchId}/${path}`,
level: fileFolder ? fileFolder.level + 1 : 0,
type: 'blob',
});
Object.assign(acc, {
[path]: file,
});
if (fileFolder) {
fileFolder.tree.push(path);
} else {
treeList.push(file.path);
}
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', (e) => {
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
return acc;
}, {});
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: treeList });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
const selectedTree = state.trees[`${projectId}/${branchId}`];
worker.terminate();
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: treeList });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
resolve();
});
resolve();
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
......
import { sortTree } from './utils';
export const openFilesMap = state => state.openFiles.map(path => state.entries[path]);
export const changedFilesMap = state => state.changedFiles.map(path => state.entries[path]);
export const activeFile = state => openFilesMap(state).find(file => file.active) || null;
......@@ -11,9 +12,9 @@ export const canEditFile = (state) => {
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const addedFiles = state => changedFilesMap(state).filter(f => f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const modifiedFiles = state => changedFilesMap(state).filter(f => !f.tempFile);
export const treeList = (state) => {
const tree = state.trees['root/testing-123/master'];
......
......@@ -41,6 +41,7 @@ export default {
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
console.log(entry);
Object.assign(entry.lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path,
......
import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { file, active }) {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state, {
entries: {
...state.entries,
[file.path]: {
...state.entries[file.path],
[path]: {
...state.entries[path],
active,
},
},
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
[types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state, {
entries: {
...state.entries,
[file.path]: {
...state.entries[file.path],
opened: !state.entries[file.path].opened,
[path]: {
...state.entries[path],
opened: !state.entries[path].opened,
},
},
});
if (state.entries[file.path].opened) {
state.openFiles.push(file.path);
if (state.entries[path].opened) {
state.openFiles.push(path);
} else {
state.openFiles.splice(state.openFiles.indexOf(file.path), 1);
state.openFiles.splice(state.openFiles.indexOf(path), 1);
}
},
[types.SET_FILE_DATA](state, { data, file }) {
......@@ -58,12 +57,18 @@ export default {
},
});
},
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
const changed = content !== file.raw;
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
Object.assign(file, {
content,
changed,
Object.assign(state, {
entries: {
...state.entries,
[path]: {
...state.entries[path],
content,
changed,
},
},
});
},
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
......@@ -101,25 +106,35 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: file.raw,
changed: false,
Object.assign(state, {
entries: {
...state.entries,
[file.path]: {
...state.entries[file.path],
content: state.entries[file.path].raw,
changed: false,
},
},
});
},
[types.CREATE_TMP_FILE](state, { file, parent }) {
parent.tree.push(file);
},
[types.ADD_FILE_TO_CHANGED](state, file) {
state.changedFiles.push(file);
[types.ADD_FILE_TO_CHANGED](state, path) {
state.changedFiles.push(path);
},
[types.REMOVE_FILE_FROM_CHANGED](state, file) {
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
state.changedFiles.splice(indexOfChangedFile, 1);
[types.REMOVE_FILE_FROM_CHANGED](state, path) {
state.changedFiles.splice(state.changedFiles.indexOf(path), 1);
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(file, {
changed,
Object.assign(state, {
entries: {
...state.entries,
[file.path]: {
...state.entries[file.path],
changed,
},
},
});
},
};
import { decorateData } from '../utils';
self.addEventListener('message', (e) => {
const { data, projectId, branchId } = e.data;
const treeList = [];
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop();
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName, folderLevel) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
const folderPath = `${(parentFolder ? `${parentFolder.path}/` : '')}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
const tree = decorateData({
projectId,
branchId,
id: folderPath,
name: folderName,
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}`,
level: parentFolder ? parentFolder.level + 1 : folderLevel,
type: 'tree',
});
Object.assign(acc, {
[folderPath]: tree,
});
if (parentFolder) {
parentFolder.tree.push(tree.path);
} else {
treeList.push(tree.path);
}
pathAcc.push(tree.path);
} else {
pathAcc.push(foundEntry.path);
}
return pathAcc;
}, []);
}
const fileFolder = acc[pathSplit.join('/')];
const file = decorateData({
projectId,
branchId,
id: path,
name: blobName,
path,
url: `/${projectId}/blob/${branchId}/${path}`,
level: fileFolder ? fileFolder.level + 1 : 0,
type: 'blob',
});
Object.assign(acc, {
[path]: file,
});
if (fileFolder) {
fileFolder.tree.push(path);
} else {
treeList.push(file.path);
}
return acc;
}, {});
self.postMessage({
entries,
treeList,
});
});
......@@ -33,113 +33,6 @@ describe('Multi-file store tree actions', () => {
resetStore(store);
});
describe('getTreeData', () => {
beforeEach(() => {
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree', path: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
});
it('calls service getTreeData', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
done();
}).catch(done.fail);
});
it('adds data into tree', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
projectTree = store.state.trees['abcproject/master'];
expect(projectTree.tree.length).toBe(3);
expect(projectTree.tree[0].type).toBe('tree');
expect(projectTree.tree[1].type).toBe('submodule');
expect(projectTree.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
it('adds temp files into tree', (done) => {
const f = {
...file('tempFile'),
path: 'tree/tempFile',
tempFile: true,
};
store.state.changedFiles.push(f);
store.dispatch('getTreeData', basicCallParameters)
.then(() => store.dispatch('getTreeData', {
...basicCallParameters,
tree: store.state.trees['abcproject/master'].tree[0],
}))
.then(() => {
const tree = store.state.trees['abcproject/master'].tree[0].tree;
expect(tree.length).toBe(4);
expect(tree[3].name).toBe(f.name);
done();
}).catch(done.fail);
});
it('sets parent tree URL', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.parentTreeUrl).toBe('parent_tree_url');
done();
}).catch(done.fail);
});
it('sets last commit path', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.trees['abcproject/master'].lastCommitPath).toBe('last_commit_path');
done();
}).catch(done.fail);
});
it('sets page title', (done) => {
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(document.title).toBe('test');
done();
}).catch(done.fail);
});
it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
store.state.prevLastCommitPath = 'test';
store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(getLastCommitDataSpy).toHaveBeenCalledWith(projectTree);
store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
done();
}).catch(done.fail);
});
});
describe('getFiles', () => {
beforeEach(() => {
spyOn(service, 'getFiles').and.returnValue(Promise.resolve({
......@@ -339,145 +232,4 @@ describe('Multi-file store tree actions', () => {
}).catch(done.fail);
});
});
describe('updateDirectoryData', () => {
it('adds data into tree', (done) => {
const tree = {
tree: [],
};
const data = {
trees: [{ name: 'tree' }],
submodules: [{ name: 'submodule' }],
blobs: [{ name: 'blob' }],
};
store.dispatch('updateDirectoryData', {
data,
tree,
}).then(() => {
expect(tree.tree[0].name).toBe('tree');
expect(tree.tree[0].type).toBe('tree');
expect(tree.tree[1].name).toBe('submodule');
expect(tree.tree[1].type).toBe('submodule');
expect(tree.tree[2].name).toBe('blob');
expect(tree.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
it('adds changed state of an already existing file', (done) => {
const f = file('changedFile');
const tree = {
tree: [],
};
const data = {
trees: [{ name: 'tree' }],
submodules: [{ name: 'submodule' }],
blobs: [f],
};
store.state.changedFiles.push({
...f,
type: 'blob',
changed: true,
});
store.dispatch('updateDirectoryData', {
data,
tree,
clearTree: false,
}).then(() => {
expect(tree.tree[2].changed).toBeTruthy();
done();
}).catch(done.fail);
});
it('adds opened state of an already existing file', (done) => {
const f = file('openedFile');
const tree = {
tree: [],
};
const data = {
trees: [{ name: 'tree' }],
submodules: [{ name: 'submodule' }],
blobs: [f],
};
store.state.openFiles.push({
...f,
type: 'blob',
opened: true,
});
store.dispatch('updateDirectoryData', {
data,
tree,
clearTree: false,
}).then(() => {
expect(tree.tree[2].opened).toBeTruthy();
done();
}).catch(done.fail);
});
it('does not add changed file with same name but different path', (done) => {
const f = file('openedFile');
const tree = {
tree: [],
};
const data = {
trees: [{ name: 'tree' }],
submodules: [{ name: 'submodule' }],
blobs: [f],
};
store.state.changedFiles.push({
...f,
type: 'blob',
path: `src/${f.name}`,
changed: true,
});
store.dispatch('updateDirectoryData', {
data,
tree,
clearTree: false,
}).then(() => {
expect(tree.tree[2].changed).toBeFalsy();
done();
}).catch(done.fail);
});
it('does not add opened file with same name but different path', (done) => {
const f = file('openedFile');
const tree = {
tree: [],
};
const data = {
trees: [{ name: 'tree' }],
submodules: [{ name: 'submodule' }],
blobs: [f],
};
store.state.openFiles.push({
...f,
type: 'blob',
path: `src/${f.name}`,
opened: true,
});
store.dispatch('updateDirectoryData', {
data,
tree,
clearTree: false,
}).then(() => {
expect(tree.tree[2].opened).toBeFalsy();
done();
}).catch(done.fail);
});
});
});
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