Commit 33c9700e authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '195776-fix-discard-rename-web-ide' into 'master'

Fix discarding renamed directories in Web IDE

See merge request gitlab-org/gitlab!22943
parents 8cf9b74e 32173740
...@@ -67,8 +67,8 @@ export default { ...@@ -67,8 +67,8 @@ export default {
if (this.entryModal.type === modalTypes.rename) { if (this.entryModal.type === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) { if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
flash( flash(
sprintf(s__('The name %{entryName} is already taken in this directory.'), { sprintf(s__('The name "%{name}" is already taken in this directory.'), {
entryName: this.entryName, name: this.entryName,
}), }),
'alert', 'alert',
document, document,
...@@ -81,22 +81,11 @@ export default { ...@@ -81,22 +81,11 @@ export default {
const entryName = parentPath.pop(); const entryName = parentPath.pop();
parentPath = parentPath.join('/'); parentPath = parentPath.join('/');
const createPromise = this.renameEntry({
parentPath && !this.entries[parentPath] path: this.entryModal.entry.path,
? this.createTempEntry({ name: parentPath, type: 'tree' }) name: entryName,
: Promise.resolve(); parentPath,
});
createPromise
.then(() =>
this.renameEntry({
path: this.entryModal.entry.path,
name: entryName,
parentPath,
}),
)
.catch(() =>
flash(__('Error creating a new path'), 'alert', document, null, false, true),
);
} }
} else { } else {
this.createTempEntry({ this.createTempEntry({
......
...@@ -53,60 +53,55 @@ export const setResizingStatus = ({ commit }, resizing) => { ...@@ -53,60 +53,55 @@ export const setResizingStatus = ({ commit }, resizing) => {
export const createTempEntry = ( export const createTempEntry = (
{ state, commit, dispatch }, { state, commit, dispatch },
{ name, type, content = '', base64 = false, binary = false, rawPath = '' }, { name, type, content = '', base64 = false, binary = false, rawPath = '' },
) => ) => {
new Promise(resolve => { const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name] && !state.entries[name].deleted) {
if (state.entries[name] && !state.entries[name].deleted) { flash(
flash( sprintf(__('The name "%{name}" is already taken in this directory.'), {
`The name "${name.split('/').pop()}" is already taken in this directory.`, name: name.split('/').pop(),
'alert', }),
document, 'alert',
null, document,
false, null,
true, false,
); true,
);
resolve();
return null;
}
const data = decorateFiles({
data: [fullName],
projectId: state.currentProjectId,
branchId: state.currentBranchId,
type,
tempFile: true,
content,
base64,
binary,
rawPath,
});
const { file, parentPath } = data;
commit(types.CREATE_TMP_ENTRY, { return;
data, }
projectId: state.currentProjectId,
branchId: state.currentBranchId,
});
if (type === 'blob') { const data = decorateFiles({
commit(types.TOGGLE_FILE_OPEN, file.path); data: [fullName],
commit(types.ADD_FILE_TO_CHANGED, file.path); projectId: state.currentProjectId,
dispatch('setFileActive', file.path); branchId: state.currentBranchId,
dispatch('triggerFilesChange'); type,
dispatch('burstUnusedSeal'); tempFile: true,
} content,
base64,
binary,
rawPath,
});
const { file, parentPath } = data;
if (parentPath && !state.entries[parentPath].opened) { commit(types.CREATE_TMP_ENTRY, {
commit(types.TOGGLE_TREE_OPEN, parentPath); data,
} projectId: state.currentProjectId,
branchId: state.currentBranchId,
});
resolve(file); if (type === 'blob') {
commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
dispatch('triggerFilesChange');
dispatch('burstUnusedSeal');
}
return null; if (parentPath && !state.entries[parentPath].opened) {
}); commit(types.TOGGLE_TREE_OPEN, parentPath);
}
};
export const scrollToTab = () => { export const scrollToTab = () => {
Vue.nextTick(() => { Vue.nextTick(() => {
...@@ -211,8 +206,9 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => { ...@@ -211,8 +206,9 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path]; const entry = state.entries[path];
const { prevPath, prevName, prevParentPath } = entry; const { prevPath, prevName, prevParentPath } = entry;
const isTree = entry.type === 'tree'; const isTree = entry.type === 'tree';
const prevEntry = prevPath && state.entries[prevPath];
if (prevPath) { if (prevPath && (!prevEntry || prevEntry.deleted)) {
dispatch('renameEntry', { dispatch('renameEntry', {
path, path,
name: prevName, name: prevName,
...@@ -245,6 +241,11 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); ...@@ -245,6 +241,11 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => { export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => {
const entry = state.entries[path]; const entry = state.entries[path];
const newPath = parentPath ? `${parentPath}/${name}` : name; const newPath = parentPath ? `${parentPath}/${name}` : name;
const existingParent = parentPath && state.entries[parentPath];
if (parentPath && (!existingParent || existingParent.deleted)) {
dispatch('createTempEntry', { name: parentPath, type: 'tree' });
}
commit(types.RENAME_ENTRY, { path, name, parentPath }); commit(types.RENAME_ENTRY, { path, name, parentPath });
......
---
title: Fix discarding renamed directories in Web IDE
merge_request: 22943
author:
type: fixed
...@@ -7209,9 +7209,6 @@ msgstr "" ...@@ -7209,9 +7209,6 @@ msgstr ""
msgid "Error Tracking" msgid "Error Tracking"
msgstr "" msgstr ""
msgid "Error creating a new path"
msgstr ""
msgid "Error creating epic" msgid "Error creating epic"
msgstr "" msgstr ""
...@@ -18263,7 +18260,7 @@ msgstr "" ...@@ -18263,7 +18260,7 @@ msgstr ""
msgid "The merge request can now be merged." msgid "The merge request can now be merged."
msgstr "" msgstr ""
msgid "The name %{entryName} is already taken in this directory." msgid "The name \"%{name}\" is already taken in this directory."
msgstr "" msgstr ""
msgid "The number of changes to be fetched from GitLab when cloning a repository. This can speed up Pipelines execution. Keep empty or set to 0 to disable shallow clone by default and make GitLab CI fetch all branches and tags each time." msgid "The number of changes to be fetched from GitLab when cloning a repository. This can speed up Pipelines execution. Keep empty or set to 0 to disable shallow clone by default and make GitLab CI fetch all branches and tags each time."
......
...@@ -52,19 +52,6 @@ describe('new file modal component', () => { ...@@ -52,19 +52,6 @@ describe('new file modal component', () => {
expect(templateFilesEl instanceof Element).toBeTruthy(); expect(templateFilesEl instanceof Element).toBeTruthy();
} }
}); });
describe('createEntryInStore', () => {
it('$emits create', () => {
spyOn(vm, 'createTempEntry');
vm.submitForm();
expect(vm.createTempEntry).toHaveBeenCalledWith({
name: 'testing',
type,
});
});
});
}); });
}); });
...@@ -145,31 +132,19 @@ describe('new file modal component', () => { ...@@ -145,31 +132,19 @@ describe('new file modal component', () => {
vm = createComponentWithStore(Component, store).$mount(); vm = createComponentWithStore(Component, store).$mount();
const flashSpy = spyOnDependency(modal, 'flash'); const flashSpy = spyOnDependency(modal, 'flash');
vm.submitForm();
expect(flashSpy).toHaveBeenCalled(); expect(flashSpy).not.toHaveBeenCalled();
});
it('calls createTempEntry when target path does not exist', () => {
const store = createStore();
store.state.entryModal = {
type: 'rename',
path: 'test-path/test',
entry: {
name: 'test',
type: 'blob',
path: 'test-path1/test',
},
};
vm = createComponentWithStore(Component, store).$mount();
spyOn(vm, 'createTempEntry').and.callFake(() => Promise.resolve());
vm.submitForm(); vm.submitForm();
expect(vm.createTempEntry).toHaveBeenCalledWith({ expect(flashSpy).toHaveBeenCalledWith(
name: 'test-path1', 'The name "test-path/test" is already taken in this directory.',
type: 'tree', 'alert',
}); jasmine.anything(),
null,
false,
true,
);
}); });
}); });
}); });
...@@ -206,13 +206,17 @@ describe('Multi-file store actions', () => { ...@@ -206,13 +206,17 @@ describe('Multi-file store actions', () => {
describe('blob', () => { describe('blob', () => {
it('creates temp file', done => { it('creates temp file', done => {
const name = 'test';
store store
.dispatch('createTempEntry', { .dispatch('createTempEntry', {
name: 'test', name,
branchId: 'mybranch', branchId: 'mybranch',
type: 'blob', type: 'blob',
}) })
.then(f => { .then(() => {
const f = store.state.entries[name];
expect(f.tempFile).toBeTruthy(); expect(f.tempFile).toBeTruthy();
expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1); expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
...@@ -222,13 +226,17 @@ describe('Multi-file store actions', () => { ...@@ -222,13 +226,17 @@ describe('Multi-file store actions', () => {
}); });
it('adds tmp file to open files', done => { it('adds tmp file to open files', done => {
const name = 'test';
store store
.dispatch('createTempEntry', { .dispatch('createTempEntry', {
name: 'test', name,
branchId: 'mybranch', branchId: 'mybranch',
type: 'blob', type: 'blob',
}) })
.then(f => { .then(() => {
const f = store.state.entries[name];
expect(store.state.openFiles.length).toBe(1); expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(f.name); expect(store.state.openFiles[0].name).toBe(f.name);
...@@ -238,13 +246,17 @@ describe('Multi-file store actions', () => { ...@@ -238,13 +246,17 @@ describe('Multi-file store actions', () => {
}); });
it('adds tmp file to changed files', done => { it('adds tmp file to changed files', done => {
const name = 'test';
store store
.dispatch('createTempEntry', { .dispatch('createTempEntry', {
name: 'test', name,
branchId: 'mybranch', branchId: 'mybranch',
type: 'blob', type: 'blob',
}) })
.then(f => { .then(() => {
const f = store.state.entries[name];
expect(store.state.changedFiles.length).toBe(1); expect(store.state.changedFiles.length).toBe(1);
expect(store.state.changedFiles[0].name).toBe(f.name); expect(store.state.changedFiles[0].name).toBe(f.name);
...@@ -292,7 +304,9 @@ describe('Multi-file store actions', () => { ...@@ -292,7 +304,9 @@ describe('Multi-file store actions', () => {
type: 'blob', type: 'blob',
}) })
.then(() => { .then(() => {
expect(document.querySelector('.flash-alert')).not.toBeNull(); expect(document.querySelector('.flash-alert')?.textContent.trim()).toEqual(
`The name "${f.name}" is already taken in this directory.`,
);
done(); done();
}) })
...@@ -604,36 +618,98 @@ describe('Multi-file store actions', () => { ...@@ -604,36 +618,98 @@ describe('Multi-file store actions', () => {
); );
}); });
it('if renamed, reverts the rename before deleting', () => { describe('when renamed', () => {
const testEntry = { let testEntry;
path: 'test',
name: 'test',
prevPath: 'lorem/ipsum',
prevName: 'ipsum',
prevParentPath: 'lorem',
};
store.state.entries = { test: testEntry }; beforeEach(() => {
testAction( testEntry = {
deleteEntry, path: 'test',
testEntry.path, name: 'test',
store.state, prevPath: 'test_old',
[], prevName: 'test_old',
[ prevParentPath: '',
{ };
type: 'renameEntry',
payload: { store.state.entries = { test: testEntry };
path: testEntry.path, });
name: testEntry.prevName,
parentPath: testEntry.prevParentPath, describe('and previous does not exist', () => {
}, it('reverts the rename before deleting', done => {
}, testAction(
{ deleteEntry,
type: 'deleteEntry', testEntry.path,
payload: testEntry.prevPath, store.state,
}, [],
], [
); {
type: 'renameEntry',
payload: {
path: testEntry.path,
name: testEntry.prevName,
parentPath: testEntry.prevParentPath,
},
},
{
type: 'deleteEntry',
payload: testEntry.prevPath,
},
],
done,
);
});
});
describe('and previous exists', () => {
beforeEach(() => {
const oldEntry = {
path: testEntry.prevPath,
name: testEntry.prevName,
};
store.state.entries[oldEntry.path] = oldEntry;
});
it('does not revert rename before deleting', done => {
testAction(
deleteEntry,
testEntry.path,
store.state,
[{ type: types.DELETE_ENTRY, payload: testEntry.path }],
[
{ type: 'burstUnusedSeal' },
{ type: 'stageChange', payload: testEntry.path },
{ type: 'triggerFilesChange' },
],
done,
);
});
it('when previous is deleted, it reverts rename before deleting', done => {
store.state.entries[testEntry.prevPath].deleted = true;
testAction(
deleteEntry,
testEntry.path,
store.state,
[],
[
{
type: 'renameEntry',
payload: {
path: testEntry.path,
name: testEntry.prevName,
parentPath: testEntry.prevParentPath,
},
},
{
type: 'deleteEntry',
payload: testEntry.prevPath,
},
],
done,
);
});
});
}); });
it('bursts unused seal', done => { it('bursts unused seal', done => {
...@@ -918,6 +994,103 @@ describe('Multi-file store actions', () => { ...@@ -918,6 +994,103 @@ describe('Multi-file store actions', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
describe('with file in directory', () => {
const parentPath = 'original-dir';
const newParentPath = 'new-dir';
const fileName = 'test.md';
const filePath = `${parentPath}/${fileName}`;
let rootDir;
beforeEach(() => {
const parentEntry = file(parentPath, parentPath, 'tree');
const fileEntry = file(filePath, filePath, 'blob', parentEntry);
rootDir = {
tree: [],
};
Object.assign(store.state, {
entries: {
[parentPath]: {
...parentEntry,
tree: [fileEntry],
},
[filePath]: fileEntry,
},
trees: {
'/': rootDir,
},
});
});
it('creates new directory', done => {
expect(store.state.entries[newParentPath]).toBeUndefined();
store
.dispatch('renameEntry', { path: filePath, name: fileName, parentPath: newParentPath })
.then(() => {
expect(store.state.entries[newParentPath]).toEqual(
jasmine.objectContaining({
path: newParentPath,
type: 'tree',
tree: jasmine.arrayContaining([
store.state.entries[`${newParentPath}/${fileName}`],
]),
}),
);
})
.then(done)
.catch(done.fail);
});
describe('when new directory exists', () => {
let newDir;
beforeEach(() => {
newDir = file(newParentPath, newParentPath, 'tree');
store.state.entries[newDir.path] = newDir;
rootDir.tree.push(newDir);
});
it('inserts in new directory', done => {
expect(newDir.tree).toEqual([]);
store
.dispatch('renameEntry', {
path: filePath,
name: fileName,
parentPath: newParentPath,
})
.then(() => {
expect(newDir.tree).toEqual([store.state.entries[`${newParentPath}/${fileName}`]]);
})
.then(done)
.catch(done.fail);
});
it('when new directory is deleted, it undeletes it', done => {
store.dispatch('deleteEntry', newParentPath);
expect(store.state.entries[newParentPath].deleted).toBe(true);
expect(rootDir.tree.some(x => x.path === newParentPath)).toBe(false);
store
.dispatch('renameEntry', {
path: filePath,
name: fileName,
parentPath: newParentPath,
})
.then(() => {
expect(store.state.entries[newParentPath].deleted).toBe(false);
expect(rootDir.tree.some(x => x.path === newParentPath)).toBe(true);
})
.then(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