Commit 3630d71d authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '66293-webide-renaming' into 'master'

Resolve "Renaming in the Web IDE causes file changes to be lost"

Closes #31058

See merge request gitlab-org/gitlab!16539
parents 0450e8ee 3f6bdd5e
...@@ -43,7 +43,12 @@ export default { ...@@ -43,7 +43,12 @@ export default {
<template> <template>
<div class="d-flex ide-commit-editor-header align-items-center"> <div class="d-flex ide-commit-editor-header align-items-center">
<file-icon :file-name="activeFile.name" :size="16" class="mr-2" /> <file-icon :file-name="activeFile.name" :size="16" class="mr-2" />
<strong class="mr-2"> {{ activeFile.path }} </strong> <strong class="mr-2">
<template v-if="activeFile.prevPath && activeFile.prevPath !== activeFile.path">
{{ activeFile.prevPath }} &#x2192;
</template>
{{ activeFile.path }}
</strong>
<changed-file-icon :file="activeFile" :is-centered="false" /> <changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto"> <div class="ml-auto">
<button <button
......
...@@ -110,6 +110,9 @@ export default { ...@@ -110,6 +110,9 @@ export default {
> >
<span class="multi-file-commit-list-file-path d-flex align-items-center"> <span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon :file-name="file.name" class="append-right-8" /> <file-icon :file-name="file.name" class="append-right-8" />
<template v-if="file.prevName && file.prevName !== file.name">
{{ file.prevName }} &#x2192;
</template>
{{ file.name }} {{ file.name }}
</span> </span>
<div class="ml-auto d-flex align-items-center"> <div class="ml-auto d-flex align-items-center">
......
...@@ -34,6 +34,9 @@ export default { ...@@ -34,6 +34,9 @@ export default {
'getUnstagedFilesCountForPath', 'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath', 'getStagedFilesCountForPath',
]), ]),
isTree() {
return this.file.type === 'tree';
},
folderUnstagedCount() { folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path); return this.getUnstagedFilesCountForPath(this.file.path);
}, },
...@@ -58,10 +61,13 @@ export default { ...@@ -58,10 +61,13 @@ export default {
}); });
}, },
showTreeChangesCount() { showTreeChangesCount() {
return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened; return this.isTree && this.changesCount > 0 && !this.file.opened;
},
isModified() {
return this.file.changed || this.file.tempFile || this.file.staged || this.file.prevPath;
}, },
showChangedFileIcon() { showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged; return !this.isTree && this.isModified;
}, },
}, },
}; };
......
...@@ -30,9 +30,6 @@ export default { ...@@ -30,9 +30,6 @@ export default {
showLoading() { showLoading() {
return !this.currentTree || this.currentTree.loading; return !this.currentTree || this.currentTree.loading;
}, },
actualTreeList() {
return this.currentTree.tree.filter(entry => !entry.moved);
},
}, },
mounted() { mounted() {
this.updateViewer(this.viewerType); this.updateViewer(this.viewerType);
...@@ -57,9 +54,9 @@ export default { ...@@ -57,9 +54,9 @@ export default {
<slot name="header"></slot> <slot name="header"></slot>
</header> </header>
<div class="ide-tree-body h-100"> <div class="ide-tree-body h-100">
<template v-if="actualTreeList.length"> <template v-if="currentTree.tree.length">
<file-row <file-row
v-for="file in actualTreeList" v-for="file in currentTree.tree"
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
......
...@@ -91,7 +91,6 @@ export default { ...@@ -91,7 +91,6 @@ export default {
this.renameEntry({ this.renameEntry({
path: this.entryModal.entry.path, path: this.entryModal.entry.path,
name: entryName, name: entryName,
entryPath: null,
parentPath, parentPath,
}), }),
) )
......
...@@ -155,15 +155,7 @@ export default { ...@@ -155,15 +155,7 @@ export default {
this.editor.clearEditor(); this.editor.clearEditor();
this.getFileData({ this.fetchFileData()
path: this.file.path,
makeFileActive: false,
})
.then(() =>
this.getRawFileData({
path: this.file.path,
}),
)
.then(() => { .then(() => {
this.createEditorInstance(); this.createEditorInstance();
}) })
...@@ -179,6 +171,20 @@ export default { ...@@ -179,6 +171,20 @@ export default {
throw err; throw err;
}); });
}, },
fetchFileData() {
if (this.file.tempFile) {
return Promise.resolve();
}
return this.getFileData({
path: this.file.path,
makeFileActive: false,
}).then(() =>
this.getRawFileData({
path: this.file.path,
}),
);
},
createEditorInstance() { createEditorInstance() {
this.editor.dispose(); this.editor.dispose();
......
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../stores/utils'; import { decorateData, sortTree, escapeFileUrl } from '../stores/utils';
export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
export const splitParent = path => { export const splitParent = path => {
const idx = path.lastIndexOf('/'); const idx = path.lastIndexOf('/');
......
...@@ -9,6 +9,7 @@ import { decorateFiles } from '../lib/files'; ...@@ -9,6 +9,7 @@ import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants'; import { stageKeys } from '../constants';
import service from '../services'; import service from '../services';
import router from '../ide_router'; import router from '../ide_router';
import eventHub from '../eventhub';
export const redirectToUrl = (self, url) => visitUrl(url); export const redirectToUrl = (self, url) => visitUrl(url);
...@@ -171,8 +172,10 @@ export const setCurrentBranchId = ({ commit }, currentBranchId) => { ...@@ -171,8 +172,10 @@ export const setCurrentBranchId = ({ commit }, currentBranchId) => {
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => { export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile }); commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
if (file.parentPath) { const parent = file.parentPath && state.entries[file.parentPath];
dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
if (parent) {
dispatch('updateTempFlagForEntry', { file: parent, tempFile });
} }
}; };
...@@ -199,51 +202,71 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { ...@@ -199,51 +202,71 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
export const deleteEntry = ({ commit, dispatch, state }, path) => { export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path]; const entry = state.entries[path];
const { prevPath, prevName, prevParentPath } = entry;
const isTree = entry.type === 'tree';
if (prevPath) {
dispatch('renameEntry', {
path,
name: prevName,
parentPath: prevParentPath,
});
dispatch('deleteEntry', prevPath);
return;
}
if (state.unusedSeal) dispatch('burstUnusedSeal'); if (state.unusedSeal) dispatch('burstUnusedSeal');
if (entry.opened) dispatch('closeFile', entry); if (entry.opened) dispatch('closeFile', entry);
if (entry.type === 'tree') { if (isTree) {
entry.tree.forEach(f => dispatch('deleteEntry', f.path)); entry.tree.forEach(f => dispatch('deleteEntry', f.path));
} }
commit(types.DELETE_ENTRY, path); commit(types.DELETE_ENTRY, path);
dispatch('stageChange', path);
// Only stage if we're not a directory or a new file
if (!isTree && !entry.tempFile) {
dispatch('stageChange', path);
}
dispatch('triggerFilesChange'); dispatch('triggerFilesChange');
}; };
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ( export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => {
{ dispatch, commit, state }, const entry = state.entries[path];
{ path, name, entryPath = null, parentPath }, const newPath = parentPath ? `${parentPath}/${name}` : name;
) => {
const entry = state.entries[entryPath || path];
commit(types.RENAME_ENTRY, { path, name, entryPath, parentPath }); commit(types.RENAME_ENTRY, { path, name, parentPath });
if (entry.type === 'tree') { if (entry.type === 'tree') {
const slashedParentPath = parentPath ? `${parentPath}/` : ''; state.entries[newPath].tree.forEach(f => {
const targetEntry = entryPath ? entryPath.split('/').pop() : name;
const newParentPath = `${slashedParentPath}${targetEntry}`;
state.entries[entryPath || path].tree.forEach(f => {
dispatch('renameEntry', { dispatch('renameEntry', {
path, path: f.path,
name, name: f.name,
entryPath: f.path, parentPath: newPath,
parentPath: newParentPath,
}); });
}); });
} else { } else {
const newPath = parentPath ? `${parentPath}/${name}` : name;
const newEntry = state.entries[newPath]; const newEntry = state.entries[newPath];
commit(types.TOGGLE_FILE_CHANGED, { file: newEntry, changed: true }); const isRevert = newPath === entry.prevPath;
const isReset = isRevert && !newEntry.changed && !newEntry.tempFile;
const isInChanges = state.changedFiles
.concat(state.stagedFiles)
.some(({ key }) => key === newEntry.key);
if (isReset) {
commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
} else if (!isInChanges) {
commit(types.ADD_FILE_TO_CHANGED, newPath);
}
if (!newEntry.tempFile) {
eventHub.$emit(`editor.update.model.dispose.${entry.key}`);
}
if (entry.opened) { if (newEntry.opened) {
router.push(`/project${newEntry.url}`); router.push(`/project${newEntry.url}`);
commit(types.TOGGLE_FILE_OPEN, entry.path);
} }
} }
......
...@@ -5,7 +5,7 @@ import eventHub from '../../eventhub'; ...@@ -5,7 +5,7 @@ import eventHub from '../../eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle, replaceFileUrl } from '../utils';
import { viewerTypes, stageKeys } from '../../constants'; import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => { export const closeFile = ({ commit, state, dispatch }, file) => {
...@@ -67,7 +67,7 @@ export const getFileData = ( ...@@ -67,7 +67,7 @@ export const getFileData = (
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url; const url = file.prevPath ? replaceFileUrl(file.url, file.path, file.prevPath) : file.url;
return service return service
.getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/'))) .getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/')))
...@@ -186,11 +186,6 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = ...@@ -186,11 +186,6 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
dispatch('restoreTree', file.parentPath); dispatch('restoreTree', file.parentPath);
} }
if (file.movedPath) {
commit(types.DISCARD_FILE_CHANGES, file.movedPath);
commit(types.REMOVE_FILE_FROM_CHANGED, file.movedPath);
}
commit(types.DISCARD_FILE_CHANGES, path); commit(types.DISCARD_FILE_CHANGES, path);
commit(types.REMOVE_FILE_FROM_CHANGED, path); commit(types.REMOVE_FILE_FROM_CHANGED, path);
......
...@@ -92,13 +92,27 @@ export const showEmptyState = ({ commit, state }, { projectId, branchId }) => { ...@@ -92,13 +92,27 @@ export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
}); });
}; };
export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => { export const loadFile = ({ dispatch, state }, { basePath }) => {
dispatch('setCurrentBranchId', branchId); if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (getters.emptyRepo) { if (treeEntry) {
return dispatch('showEmptyState', { projectId, branchId }); dispatch('handleTreeEntryAction', treeEntry);
} else {
dispatch('createTempEntry', {
name: path,
type: 'blob',
});
}
} }
return dispatch('getBranchData', { };
export const loadBranch = ({ dispatch }, { projectId, branchId }) =>
dispatch('getBranchData', {
projectId, projectId,
branchId, branchId,
}) })
...@@ -107,42 +121,38 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, ...@@ -107,42 +121,38 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId,
projectId, projectId,
branchId, branchId,
}); });
dispatch('getFiles', { return dispatch('getFiles', {
projectId, projectId,
branchId, branchId,
}) });
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
} else {
dispatch('createTempEntry', {
name: path,
type: 'blob',
});
}
}
})
.catch(
() =>
new Error(
sprintf(
__('An error occurred whilst getting files for - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
},
false,
),
),
);
}) })
.catch(() => { .catch(() => {
dispatch('showBranchNotFoundError', branchId); dispatch('showBranchNotFoundError', branchId);
return Promise.reject();
}); });
export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
const currentProject = state.projects[projectId];
if (getters.emptyRepo) {
return dispatch('showEmptyState', { projectId, branchId });
}
if (!currentProject || !currentProject.branches[branchId]) {
dispatch('setCurrentBranchId', branchId);
return dispatch('loadBranch', { projectId, branchId })
.then(() => dispatch('loadFile', { basePath }))
.catch(
() =>
new Error(
sprintf(
__('An error occurred whilst getting files for - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
},
false,
),
),
);
}
return Promise.resolve(dispatch('loadFile', { basePath }));
}; };
...@@ -154,8 +154,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -154,8 +154,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
.then(() => { .then(() => {
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
commit(rootTypes.CLEAR_REPLACED_FILES, null, { root: true });
setTimeout(() => { setTimeout(() => {
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 5000);
......
...@@ -59,8 +59,7 @@ export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; ...@@ -59,8 +59,7 @@ export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES'; export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
export const STAGE_CHANGE = 'STAGE_CHANGE'; export const STAGE_CHANGE = 'STAGE_CHANGE';
export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE'; export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
export const REMOVE_FILE_FROM_STAGED_AND_CHANGED = 'REMOVE_FILE_FROM_STAGED_AND_CHANGED';
export const CLEAR_REPLACED_FILES = 'CLEAR_REPLACED_FILES';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
...@@ -79,5 +78,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; ...@@ -79,5 +78,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY'; export const DELETE_ENTRY = 'DELETE_ENTRY';
export const RENAME_ENTRY = 'RENAME_ENTRY'; export const RENAME_ENTRY = 'RENAME_ENTRY';
export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
export const RESTORE_TREE = 'RESTORE_TREE'; export const RESTORE_TREE = 'RESTORE_TREE';
...@@ -5,7 +5,14 @@ import mergeRequestMutation from './mutations/merge_request'; ...@@ -5,7 +5,14 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file'; import fileMutations from './mutations/file';
import treeMutations from './mutations/tree'; import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch'; import branchMutations from './mutations/branch';
import { sortTree } from './utils'; import {
sortTree,
replaceFileUrl,
swapInParentTreeWithSorting,
updateFileCollections,
removeFromParentTree,
pathsAreEqual,
} from './utils';
export default { export default {
[types.SET_INITIAL_DATA](state, data) { [types.SET_INITIAL_DATA](state, data) {
...@@ -56,11 +63,6 @@ export default { ...@@ -56,11 +63,6 @@ export default {
stagedFiles: [], stagedFiles: [],
}); });
}, },
[types.CLEAR_REPLACED_FILES](state) {
Object.assign(state, {
replacedFiles: [],
});
},
[types.SET_ENTRIES](state, entries) { [types.SET_ENTRIES](state, entries) {
Object.assign(state, { Object.assign(state, {
entries, entries,
...@@ -157,9 +159,14 @@ export default { ...@@ -157,9 +159,14 @@ export default {
changed: Boolean(changedFile), changed: Boolean(changedFile),
staged: false, staged: false,
replaces: false, replaces: false,
prevPath: '',
moved: false,
lastCommitSha: lastCommit.commit.id, lastCommitSha: lastCommit.commit.id,
prevId: undefined,
prevPath: undefined,
prevName: undefined,
prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
}); });
if (prevPath) { if (prevPath) {
...@@ -209,7 +216,9 @@ export default { ...@@ -209,7 +216,9 @@ export default {
entry.deleted = true; entry.deleted = true;
parent.tree = parent.tree.filter(f => f.path !== entry.path); if (parent) {
parent.tree = parent.tree.filter(f => f.path !== entry.path);
}
if (entry.type === 'blob') { if (entry.type === 'blob') {
if (tempFile) { if (tempFile) {
...@@ -219,51 +228,61 @@ export default { ...@@ -219,51 +228,61 @@ export default {
} }
} }
}, },
[types.RENAME_ENTRY](state, { path, name, entryPath = null, parentPath }) { [types.RENAME_ENTRY](state, { path, name, parentPath }) {
const oldEntry = state.entries[entryPath || path]; const oldEntry = state.entries[path];
const slashedParentPath = parentPath ? `${parentPath}/` : ''; const newPath = parentPath ? `${parentPath}/${name}` : name;
const newPath = entryPath const isRevert = newPath === oldEntry.prevPath;
? `${slashedParentPath}${oldEntry.name}`
: `${slashedParentPath}${name}`;
Vue.set(state.entries, newPath, { const newUrl = replaceFileUrl(oldEntry.url, oldEntry.path, newPath);
const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath);
const baseProps = {
...oldEntry, ...oldEntry,
name,
id: newPath, id: newPath,
key: `${newPath}-${oldEntry.type}-${oldEntry.path}`,
path: newPath, path: newPath,
name: entryPath ? oldEntry.name : name, url: newUrl,
tempFile: true, key: newKey,
prevPath: oldEntry.tempFile ? null : oldEntry.path, parentPath: parentPath || '',
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath), };
tree: [],
raw: '',
opened: false,
parentPath,
});
oldEntry.moved = true; const prevProps =
oldEntry.movedPath = newPath; oldEntry.tempFile || isRevert
? {
prevId: undefined,
prevPath: undefined,
prevName: undefined,
prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
}
: {
prevId: oldEntry.prevId || oldEntry.id,
prevPath: oldEntry.prevPath || oldEntry.path,
prevName: oldEntry.prevName || oldEntry.name,
prevUrl: oldEntry.prevUrl || oldEntry.url,
prevKey: oldEntry.prevKey || oldEntry.key,
prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath,
};
const parent = parentPath Vue.set(state.entries, newPath, {
? state.entries[parentPath] ...baseProps,
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; ...prevProps,
const newEntry = state.entries[newPath]; });
parent.tree = sortTree(parent.tree.concat(newEntry));
if (newEntry.type === 'blob') { if (pathsAreEqual(oldEntry.parentPath, parentPath)) {
state.changedFiles = state.changedFiles.concat(newEntry); swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath);
} else {
removeFromParentTree(state, oldEntry.key, oldEntry.parentPath);
swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath);
} }
if (oldEntry.tempFile) { if (oldEntry.type === 'blob') {
const filterMethod = f => f.path !== oldEntry.path; updateFileCollections(state, oldEntry.key, newPath);
state.openFiles = state.openFiles.filter(filterMethod);
state.changedFiles = state.changedFiles.filter(filterMethod);
parent.tree = parent.tree.filter(filterMethod);
Vue.delete(state.entries, oldEntry.path);
} }
Vue.delete(state.entries, oldEntry.path);
}, },
...projectMutations, ...projectMutations,
......
...@@ -138,8 +138,6 @@ export default { ...@@ -138,8 +138,6 @@ export default {
content: stagedFile ? stagedFile.content : state.entries[path].raw, content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false, changed: false,
deleted: false, deleted: false,
moved: false,
movedPath: '',
}); });
if (deleted) { if (deleted) {
...@@ -179,11 +177,6 @@ export default { ...@@ -179,11 +177,6 @@ export default {
}); });
if (stagedFile) { if (stagedFile) {
Object.assign(state, {
replacedFiles: state.replacedFiles.concat({
...stagedFile,
}),
});
Object.assign(stagedFile, { Object.assign(stagedFile, {
...state.entries[path], ...state.entries[path],
}); });
...@@ -252,4 +245,15 @@ export default { ...@@ -252,4 +245,15 @@ export default {
openFiles: state.openFiles.filter(f => f.key !== file.key), openFiles: state.openFiles.filter(f => f.key !== file.key),
}); });
}, },
[types.REMOVE_FILE_FROM_STAGED_AND_CHANGED](state, file) {
Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.key !== file.key),
stagedFiles: state.stagedFiles.filter(f => f.key !== file.key),
});
Object.assign(state.entries[file.path], {
changed: false,
staged: false,
});
},
}; };
...@@ -6,7 +6,6 @@ export default () => ({ ...@@ -6,7 +6,6 @@ export default () => ({
currentMergeRequestId: '', currentMergeRequestId: '',
changedFiles: [], changedFiles: [],
stagedFiles: [], stagedFiles: [],
replacedFiles: [],
endpoints: {}, endpoints: {},
lastCommitMsg: '', lastCommitMsg: '',
lastCommitPath: '', lastCommitPath: '',
......
...@@ -50,9 +50,7 @@ export const dataStructure = () => ({ ...@@ -50,9 +50,7 @@ export const dataStructure = () => ({
lastOpenedAt: 0, lastOpenedAt: 0,
mrChange: null, mrChange: null,
deleted: false, deleted: false,
prevPath: '', prevPath: undefined,
movedPath: '',
moved: false,
}); });
export const decorateData = entity => { export const decorateData = entity => {
...@@ -129,7 +127,7 @@ export const commitActionForFile = file => { ...@@ -129,7 +127,7 @@ export const commitActionForFile = file => {
export const getCommitFiles = stagedFiles => export const getCommitFiles = stagedFiles =>
stagedFiles.reduce((acc, file) => { stagedFiles.reduce((acc, file) => {
if (file.moved || file.type === 'tree') return acc; if (file.type === 'tree') return acc;
return acc.concat({ return acc.concat({
...file, ...file,
...@@ -148,9 +146,9 @@ export const createCommitPayload = ({ ...@@ -148,9 +146,9 @@ export const createCommitPayload = ({
commit_message: state.commitMessage || getters.preBuiltCommitMessage, commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: getCommitFiles(rootState.stagedFiles).map(f => ({ actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: commitActionForFile(f), action: commitActionForFile(f),
file_path: f.moved ? f.movedPath : f.path, file_path: f.path,
previous_path: f.prevPath === '' ? undefined : f.prevPath, previous_path: f.prevPath || undefined,
content: f.prevPath ? null : f.content || undefined, content: f.prevPath && !f.changed ? null : f.content || undefined,
encoding: f.base64 ? 'base64' : 'text', encoding: f.base64 ? 'base64' : 'text',
last_commit_id: last_commit_id:
newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha, newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha,
...@@ -213,3 +211,61 @@ export const mergeTrees = (fromTree, toTree) => { ...@@ -213,3 +211,61 @@ export const mergeTrees = (fromTree, toTree) => {
return toTree; return toTree;
}; };
export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
export const replaceFileUrl = (url, oldPath, newPath) => {
// Add `/-/` so that we don't accidentally replace project path
const result = url.replace(`/-/${escapeFileUrl(oldPath)}`, `/-/${escapeFileUrl(newPath)}`);
return result;
};
export const swapInStateArray = (state, arr, key, entryPath) =>
Object.assign(state, {
[arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)),
});
export const getEntryOrRoot = (state, path) =>
path ? state.entries[path] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
export const swapInParentTreeWithSorting = (state, oldKey, newPath, parentPath) => {
if (!newPath) {
return;
}
const parent = getEntryOrRoot(state, parentPath);
if (parent) {
const tree = parent.tree
// filter out old entry && new entry
.filter(({ key, path }) => key !== oldKey && path !== newPath)
// concat new entry
.concat(state.entries[newPath]);
parent.tree = sortTree(tree);
}
};
export const removeFromParentTree = (state, oldKey, parentPath) => {
const parent = getEntryOrRoot(state, parentPath);
if (parent) {
parent.tree = sortTree(parent.tree.filter(({ key }) => key !== oldKey));
}
};
export const updateFileCollections = (state, key, entryPath) => {
['openFiles', 'changedFiles', 'stagedFiles'].forEach(fileCollection => {
swapInStateArray(state, fileCollection, key, entryPath);
});
};
export const cleanTrailingSlash = path => path.replace(/\/$/, '');
export const pathsAreEqual = (a, b) => {
const cleanA = a ? cleanTrailingSlash(a) : '';
const cleanB = b ? cleanTrailingSlash(b) : '';
return cleanA === cleanB;
};
...@@ -70,7 +70,13 @@ export default { ...@@ -70,7 +70,13 @@ export default {
return undefined; return undefined;
}, },
showIcon() { showIcon() {
return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted; return (
this.file.changed ||
this.file.tempFile ||
this.file.staged ||
this.file.deleted ||
this.file.prevPath
);
}, },
}, },
}; };
......
...@@ -131,7 +131,7 @@ export default { ...@@ -131,7 +131,7 @@ export default {
</script> </script>
<template> <template>
<div v-if="!file.moved"> <div>
<file-header v-if="file.isHeader" :path="file.path" /> <file-header v-if="file.isHeader" :path="file.path" />
<div <div
v-else v-else
......
---
title: Fixed renaming changed files
merge_request: 16539
author:
type: fixed
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateFiles, splitParent, escapeFileUrl } from '~/ide/lib/files'; import { decorateFiles, splitParent } from '~/ide/lib/files';
import { decorateData } from '~/ide/stores/utils'; import { decorateData, escapeFileUrl } from '~/ide/stores/utils';
const TEST_BRANCH_ID = 'lorem-ipsum'; const TEST_BRANCH_ID = 'lorem-ipsum';
const TEST_PROJECT_ID = 10; const TEST_PROJECT_ID = 10;
......
...@@ -2,12 +2,14 @@ import Vue from 'vue'; ...@@ -2,12 +2,14 @@ import Vue from 'vue';
import store from '~/ide/stores'; import store from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue'; import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
import { trimText } from 'spec/helpers/text_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers'; import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => { describe('Multi-file editor commit sidebar list item', () => {
let vm; let vm;
let f; let f;
let findPathEl;
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(listItem); const Component = Vue.extend(listItem);
...@@ -21,6 +23,8 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -21,6 +23,8 @@ describe('Multi-file editor commit sidebar list item', () => {
actionComponent: 'stage-button', actionComponent: 'stage-button',
activeFileKey: `staged-${f.key}`, activeFileKey: `staged-${f.key}`,
}).$mount(); }).$mount();
findPathEl = vm.$el.querySelector('.multi-file-commit-list-path');
}); });
afterEach(() => { afterEach(() => {
...@@ -29,15 +33,39 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -29,15 +33,39 @@ describe('Multi-file editor commit sidebar list item', () => {
resetStore(store); resetStore(store);
}); });
const findPathText = () => trimText(findPathEl.textContent);
it('renders file path', () => { it('renders file path', () => {
expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path); expect(findPathText()).toContain(f.path);
});
it('correctly renders renamed entries', done => {
Vue.set(vm.file, 'prevName', 'Old name');
vm.$nextTick()
.then(() => {
expect(findPathText()).toEqual(`Old name → ${f.name}`);
})
.then(done)
.catch(done.fail);
});
it('correctly renders entry, the name of which did not change after rename (as within a folder)', done => {
Vue.set(vm.file, 'prevName', f.name);
vm.$nextTick()
.then(() => {
expect(findPathText()).toEqual(f.name);
})
.then(done)
.catch(done.fail);
}); });
it('opens a closed file in the editor when clicking the file path', done => { it('opens a closed file in the editor when clicking the file path', done => {
spyOn(vm, 'openPendingTab').and.callThrough(); spyOn(vm, 'openPendingTab').and.callThrough();
spyOn(router, 'push'); spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click(); findPathEl.click();
setTimeout(() => { setTimeout(() => {
expect(vm.openPendingTab).toHaveBeenCalled(); expect(vm.openPendingTab).toHaveBeenCalled();
...@@ -52,7 +80,7 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -52,7 +80,7 @@ describe('Multi-file editor commit sidebar list item', () => {
spyOn(vm, 'updateViewer').and.callThrough(); spyOn(vm, 'updateViewer').and.callThrough();
spyOn(router, 'push'); spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click(); findPathEl.click();
setTimeout(() => { setTimeout(() => {
expect(vm.updateViewer).toHaveBeenCalledWith('diff'); expect(vm.updateViewer).toHaveBeenCalledWith('diff');
......
...@@ -139,6 +139,27 @@ describe('IDE extra file row component', () => { ...@@ -139,6 +139,27 @@ describe('IDE extra file row component', () => {
done(); done();
}); });
}); });
it('shows when file is renamed', done => {
vm.file.prevPath = 'original-file';
vm.$nextTick(() => {
expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
done();
});
});
it('hides when file is renamed', done => {
vm.file.prevPath = 'original-file';
vm.file.type = 'tree';
vm.$nextTick(() => {
expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
done();
});
});
}); });
describe('merge request icon', () => { describe('merge request icon', () => {
......
...@@ -58,20 +58,6 @@ describe('IDE tree list', () => { ...@@ -58,20 +58,6 @@ describe('IDE tree list', () => {
it('renders list of files', () => { it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName'); expect(vm.$el.textContent).toContain('fileName');
}); });
it('does not render moved entries', done => {
const tree = [file('moved entry'), file('normal entry')];
tree[0].moved = true;
store.state.trees['abcproject/master'].tree = tree;
const container = vm.$el.querySelector('.ide-tree-body');
vm.$nextTick(() => {
expect(container.children.length).toBe(1);
expect(vm.$el.textContent).not.toContain('moved entry');
expect(vm.$el.textContent).toContain('normal entry');
done();
});
});
}); });
describe('empty-branch state', () => { describe('empty-branch state', () => {
......
...@@ -410,10 +410,23 @@ describe('RepoEditor', () => { ...@@ -410,10 +410,23 @@ describe('RepoEditor', () => {
describe('initEditor', () => { describe('initEditor', () => {
beforeEach(() => { beforeEach(() => {
vm.file.tempFile = false;
spyOn(vm.editor, 'createInstance'); spyOn(vm.editor, 'createInstance');
spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
}); });
it('does not fetch file information for temp entries', done => {
vm.file.tempFile = true;
vm.initEditor();
vm.$nextTick()
.then(() => {
expect(vm.getFileData).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('is being initialised for files without content even if shouldHideEditor is `true`', done => { it('is being initialised for files without content even if shouldHideEditor is `true`', done => {
vm.file.content = ''; vm.file.content = '';
vm.file.raw = ''; vm.file.raw = '';
...@@ -429,16 +442,13 @@ describe('RepoEditor', () => { ...@@ -429,16 +442,13 @@ describe('RepoEditor', () => {
}); });
it('does not initialize editor for files already with content', done => { it('does not initialize editor for files already with content', done => {
expect(vm.getFileData.calls.count()).toEqual(1);
expect(vm.getRawFileData.calls.count()).toEqual(1);
vm.file.content = 'foo'; vm.file.content = 'foo';
vm.initEditor(); vm.initEditor();
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(vm.getFileData.calls.count()).toEqual(1); expect(vm.getFileData).not.toHaveBeenCalled();
expect(vm.getRawFileData.calls.count()).toEqual(1); expect(vm.getRawFileData).not.toHaveBeenCalled();
expect(vm.editor.createInstance).not.toHaveBeenCalled(); expect(vm.editor.createInstance).not.toHaveBeenCalled();
}) })
.then(done) .then(done)
...@@ -446,23 +456,56 @@ describe('RepoEditor', () => { ...@@ -446,23 +456,56 @@ describe('RepoEditor', () => {
}); });
}); });
it('calls removePendingTab when old file is pending', done => { describe('updates on file changes', () => {
spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); beforeEach(() => {
spyOn(vm, 'removePendingTab'); spyOn(vm, 'initEditor');
});
vm.file.pending = true; it('calls removePendingTab when old file is pending', done => {
spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
spyOn(vm, 'removePendingTab');
vm.$nextTick() vm.file.pending = true;
.then(() => {
vm.file = file('testing'); vm.$nextTick()
vm.file.content = 'foo'; // need to prevent full cycle of initEditor .then(() => {
vm.file = file('testing');
vm.file.content = 'foo'; // need to prevent full cycle of initEditor
return vm.$nextTick(); return vm.$nextTick();
}) })
.then(() => { .then(() => {
expect(vm.removePendingTab).toHaveBeenCalled(); expect(vm.removePendingTab).toHaveBeenCalled();
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
});
it('does not call initEditor if the file did not change', done => {
Vue.set(vm, 'file', vm.file);
vm.$nextTick()
.then(() => {
expect(vm.initEditor).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('calls initEditor when file key is changed', done => {
expect(vm.initEditor).not.toHaveBeenCalled();
Vue.set(vm, 'file', {
...vm.file,
key: 'new',
});
vm.$nextTick()
.then(() => {
expect(vm.initEditor).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
}); });
}); });
...@@ -6,8 +6,10 @@ import { ...@@ -6,8 +6,10 @@ import {
createNewBranchFromDefault, createNewBranchFromDefault,
showEmptyState, showEmptyState,
openBranch, openBranch,
loadFile,
loadBranch,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import service from '~/ide/services'; import service from '~/ide/services';
import api from '~/api'; import api from '~/api';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
...@@ -16,8 +18,10 @@ import testAction from '../../../helpers/vuex_action_helper'; ...@@ -16,8 +18,10 @@ import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => { describe('IDE store project actions', () => {
let mock; let mock;
let store;
beforeEach(() => { beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
store.state.projects['abc/def'] = { store.state.projects['abc/def'] = {
...@@ -231,28 +235,139 @@ describe('IDE store project actions', () => { ...@@ -231,28 +235,139 @@ describe('IDE store project actions', () => {
}); });
}); });
describe('loadFile', () => {
beforeEach(() => {
Object.assign(store.state, {
entries: {
foo: { pending: false },
'foo/bar-pending': { pending: true },
'foo/bar': { pending: false },
},
});
spyOn(store, 'dispatch');
});
it('does nothing, if basePath is not given', () => {
loadFile(store, { basePath: undefined });
expect(store.dispatch).not.toHaveBeenCalled();
});
it('handles tree entry action, if basePath is given and the entry is not pending', () => {
loadFile(store, { basePath: 'foo/bar/' });
expect(store.dispatch).toHaveBeenCalledWith(
'handleTreeEntryAction',
store.state.entries['foo/bar'],
);
});
it('does not handle tree entry action, if entry is pending', () => {
loadFile(store, { basePath: 'foo/bar-pending/' });
expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything());
});
it('creates a new temp file supplied via URL if the file does not exist yet', () => {
loadFile(store, { basePath: 'not-existent.md' });
expect(store.dispatch.calls.count()).toBe(1);
expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything());
expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
name: 'not-existent.md',
type: 'blob',
});
});
});
describe('loadBranch', () => {
const projectId = 'abc/def';
const branchId = '123-lorem';
it('fetches branch data', done => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
loadBranch(store, { projectId, branchId })
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['getBranchData', { projectId, branchId }],
['getMergeRequestsForBranch', { projectId, branchId }],
['getFiles', { projectId, branchId }],
]);
})
.then(done)
.catch(done.fail);
});
it('shows an error if branch can not be fetched', done => {
spyOn(store, 'dispatch').and.returnValue(Promise.reject());
loadBranch(store, { projectId, branchId })
.then(done.fail)
.catch(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['getBranchData', { projectId, branchId }],
['showBranchNotFoundError', branchId],
]);
done();
});
});
});
describe('openBranch', () => { describe('openBranch', () => {
const projectId = 'abc/def';
const branchId = '123-lorem';
const branch = { const branch = {
projectId: 'abc/def', projectId,
branchId: '123-lorem', branchId,
}; };
beforeEach(() => { beforeEach(() => {
store.state.entries = { Object.assign(store.state, {
foo: { pending: false }, entries: {
'foo/bar-pending': { pending: true }, foo: { pending: false },
'foo/bar': { pending: false }, 'foo/bar-pending': { pending: true },
}; 'foo/bar': { pending: false },
},
});
});
it('loads file right away if the branch has already been fetched', done => {
spyOn(store, 'dispatch');
Object.assign(store.state, {
projects: {
[projectId]: {
branches: {
[branchId]: { foo: 'bar' },
},
},
},
});
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([['loadFile', { basePath: undefined }]]);
})
.then(done)
.catch(done.fail);
}); });
describe('empty repo', () => { describe('empty repo', () => {
beforeEach(() => { beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
store.state.currentProjectId = 'abc/def'; Object.assign(store.state, {
store.state.projects['abc/def'] = { currentProjectId: 'abc/def',
empty_repo: true, projects: {
}; 'abc/def': {
empty_repo: true,
},
},
});
}); });
afterEach(() => { afterEach(() => {
...@@ -262,10 +377,7 @@ describe('IDE store project actions', () => { ...@@ -262,10 +377,7 @@ describe('IDE store project actions', () => {
it('dispatches showEmptyState action right away', done => { it('dispatches showEmptyState action right away', done => {
openBranch(store, branch) openBranch(store, branch)
.then(() => { .then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([ expect(store.dispatch.calls.allArgs()).toEqual([['showEmptyState', branch]]);
['setCurrentBranchId', branch.branchId],
['showEmptyState', branch],
]);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
...@@ -281,56 +393,14 @@ describe('IDE store project actions', () => { ...@@ -281,56 +393,14 @@ describe('IDE store project actions', () => {
openBranch(store, branch) openBranch(store, branch)
.then(() => { .then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([ expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId], ['setCurrentBranchId', branchId],
['getBranchData', branch], ['loadBranch', { projectId, branchId }],
['getMergeRequestsForBranch', branch], ['loadFile', { basePath: undefined }],
['getFiles', branch],
]); ]);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('handles tree entry action, if basePath is given', done => {
openBranch(store, { ...branch, basePath: 'foo/bar/' })
.then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'handleTreeEntryAction',
store.state.entries['foo/bar'],
);
})
.then(done)
.catch(done.fail);
});
it('does not handle tree entry action, if entry is pending', done => {
openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
.then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith(
'handleTreeEntryAction',
jasmine.anything(),
);
})
.then(done)
.catch(done.fail);
});
it('creates a new file supplied via URL if the file does not exist yet', done => {
openBranch(store, { ...branch, basePath: 'not-existent.md' })
.then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith(
'handleTreeEntryAction',
jasmine.anything(),
);
expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
name: 'not-existent.md',
type: 'blob',
});
})
.then(done)
.catch(done.fail);
});
}); });
describe('non-existent branch', () => { describe('non-existent branch', () => {
...@@ -342,9 +412,8 @@ describe('IDE store project actions', () => { ...@@ -342,9 +412,8 @@ describe('IDE store project actions', () => {
openBranch(store, branch) openBranch(store, branch)
.then(() => { .then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([ expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId], ['setCurrentBranchId', branchId],
['getBranchData', branch], ['loadBranch', { projectId, branchId }],
['showBranchNotFoundError', branch.branchId],
]); ]);
}) })
.then(done) .then(done)
......
...@@ -13,12 +13,15 @@ import actions, { ...@@ -13,12 +13,15 @@ import actions, {
createTempEntry, createTempEntry,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers'; import { resetStore, file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import eventHub from '~/ide/eventhub';
const store = createStore();
describe('Multi-file store actions', () => { describe('Multi-file store actions', () => {
beforeEach(() => { beforeEach(() => {
...@@ -451,6 +454,24 @@ describe('Multi-file store actions', () => { ...@@ -451,6 +454,24 @@ describe('Multi-file store actions', () => {
done, done,
); );
}); });
it('does not dispatch for parent, if parent does not exist', done => {
const f = {
...file(),
path: 'test',
parentPath: 'testing',
};
store.state.entries[f.path] = f;
testAction(
updateTempFlagForEntry,
{ file: f, tempFile: false },
store.state,
[{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
[],
done,
);
});
}); });
describe('setCurrentBranchId', () => { describe('setCurrentBranchId', () => {
...@@ -540,82 +561,298 @@ describe('Multi-file store actions', () => { ...@@ -540,82 +561,298 @@ describe('Multi-file store actions', () => {
done, done,
); );
}); });
});
describe('renameEntry', () => { it('if renamed, reverts the rename before deleting', () => {
it('renames entry', done => { const testEntry = {
store.state.entries.test = { path: 'test',
tree: [], name: 'test',
prevPath: 'lorem/ipsum',
prevName: 'ipsum',
prevParentPath: 'lorem',
}; };
store.state.entries = { test: testEntry };
testAction( testAction(
renameEntry, deleteEntry,
{ path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' }, testEntry.path,
store.state, store.state,
[],
[ [
{ {
type: types.RENAME_ENTRY, type: 'renameEntry',
payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
},
{
type: types.TOGGLE_FILE_CHANGED,
payload: { payload: {
file: store.state.entries['parent-path/new-name'], path: testEntry.path,
changed: true, name: testEntry.prevName,
parentPath: testEntry.prevParentPath,
}, },
}, },
{
type: 'deleteEntry',
payload: testEntry.prevPath,
},
], ],
[{ type: 'triggerFilesChange' }],
done,
); );
}); });
});
it('renames all entries in tree', done => { describe('renameEntry', () => {
store.state.entries.test = { describe('purging of file model cache', () => {
type: 'tree', beforeEach(() => {
tree: [ spyOn(eventHub, '$emit');
{ });
path: 'tree-1',
}, it('does not purge model cache for temporary entries that got renamed', done => {
{ Object.assign(store.state.entries, {
path: 'tree-2', test: {
...file('test'),
key: 'foo-key',
type: 'blob',
tempFile: true,
}, },
], });
};
testAction( store
renameEntry, .dispatch('renameEntry', {
{ path: 'test', name: 'new-name', parentPath: 'parent-path' }, path: 'test',
store.state, name: 'new',
[ })
{ .then(() => {
type: types.RENAME_ENTRY, expect(eventHub.$emit.calls.allArgs()).not.toContain(
payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' }, 'editor.update.model.dispose.foo-bar',
);
})
.then(done)
.catch(done.fail);
});
it('purges model cache for renamed entry', done => {
Object.assign(store.state.entries, {
test: {
...file('test'),
key: 'foo-key',
type: 'blob',
tempFile: false,
}, },
], });
[
{ store
type: 'renameEntry', .dispatch('renameEntry', {
payload: { path: 'test',
path: 'test', name: 'new',
name: 'new-name', })
entryPath: 'tree-1', .then(() => {
parentPath: 'parent-path/new-name', expect(eventHub.$emit).toHaveBeenCalled();
expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.foo-key`);
})
.then(done)
.catch(done.fail);
});
});
describe('single entry', () => {
let origEntry;
let renamedEntry;
beforeEach(() => {
// Need to insert both because `testAction` doesn't actually call the mutation
origEntry = file('orig', 'orig', 'blob');
renamedEntry = {
...file('renamed', 'renamed', 'blob'),
prevKey: origEntry.key,
prevName: origEntry.name,
prevPath: origEntry.path,
};
Object.assign(store.state.entries, {
orig: origEntry,
renamed: renamedEntry,
});
});
afterEach(() => {
resetStore(store);
});
it('by default renames an entry and adds to changed', done => {
testAction(
renameEntry,
{ path: 'orig', name: 'renamed' },
store.state,
[
{
type: types.RENAME_ENTRY,
payload: {
path: 'orig',
name: 'renamed',
parentPath: undefined,
},
}, },
}, {
{ type: types.ADD_FILE_TO_CHANGED,
type: 'renameEntry', payload: 'renamed',
payload: { },
path: 'test', ],
name: 'new-name', [{ type: 'triggerFilesChange' }],
entryPath: 'tree-2', done,
parentPath: 'parent-path/new-name', );
});
it('if not changed, completely unstages entry if renamed to original', done => {
testAction(
renameEntry,
{ path: 'renamed', name: 'orig' },
store.state,
[
{
type: types.RENAME_ENTRY,
payload: {
path: 'renamed',
name: 'orig',
parentPath: undefined,
},
},
{
type: types.REMOVE_FILE_FROM_STAGED_AND_CHANGED,
payload: origEntry,
},
],
[{ type: 'triggerFilesChange' }],
done,
);
});
it('if already in changed, does not add to change', done => {
store.state.changedFiles.push(renamedEntry);
testAction(
renameEntry,
{ path: 'orig', name: 'renamed' },
store.state,
[jasmine.objectContaining({ type: types.RENAME_ENTRY })],
[{ type: 'triggerFilesChange' }],
done,
);
});
it('routes to the renamed file if the original file has been opened', done => {
Object.assign(store.state.entries.orig, {
opened: true,
url: '/foo-bar.md',
});
store
.dispatch('renameEntry', {
path: 'orig',
name: 'renamed',
})
.then(() => {
expect(router.push.calls.count()).toBe(1);
expect(router.push).toHaveBeenCalledWith(`/project/foo-bar.md`);
})
.then(done)
.catch(done.fail);
});
});
describe('folder', () => {
let folder;
let file1;
let file2;
beforeEach(() => {
folder = file('folder', 'folder', 'tree');
file1 = file('file-1', 'file-1', 'blob', folder);
file2 = file('file-2', 'file-2', 'blob', folder);
folder.tree = [file1, file2];
Object.assign(store.state.entries, {
[folder.path]: folder,
[file1.path]: file1,
[file2.path]: file2,
});
});
it('updates entries in a folder correctly, when folder is renamed', done => {
store
.dispatch('renameEntry', {
path: 'folder',
name: 'new-folder',
})
.then(() => {
const keys = Object.keys(store.state.entries);
expect(keys.length).toBe(3);
expect(keys.indexOf('new-folder')).toBe(0);
expect(keys.indexOf('new-folder/file-1')).toBe(1);
expect(keys.indexOf('new-folder/file-2')).toBe(2);
})
.then(done)
.catch(done.fail);
});
it('discards renaming of an entry if the root folder is renamed back to a previous name', done => {
const rootFolder = file('old-folder', 'old-folder', 'tree');
const testEntry = file('test', 'test', 'blob', rootFolder);
Object.assign(store.state, {
entries: {
'old-folder': {
...rootFolder,
tree: [testEntry],
}, },
'old-folder/test': testEntry,
}, },
{ type: 'triggerFilesChange' }, });
],
done, store
); .dispatch('renameEntry', {
path: 'old-folder',
name: 'new-folder',
})
.then(() => {
const { entries } = store.state;
expect(Object.keys(entries).length).toBe(2);
expect(entries['old-folder']).toBeUndefined();
expect(entries['old-folder/test']).toBeUndefined();
expect(entries['new-folder']).toBeDefined();
expect(entries['new-folder/test']).toEqual(
jasmine.objectContaining({
path: 'new-folder/test',
name: 'test',
prevPath: 'old-folder/test',
prevName: 'test',
}),
);
})
.then(() =>
store.dispatch('renameEntry', {
path: 'new-folder',
name: 'old-folder',
}),
)
.then(() => {
const { entries } = store.state;
expect(Object.keys(entries).length).toBe(2);
expect(entries['new-folder']).toBeUndefined();
expect(entries['new-folder/test']).toBeUndefined();
expect(entries['old-folder']).toBeDefined();
expect(entries['old-folder/test']).toEqual(
jasmine.objectContaining({
path: 'old-folder/test',
name: 'test',
prevPath: undefined,
prevName: undefined,
}),
);
})
.then(done)
.catch(done.fail);
});
}); });
}); });
......
import rootActions from '~/ide/stores/actions'; import rootActions from '~/ide/stores/actions';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import service from '~/ide/services'; import service from '~/ide/services';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
...@@ -11,6 +11,7 @@ import { resetStore, file } from 'spec/ide/helpers'; ...@@ -11,6 +11,7 @@ import { resetStore, file } from 'spec/ide/helpers';
import testAction from '../../../../helpers/vuex_action_helper'; import testAction from '../../../../helpers/vuex_action_helper';
const TEST_COMMIT_SHA = '123456789'; const TEST_COMMIT_SHA = '123456789';
const store = createStore();
describe('IDE commit module actions', () => { describe('IDE commit module actions', () => {
beforeEach(() => { beforeEach(() => {
...@@ -59,7 +60,9 @@ describe('IDE commit module actions', () => { ...@@ -59,7 +60,9 @@ describe('IDE commit module actions', () => {
}); });
it('sets shouldCreateMR to true if "Create new MR" option is visible', done => { it('sets shouldCreateMR to true if "Create new MR" option is visible', done => {
store.state.shouldHideNewMrOption = false; Object.assign(store.state, {
shouldHideNewMrOption: false,
});
testAction( testAction(
actions.updateCommitAction, actions.updateCommitAction,
...@@ -78,7 +81,9 @@ describe('IDE commit module actions', () => { ...@@ -78,7 +81,9 @@ describe('IDE commit module actions', () => {
}); });
it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => { it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => {
store.state.shouldHideNewMrOption = true; Object.assign(store.state, {
shouldHideNewMrOption: true,
});
testAction( testAction(
actions.updateCommitAction, actions.updateCommitAction,
...@@ -172,24 +177,31 @@ describe('IDE commit module actions', () => { ...@@ -172,24 +177,31 @@ describe('IDE commit module actions', () => {
content: 'file content', content: 'file content',
}); });
store.state.currentProjectId = 'abcproject'; Object.assign(store.state, {
store.state.currentBranchId = 'master'; currentProjectId: 'abcproject',
store.state.projects.abcproject = { currentBranchId: 'master',
web_url: 'web_url', projects: {
branches: { abcproject: {
master: { web_url: 'web_url',
workingReference: '', branches: {
commit: { master: {
short_id: TEST_COMMIT_SHA, workingReference: '',
commit: {
short_id: TEST_COMMIT_SHA,
},
},
}, },
}, },
}, },
}; stagedFiles: [
store.state.stagedFiles.push(f, { f,
...file('changedFile2'), {
changed: true, ...file('changedFile2'),
changed: true,
},
],
openFiles: store.state.stagedFiles,
}); });
store.state.openFiles = store.state.stagedFiles;
store.state.stagedFiles.forEach(stagedFile => { store.state.stagedFiles.forEach(stagedFile => {
store.state.entries[stagedFile.path] = stagedFile; store.state.entries[stagedFile.path] = stagedFile;
...@@ -275,40 +287,40 @@ describe('IDE commit module actions', () => { ...@@ -275,40 +287,40 @@ describe('IDE commit module actions', () => {
document.body.innerHTML += '<div class="flash-container"></div>'; document.body.innerHTML += '<div class="flash-container"></div>';
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
web_url: 'webUrl',
branches: {
master: {
workingReference: '1',
commit: {
id: TEST_COMMIT_SHA,
},
},
},
};
const f = { const f = {
...file('changed'), ...file('changed'),
type: 'blob', type: 'blob',
active: true, active: true,
lastCommitSha: TEST_COMMIT_SHA, lastCommitSha: TEST_COMMIT_SHA,
}; };
store.state.stagedFiles.push(f);
store.state.changedFiles = [
{
...f,
},
];
store.state.openFiles = store.state.changedFiles;
store.state.openFiles.forEach(localF => { Object.assign(store.state, {
store.state.entries[localF.path] = localF; stagedFiles: [f],
changedFiles: [f],
openFiles: [f],
currentProjectId: 'abcproject',
currentBranchId: 'master',
projects: {
abcproject: {
web_url: 'webUrl',
branches: {
master: {
workingReference: '1',
commit: {
id: TEST_COMMIT_SHA,
},
},
},
},
},
}); });
store.state.commit.commitAction = '2'; store.state.commit.commitAction = '2';
store.state.commit.commitMessage = 'testing 123'; store.state.commit.commitMessage = 'testing 123';
store.state.openFiles.forEach(localF => {
store.state.entries[localF.path] = localF;
});
}); });
afterEach(() => { afterEach(() => {
......
...@@ -356,16 +356,16 @@ describe('IDE store file mutations', () => { ...@@ -356,16 +356,16 @@ describe('IDE store file mutations', () => {
}); });
describe('STAGE_CHANGE', () => { describe('STAGE_CHANGE', () => {
it('adds file into stagedFiles array', () => { beforeEach(() => {
mutations.STAGE_CHANGE(localState, localFile.path); mutations.STAGE_CHANGE(localState, localFile.path);
});
it('adds file into stagedFiles array', () => {
expect(localState.stagedFiles.length).toBe(1); expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0]).toEqual(localFile); expect(localState.stagedFiles[0]).toEqual(localFile);
}); });
it('updates stagedFile if it is already staged', () => { it('updates stagedFile if it is already staged', () => {
mutations.STAGE_CHANGE(localState, localFile.path);
localFile.raw = 'testing 123'; localFile.raw = 'testing 123';
mutations.STAGE_CHANGE(localState, localFile.path); mutations.STAGE_CHANGE(localState, localFile.path);
...@@ -373,19 +373,6 @@ describe('IDE store file mutations', () => { ...@@ -373,19 +373,6 @@ describe('IDE store file mutations', () => {
expect(localState.stagedFiles.length).toBe(1); expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0].raw).toEqual('testing 123'); expect(localState.stagedFiles[0].raw).toEqual('testing 123');
}); });
it('adds already-staged file to `replacedFiles`', () => {
localFile.raw = 'already-staged';
mutations.STAGE_CHANGE(localState, localFile.path);
localFile.raw = 'testing 123';
mutations.STAGE_CHANGE(localState, localFile.path);
expect(localState.replacedFiles.length).toBe(1);
expect(localState.replacedFiles[0].raw).toEqual('already-staged');
});
}); });
describe('UNSTAGE_CHANGE', () => { describe('UNSTAGE_CHANGE', () => {
......
...@@ -79,16 +79,6 @@ describe('Multi-file store mutations', () => { ...@@ -79,16 +79,6 @@ describe('Multi-file store mutations', () => {
}); });
}); });
describe('CLEAR_REPLACED_FILES', () => {
it('clears replacedFiles array', () => {
localState.replacedFiles.push('a');
mutations.CLEAR_REPLACED_FILES(localState);
expect(localState.replacedFiles.length).toBe(0);
});
});
describe('UPDATE_VIEWER', () => { describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => { it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff'); mutations.UPDATE_VIEWER(localState, 'diff');
...@@ -311,8 +301,7 @@ describe('Multi-file store mutations', () => { ...@@ -311,8 +301,7 @@ describe('Multi-file store mutations', () => {
describe('UPDATE_FILE_AFTER_COMMIT', () => { describe('UPDATE_FILE_AFTER_COMMIT', () => {
it('updates URLs if prevPath is set', () => { it('updates URLs if prevPath is set', () => {
const f = { const f = {
...file(), ...file('test'),
path: 'test',
prevPath: 'testing-123', prevPath: 'testing-123',
rawPath: `${gl.TEST_HOST}/testing-123`, rawPath: `${gl.TEST_HOST}/testing-123`,
permalink: `${gl.TEST_HOST}/testing-123`, permalink: `${gl.TEST_HOST}/testing-123`,
...@@ -325,19 +314,26 @@ describe('Multi-file store mutations', () => { ...@@ -325,19 +314,26 @@ describe('Multi-file store mutations', () => {
mutations.UPDATE_FILE_AFTER_COMMIT(localState, { file: f, lastCommit: { commit: {} } }); mutations.UPDATE_FILE_AFTER_COMMIT(localState, { file: f, lastCommit: { commit: {} } });
expect(f.rawPath).toBe(`${gl.TEST_HOST}/test`); expect(f).toEqual(
expect(f.permalink).toBe(`${gl.TEST_HOST}/test`); jasmine.objectContaining({
expect(f.commitsPath).toBe(`${gl.TEST_HOST}/test`); rawPath: `${gl.TEST_HOST}/test`,
expect(f.blamePath).toBe(`${gl.TEST_HOST}/test`); permalink: `${gl.TEST_HOST}/test`,
expect(f.replaces).toBe(false); commitsPath: `${gl.TEST_HOST}/test`,
blamePath: `${gl.TEST_HOST}/test`,
replaces: false,
prevId: undefined,
prevPath: undefined,
prevName: undefined,
prevUrl: undefined,
prevKey: undefined,
}),
);
}); });
}); });
describe('OPEN_NEW_ENTRY_MODAL', () => { describe('OPEN_NEW_ENTRY_MODAL', () => {
it('sets entryModal', () => { it('sets entryModal', () => {
localState.entries.testPath = { localState.entries.testPath = file();
...file(),
};
mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' }); mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' });
...@@ -356,58 +352,178 @@ describe('Multi-file store mutations', () => { ...@@ -356,58 +352,178 @@ describe('Multi-file store mutations', () => {
}; };
localState.currentProjectId = 'gitlab-ce'; localState.currentProjectId = 'gitlab-ce';
localState.currentBranchId = 'master'; localState.currentBranchId = 'master';
localState.entries.oldPath = { localState.entries = {
...file(), oldPath: file('oldPath', 'oldPath', 'blob'),
type: 'blob',
name: 'oldPath',
path: 'oldPath',
url: `${gl.TEST_HOST}/oldPath`,
}; };
}); });
it('creates new renamed entry', () => { it('updates existing entry without creating a new one', () => {
mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
name: 'newPath',
parentPath: '',
});
expect(localState.entries).toEqual({
newPath: jasmine.objectContaining({
path: 'newPath',
prevPath: 'oldPath',
}),
});
});
it('correctly handles consecutive renames for the same entry', () => {
mutations.RENAME_ENTRY(localState, { mutations.RENAME_ENTRY(localState, {
path: 'oldPath', path: 'oldPath',
name: 'newPath', name: 'newPath',
parentPath: '',
});
mutations.RENAME_ENTRY(localState, {
path: 'newPath',
name: 'newestPath',
parentPath: '',
});
expect(localState.entries).toEqual({
newestPath: jasmine.objectContaining({
path: 'newestPath',
prevPath: 'oldPath',
}),
});
});
it('correctly handles the same entry within a consecutively renamed folder', () => {
const oldPath = file('root-folder/oldPath', 'root-folder/oldPath', 'blob');
localState.entries = {
'root-folder': {
...file('root-folder', 'root-folder', 'tree'),
tree: [oldPath],
},
'root-folder/oldPath': oldPath,
};
Object.assign(localState.entries['root-folder/oldPath'], {
parentPath: 'root-folder',
url: 'root-folder/oldPath-blob-root-folder/oldPath',
});
mutations.RENAME_ENTRY(localState, {
path: 'root-folder/oldPath',
name: 'renamed-folder/oldPath',
entryPath: null, entryPath: null,
parentPath: '', parentPath: '',
}); });
mutations.RENAME_ENTRY(localState, {
path: 'renamed-folder/oldPath',
name: 'simply-renamed/oldPath',
entryPath: null,
parentPath: '',
});
expect(localState.entries).toEqual({
'root-folder': jasmine.objectContaining({
path: 'root-folder',
}),
'simply-renamed/oldPath': jasmine.objectContaining({
path: 'simply-renamed/oldPath',
prevPath: 'root-folder/oldPath',
}),
});
});
it('renames entry, preserving old parameters', () => {
Object.assign(localState.entries.oldPath, {
url: `project/-/oldPath`,
});
const oldPathData = localState.entries.oldPath;
mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
name: 'newPath',
parentPath: '',
});
expect(localState.entries.newPath).toEqual({ expect(localState.entries.newPath).toEqual({
...localState.entries.oldPath, ...oldPathData,
id: 'newPath', id: 'newPath',
name: 'newPath',
key: 'newPath-blob-oldPath',
path: 'newPath', path: 'newPath',
tempFile: true, name: 'newPath',
url: `project/-/newPath`,
key: jasmine.stringMatching('newPath'),
prevId: 'oldPath',
prevName: 'oldPath',
prevPath: 'oldPath', prevPath: 'oldPath',
tree: [], prevUrl: `project/-/oldPath`,
parentPath: '', prevKey: oldPathData.key,
url: `${gl.TEST_HOST}/newPath`, prevParentPath: oldPathData.parentPath,
moved: jasmine.anything(),
movedPath: jasmine.anything(),
opened: false,
}); });
}); });
it('adds new entry to changedFiles', () => { it('does not store previous attributes on temp files', () => {
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); Object.assign(localState.entries.oldPath, {
tempFile: true,
});
mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
name: 'newPath',
entryPath: null,
parentPath: '',
});
expect(localState.changedFiles.length).toBe(1); expect(localState.entries.newPath).not.toEqual(
expect(localState.changedFiles[0].path).toBe('newPath'); jasmine.objectContaining({
}); prevId: jasmine.anything(),
prevName: jasmine.anything(),
prevPath: jasmine.anything(),
prevUrl: jasmine.anything(),
prevKey: jasmine.anything(),
prevParentPath: jasmine.anything(),
}),
);
});
it('properly handles files with spaces in name', () => {
const path = 'my fancy path';
const newPath = 'new path';
const oldEntry = {
...file(path, path, 'blob'),
url: `project/-/${encodeURI(path)}`,
};
it('sets oldEntry as moved', () => { localState.entries[path] = oldEntry;
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.entries.oldPath.moved).toBe(true); mutations.RENAME_ENTRY(localState, {
path,
name: newPath,
entryPath: null,
parentPath: '',
});
expect(localState.entries[newPath]).toEqual({
...oldEntry,
id: newPath,
path: newPath,
name: newPath,
url: `project/-/new%20path`,
key: jasmine.stringMatching(newPath),
prevId: path,
prevName: path,
prevPath: path,
prevUrl: `project/-/my%20fancy%20path`,
prevKey: oldEntry.key,
prevParentPath: oldEntry.parentPath,
});
}); });
it('adds to parents tree', () => { it('adds to parent tree', () => {
localState.entries.oldPath.parentPath = 'parentPath'; const parentEntry = {
localState.entries.parentPath = { ...file('parentPath', 'parentPath', 'tree'),
...file(), tree: [localState.entries.oldPath],
}; };
localState.entries.parentPath = parentEntry;
mutations.RENAME_ENTRY(localState, { mutations.RENAME_ENTRY(localState, {
path: 'oldPath', path: 'oldPath',
...@@ -416,7 +532,180 @@ describe('Multi-file store mutations', () => { ...@@ -416,7 +532,180 @@ describe('Multi-file store mutations', () => {
parentPath: 'parentPath', parentPath: 'parentPath',
}); });
expect(localState.entries.parentPath.tree.length).toBe(1); expect(parentEntry.tree.length).toBe(1);
expect(parentEntry.tree[0].name).toBe('newPath');
});
it('sorts tree after renaming an entry', () => {
const alpha = file('alpha', 'alpha', 'blob');
const beta = file('beta', 'beta', 'blob');
const gamma = file('gamma', 'gamma', 'blob');
localState.entries = { alpha, beta, gamma };
localState.trees['gitlab-ce/master'].tree = [alpha, beta, gamma];
mutations.RENAME_ENTRY(localState, {
path: 'alpha',
name: 'theta',
entryPath: null,
parentPath: '',
});
expect(localState.trees['gitlab-ce/master'].tree).toEqual([
jasmine.objectContaining({ name: 'beta' }),
jasmine.objectContaining({ name: 'gamma' }),
jasmine.objectContaining({
path: 'theta',
name: 'theta',
}),
]);
});
it('updates openFiles with the renamed one if the original one is open', () => {
Object.assign(localState.entries.oldPath, {
opened: true,
type: 'blob',
});
Object.assign(localState, {
openFiles: [localState.entries.oldPath],
});
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.openFiles.length).toBe(1);
expect(localState.openFiles[0].path).toBe('newPath');
});
it('does not add renamed entry to changedFiles', () => {
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.changedFiles.length).toBe(0);
});
it('updates existing changedFiles entry with the renamed one', () => {
const origFile = {
...file('oldPath', 'oldPath', 'blob'),
content: 'Foo',
};
Object.assign(localState, {
changedFiles: [origFile],
});
Object.assign(localState.entries, {
oldPath: origFile,
});
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.changedFiles).toEqual([
jasmine.objectContaining({
path: 'newPath',
content: 'Foo',
}),
]);
});
it('correctly saves original values if an entry is renamed multiple times', () => {
const original = { ...localState.entries.oldPath };
const paramsToCheck = ['prevId', 'prevPath', 'prevName', 'prevUrl'];
const expectedObj = paramsToCheck.reduce(
(o, param) => ({ ...o, [param]: original[param.replace('prev', '').toLowerCase()] }),
{},
);
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.entries.newPath).toEqual(jasmine.objectContaining(expectedObj));
mutations.RENAME_ENTRY(localState, { path: 'newPath', name: 'newer' });
expect(localState.entries.newer).toEqual(jasmine.objectContaining(expectedObj));
});
describe('renaming back to original', () => {
beforeEach(() => {
const renamedEntry = {
...file('renamed', 'renamed', 'blob'),
prevId: 'lorem/orig',
prevPath: 'lorem/orig',
prevName: 'orig',
prevUrl: 'project/-/loren/orig',
prevKey: 'lorem/orig',
prevParentPath: 'lorem',
};
localState.entries = {
renamed: renamedEntry,
};
mutations.RENAME_ENTRY(localState, { path: 'renamed', name: 'orig', parentPath: 'lorem' });
});
it('renames entry and clears prev properties', () => {
expect(localState.entries).toEqual({
'lorem/orig': jasmine.objectContaining({
id: 'lorem/orig',
path: 'lorem/orig',
name: 'orig',
prevId: undefined,
prevPath: undefined,
prevName: undefined,
prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
}),
});
});
});
describe('key updates', () => {
beforeEach(() => {
const rootFolder = file('rootFolder', 'rootFolder', 'tree');
localState.entries = {
rootFolder,
oldPath: file('oldPath', 'oldPath', 'blob'),
'oldPath.txt': file('oldPath.txt', 'oldPath.txt', 'blob'),
'rootFolder/oldPath.md': file('oldPath.md', 'oldPath.md', 'blob', rootFolder),
};
});
it('sets properly constucted key while preserving the original one', () => {
const key = 'oldPath.txt-blob-oldPath.txt';
localState.entries['oldPath.txt'].key = key;
mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath.md' });
expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md');
expect(localState.entries['newPath.md'].prevKey).toBe(key);
});
it('correctly updates key for an entry without an extension', () => {
localState.entries.oldPath.key = 'oldPath-blob-oldPath';
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath.md' });
expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md');
});
it('correctly updates key when new name does not have an extension', () => {
localState.entries['oldPath.txt'].key = 'oldPath.txt-blob-oldPath.txt';
mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath' });
expect(localState.entries.newPath.key).toBe('newPath-blob-newPath');
});
it('correctly updates key when renaming an entry in a folder', () => {
localState.entries['rootFolder/oldPath.md'].key =
'rootFolder/oldPath.md-blob-rootFolder/oldPath.md';
mutations.RENAME_ENTRY(localState, {
path: 'rootFolder/oldPath.md',
name: 'newPath.md',
entryPath: null,
parentPath: 'rootFolder',
});
expect(localState.entries['rootFolder/newPath.md'].key).toBe(
'rootFolder/newPath.md-blob-rootFolder/newPath.md',
);
});
}); });
}); });
}); });
...@@ -237,31 +237,6 @@ describe('Multi-file store utils', () => { ...@@ -237,31 +237,6 @@ describe('Multi-file store utils', () => {
}); });
describe('getCommitFiles', () => { describe('getCommitFiles', () => {
it('returns list of files excluding moved files', () => {
const files = [
{
path: 'a',
type: 'blob',
deleted: true,
},
{
path: 'c',
type: 'blob',
moved: true,
},
];
const flattendFiles = utils.getCommitFiles(files);
expect(flattendFiles).toEqual([
{
path: 'a',
type: 'blob',
deleted: true,
},
]);
});
it('filters out folders from the list', () => { it('filters out folders from the list', () => {
const files = [ const files = [
{ {
...@@ -422,4 +397,204 @@ describe('Multi-file store utils', () => { ...@@ -422,4 +397,204 @@ describe('Multi-file store utils', () => {
expect(res[1].tree[0].opened).toEqual(true); expect(res[1].tree[0].opened).toEqual(true);
}); });
}); });
describe('escapeFileUrl', () => {
it('encodes URL excluding the slashes', () => {
expect(utils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md');
expect(utils.escapeFileUrl('foo bar/file.md')).toBe('foo%20bar/file.md');
expect(utils.escapeFileUrl('foo/bar/file.md')).toBe('foo/bar/file.md');
});
});
describe('swapInStateArray', () => {
let localState;
beforeEach(() => {
localState = [];
});
it('swaps existing entry with a new one', () => {
const file1 = {
...file('old'),
key: 'foo',
};
const file2 = file('new');
const arr = [file1];
Object.assign(localState, {
dummyArray: arr,
entries: {
new: file2,
},
});
utils.swapInStateArray(localState, 'dummyArray', 'foo', 'new');
expect(localState.dummyArray.length).toBe(1);
expect(localState.dummyArray[0]).toBe(file2);
});
it('does not add an item if it does not exist yet in array', () => {
const file1 = file('file');
Object.assign(localState, {
dummyArray: [],
entries: {
file: file1,
},
});
utils.swapInStateArray(localState, 'dummyArray', 'foo', 'file');
expect(localState.dummyArray.length).toBe(0);
});
});
describe('swapInParentTreeWithSorting', () => {
let localState;
let branchInfo;
const currentProjectId = '123-foo';
const currentBranchId = 'master';
beforeEach(() => {
localState = {
currentBranchId,
currentProjectId,
trees: {
[`${currentProjectId}/${currentBranchId}`]: {
tree: [],
},
},
entries: {
oldPath: file('oldPath', 'oldPath', 'blob'),
newPath: file('newPath', 'newPath', 'blob'),
parentPath: file('parentPath', 'parentPath', 'tree'),
},
};
branchInfo = localState.trees[`${currentProjectId}/${currentBranchId}`];
});
it('does not change tree if newPath is not supplied', () => {
branchInfo.tree = [localState.entries.oldPath];
utils.swapInParentTreeWithSorting(localState, 'oldPath', undefined, undefined);
expect(branchInfo.tree).toEqual([localState.entries.oldPath]);
});
describe('oldPath to replace is not defined: simple addition to tree', () => {
it('adds to tree on the state if there is no parent for the entry', () => {
expect(branchInfo.tree.length).toBe(0);
utils.swapInParentTreeWithSorting(localState, undefined, 'oldPath', undefined);
expect(branchInfo.tree.length).toBe(1);
expect(branchInfo.tree[0].name).toBe('oldPath');
utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', undefined);
expect(branchInfo.tree.length).toBe(2);
expect(branchInfo.tree).toEqual([
jasmine.objectContaining({ name: 'newPath' }),
jasmine.objectContaining({ name: 'oldPath' }),
]);
});
it('adds to parent tree if it is supplied', () => {
utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath');
expect(localState.entries.parentPath.tree.length).toBe(1);
expect(localState.entries.parentPath.tree).toEqual([
jasmine.objectContaining({ name: 'newPath' }),
]);
localState.entries.parentPath.tree = [localState.entries.oldPath];
utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath');
expect(localState.entries.parentPath.tree.length).toBe(2);
expect(localState.entries.parentPath.tree).toEqual([
jasmine.objectContaining({ name: 'newPath' }),
jasmine.objectContaining({ name: 'oldPath' }),
]);
});
});
describe('swapping of the items', () => {
it('swaps entries if both paths are supplied', () => {
branchInfo.tree = [localState.entries.oldPath];
utils.swapInParentTreeWithSorting(localState, localState.entries.oldPath.key, 'newPath');
expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'newPath' })]);
utils.swapInParentTreeWithSorting(localState, localState.entries.newPath.key, 'oldPath');
expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'oldPath' })]);
});
it('sorts tree after swapping the entries', () => {
const alpha = file('alpha', 'alpha', 'blob');
const beta = file('beta', 'beta', 'blob');
const gamma = file('gamma', 'gamma', 'blob');
const theta = file('theta', 'theta', 'blob');
localState.entries = { alpha, beta, gamma, theta };
branchInfo.tree = [alpha, beta, gamma];
utils.swapInParentTreeWithSorting(localState, alpha.key, 'theta');
expect(branchInfo.tree).toEqual([
jasmine.objectContaining({ name: 'beta' }),
jasmine.objectContaining({ name: 'gamma' }),
jasmine.objectContaining({ name: 'theta' }),
]);
utils.swapInParentTreeWithSorting(localState, gamma.key, 'alpha');
expect(branchInfo.tree).toEqual([
jasmine.objectContaining({ name: 'alpha' }),
jasmine.objectContaining({ name: 'beta' }),
jasmine.objectContaining({ name: 'theta' }),
]);
utils.swapInParentTreeWithSorting(localState, beta.key, 'gamma');
expect(branchInfo.tree).toEqual([
jasmine.objectContaining({ name: 'alpha' }),
jasmine.objectContaining({ name: 'gamma' }),
jasmine.objectContaining({ name: 'theta' }),
]);
});
});
});
describe('cleanTrailingSlash', () => {
[
{ input: '', output: '' },
{ input: 'abc', output: 'abc' },
{ input: 'abc/', output: 'abc' },
{ input: 'abc/def', output: 'abc/def' },
{ input: 'abc/def/', output: 'abc/def' },
].forEach(({ input, output }) => {
it(`cleans trailing slash from string "${input}"`, () => {
expect(utils.cleanTrailingSlash(input)).toEqual(output);
});
});
});
describe('pathsAreEqual', () => {
[
{ args: ['abc', 'abc'], output: true },
{ args: ['abc', 'def'], output: false },
{ args: ['abc/', 'abc'], output: true },
{ args: ['abc/abc', 'abc'], output: false },
{ args: ['/', ''], output: true },
{ args: ['', '/'], output: true },
{ args: [false, '/'], output: true },
].forEach(({ args, output }) => {
it(`cleans and tests equality (${JSON.stringify(args)})`, () => {
expect(utils.pathsAreEqual(...args)).toEqual(output);
});
});
});
}); });
...@@ -90,19 +90,6 @@ describe('File row component', () => { ...@@ -90,19 +90,6 @@ describe('File row component', () => {
expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null); expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null);
}); });
it('is not rendered for `moved` entries in subfolders', () => {
createComponent({
file: {
path: 't5',
moved: true,
tree: [],
},
level: 2,
});
expect(vm.$el.nodeType).not.toEqual(1);
});
describe('new dropdown', () => { describe('new dropdown', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
......
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