Commit 751c8c8e authored by Phil Hughes's avatar Phil Hughes

Merge branch '49397-move-files-in-ide' into 'master'

Resolve "Move files in the Web IDE"

Closes #49397

See merge request gitlab-org/gitlab-ce!25431
parents c082ce08 d9ba40aa
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
import ItemButton from './button.vue'; import ItemButton from './button.vue';
import { modalTypes } from '../../constants'; import { modalTypes } from '../../constants';
...@@ -9,7 +8,6 @@ import { modalTypes } from '../../constants'; ...@@ -9,7 +8,6 @@ import { modalTypes } from '../../constants';
export default { export default {
components: { components: {
icon, icon,
newModal,
upload, upload,
ItemButton, ItemButton,
}, },
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { __ } from '~/locale'; import flash from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants'; import { modalTypes } from '../../constants';
...@@ -15,15 +16,17 @@ export default { ...@@ -15,15 +16,17 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['entryModal']), ...mapState(['entries', 'entryModal']),
...mapGetters('fileTemplates', ['templateTypes']), ...mapGetters('fileTemplates', ['templateTypes']),
entryName: { entryName: {
get() { get() {
const entryPath = this.entryModal.entry.path;
if (this.entryModal.type === modalTypes.rename) { if (this.entryModal.type === modalTypes.rename) {
return this.name || this.entryModal.entry.name; return this.name || entryPath;
} }
return this.name || (this.entryModal.path !== '' ? `${this.entryModal.path}/` : ''); return this.name || (entryPath ? `${entryPath}/` : '');
}, },
set(val) { set(val) {
this.name = val; this.name = val;
...@@ -62,10 +65,40 @@ export default { ...@@ -62,10 +65,40 @@ export default {
...mapActions(['createTempEntry', 'renameEntry']), ...mapActions(['createTempEntry', 'renameEntry']),
submitForm() { submitForm() {
if (this.entryModal.type === modalTypes.rename) { if (this.entryModal.type === modalTypes.rename) {
this.renameEntry({ if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
path: this.entryModal.entry.path, flash(
name: this.entryName, sprintf(s__('The name %{entryName} is already taken in this directory.'), {
}); entryName: this.entryName,
}),
'alert',
document,
null,
false,
true,
);
} else {
let parentPath = this.entryName.split('/');
const entryName = parentPath.pop();
parentPath = parentPath.join('/');
const createPromise =
parentPath && !this.entries[parentPath]
? this.createTempEntry({ name: parentPath, type: 'tree' })
: Promise.resolve();
createPromise
.then(() =>
this.renameEntry({
path: this.entryModal.entry.path,
name: entryName,
entryPath: null,
parentPath,
}),
)
.catch(() =>
flash(__('Error creating a new path'), 'alert', document, null, false, true),
);
}
} else { } else {
this.createTempEntry({ this.createTempEntry({
name: this.name, name: this.name,
...@@ -82,7 +115,14 @@ export default { ...@@ -82,7 +115,14 @@ export default {
$('#ide-new-entry').modal('toggle'); $('#ide-new-entry').modal('toggle');
}, },
focusInput() { focusInput() {
const name = this.entries[this.entryName] ? this.entries[this.entryName].name : null;
const inputValue = this.$refs.fieldName.value;
this.$refs.fieldName.focus(); this.$refs.fieldName.focus();
if (name) {
this.$refs.fieldName.setSelectionRange(inputValue.indexOf(name), inputValue.length);
}
}, },
closedModal() { closedModal() {
this.name = ''; this.name = '';
......
...@@ -215,15 +215,27 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => { ...@@ -215,15 +215,27 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => { export const renameEntry = (
{ dispatch, commit, state },
{ path, name, entryPath = null, parentPath },
) => {
const entry = state.entries[entryPath || path]; const entry = state.entries[entryPath || path];
commit(types.RENAME_ENTRY, { path, name, entryPath }); commit(types.RENAME_ENTRY, { path, name, entryPath, parentPath });
if (entry.type === 'tree') { if (entry.type === 'tree') {
state.entries[entryPath || path].tree.forEach(f => const slashedParentPath = parentPath ? `${parentPath}/` : '';
dispatch('renameEntry', { path, name, entryPath: f.path }), const targetEntry = entryPath ? entryPath.split('/').pop() : name;
); const newParentPath = `${slashedParentPath}${targetEntry}`;
state.entries[entryPath || path].tree.forEach(f => {
dispatch('renameEntry', {
path,
name,
entryPath: f.path,
parentPath: newParentPath,
});
});
} }
if (!entryPath && !entry.tempFile) { if (!entryPath && !entry.tempFile) {
......
...@@ -206,19 +206,17 @@ export default { ...@@ -206,19 +206,17 @@ export default {
} }
} }
}, },
[types.RENAME_ENTRY](state, { path, name, entryPath = null }) { [types.RENAME_ENTRY](state, { path, name, entryPath = null, parentPath }) {
const oldEntry = state.entries[entryPath || path]; const oldEntry = state.entries[entryPath || path];
const nameRegex = const slashedParentPath = parentPath ? `${parentPath}/` : '';
!entryPath && oldEntry.type === 'blob' const newPath = entryPath
? new RegExp(`${oldEntry.name}$`) ? `${slashedParentPath}${oldEntry.name}`
: new RegExp(`^${path}`); : `${slashedParentPath}${name}`;
const newPath = oldEntry.path.replace(nameRegex, name);
const parentPath = oldEntry.parentPath ? oldEntry.parentPath.replace(nameRegex, name) : '';
state.entries[newPath] = { state.entries[newPath] = {
...oldEntry, ...oldEntry,
id: newPath, id: newPath,
key: `${name}-${oldEntry.type}-${oldEntry.id}`, key: `${newPath}-${oldEntry.type}-${oldEntry.id}`,
path: newPath, path: newPath,
name: entryPath ? oldEntry.name : name, name: entryPath ? oldEntry.name : name,
tempFile: true, tempFile: true,
...@@ -228,6 +226,7 @@ export default { ...@@ -228,6 +226,7 @@ export default {
parentPath, parentPath,
raw: '', raw: '',
}; };
oldEntry.moved = true; oldEntry.moved = true;
oldEntry.movedPath = newPath; oldEntry.movedPath = newPath;
...@@ -256,6 +255,7 @@ export default { ...@@ -256,6 +255,7 @@ export default {
Vue.delete(state.entries, oldEntry.path); Vue.delete(state.entries, oldEntry.path);
} }
}, },
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
---
title: Resolve Move files in the Web IDE
merge_request: 25431
author:
type: added
...@@ -3133,6 +3133,9 @@ msgstr "" ...@@ -3133,6 +3133,9 @@ msgstr ""
msgid "Error Tracking" msgid "Error Tracking"
msgstr "" msgstr ""
msgid "Error creating a new path"
msgstr ""
msgid "Error deleting %{issuableType}" msgid "Error deleting %{issuableType}"
msgstr "" msgstr ""
...@@ -7384,6 +7387,9 @@ msgstr "" ...@@ -7384,6 +7387,9 @@ msgstr ""
msgid "The maximum file size allowed is 200KB." msgid "The maximum file size allowed is 200KB."
msgstr "" msgstr ""
msgid "The name %{entryName} is already taken in this directory."
msgstr ""
msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>" msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr "" msgstr ""
......
...@@ -18,6 +18,9 @@ describe('new file modal component', () => { ...@@ -18,6 +18,9 @@ describe('new file modal component', () => {
store.state.entryModal = { store.state.entryModal = {
type, type,
path: '', path: '',
entry: {
path: '',
},
}; };
vm = createComponentWithStore(Component, store).$mount(); vm = createComponentWithStore(Component, store).$mount();
...@@ -74,6 +77,7 @@ describe('new file modal component', () => { ...@@ -74,6 +77,7 @@ describe('new file modal component', () => {
entry: { entry: {
name: 'test', name: 'test',
type: 'blob', type: 'blob',
path: 'test-path',
}, },
}; };
...@@ -97,7 +101,7 @@ describe('new file modal component', () => { ...@@ -97,7 +101,7 @@ describe('new file modal component', () => {
describe('entryName', () => { describe('entryName', () => {
it('returns entries name', () => { it('returns entries name', () => {
expect(vm.entryName).toBe('test'); expect(vm.entryName).toBe('test-path');
}); });
it('updated name', () => { it('updated name', () => {
...@@ -107,4 +111,53 @@ describe('new file modal component', () => { ...@@ -107,4 +111,53 @@ describe('new file modal component', () => {
}); });
}); });
}); });
describe('submitForm', () => {
it('throws an error when target entry exists', () => {
const store = createStore();
store.state.entryModal = {
type: 'rename',
path: 'test-path/test',
entry: {
name: 'test',
type: 'blob',
path: 'test-path/test',
},
};
store.state.entries = {
'test-path/test': {
name: 'test',
deleted: false,
},
};
vm = createComponentWithStore(Component, store).$mount();
const flashSpy = spyOnDependency(modal, 'flash');
vm.submitForm();
expect(flashSpy).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();
expect(vm.createTempEntry).toHaveBeenCalledWith({
name: 'test-path1',
type: 'tree',
});
});
});
}); });
...@@ -499,12 +499,12 @@ describe('Multi-file store actions', () => { ...@@ -499,12 +499,12 @@ describe('Multi-file store actions', () => {
testAction( testAction(
renameEntry, renameEntry,
{ path: 'test', name: 'new-name' }, { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
store.state, store.state,
[ [
{ {
type: types.RENAME_ENTRY, type: types.RENAME_ENTRY,
payload: { path: 'test', name: 'new-name', entryPath: null }, payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
}, },
], ],
[{ type: 'deleteEntry', payload: 'test' }], [{ type: 'deleteEntry', payload: 'test' }],
...@@ -527,17 +527,33 @@ describe('Multi-file store actions', () => { ...@@ -527,17 +527,33 @@ describe('Multi-file store actions', () => {
testAction( testAction(
renameEntry, renameEntry,
{ path: 'test', name: 'new-name' }, { path: 'test', name: 'new-name', parentPath: 'parent-path' },
store.state, store.state,
[ [
{ {
type: types.RENAME_ENTRY, type: types.RENAME_ENTRY,
payload: { path: 'test', name: 'new-name', entryPath: null }, payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
}, },
], ],
[ [
{ type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-1' } }, {
{ type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-2' } }, type: 'renameEntry',
payload: {
path: 'test',
name: 'new-name',
entryPath: 'tree-1',
parentPath: 'parent-path/new-name',
},
},
{
type: 'renameEntry',
payload: {
path: 'test',
name: 'new-name',
entryPath: 'tree-2',
parentPath: 'parent-path/new-name',
},
},
{ type: 'deleteEntry', payload: 'test' }, { type: 'deleteEntry', payload: 'test' },
], ],
done, done,
......
...@@ -298,7 +298,12 @@ describe('Multi-file store mutations', () => { ...@@ -298,7 +298,12 @@ describe('Multi-file store mutations', () => {
}); });
it('creates new renamed entry', () => { it('creates new renamed entry', () => {
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
name: 'newPath',
entryPath: null,
parentPath: '',
});
expect(localState.entries.newPath).toEqual({ expect(localState.entries.newPath).toEqual({
...localState.entries.oldPath, ...localState.entries.oldPath,
...@@ -335,7 +340,12 @@ describe('Multi-file store mutations', () => { ...@@ -335,7 +340,12 @@ describe('Multi-file store mutations', () => {
...file(), ...file(),
}; };
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
name: 'newPath',
entryPath: null,
parentPath: 'parentPath',
});
expect(localState.entries.parentPath.tree.length).toBe(1); expect(localState.entries.parentPath.tree.length).toBe(1);
}); });
......
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