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>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
......@@ -9,7 +8,6 @@ import { modalTypes } from '../../constants';
export default {
components: {
icon,
newModal,
upload,
ItemButton,
},
......
<script>
import $ from 'jquery';
import { __ } from '~/locale';
import flash from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants';
......@@ -15,15 +16,17 @@ export default {
};
},
computed: {
...mapState(['entryModal']),
...mapState(['entries', 'entryModal']),
...mapGetters('fileTemplates', ['templateTypes']),
entryName: {
get() {
const entryPath = this.entryModal.entry.path;
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) {
this.name = val;
......@@ -62,10 +65,40 @@ export default {
...mapActions(['createTempEntry', 'renameEntry']),
submitForm() {
if (this.entryModal.type === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
flash(
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: this.entryName,
});
name: entryName,
entryPath: null,
parentPath,
}),
)
.catch(() =>
flash(__('Error creating a new path'), 'alert', document, null, false, true),
);
}
} else {
this.createTempEntry({
name: this.name,
......@@ -82,7 +115,14 @@ export default {
$('#ide-new-entry').modal('toggle');
},
focusInput() {
const name = this.entries[this.entryName] ? this.entries[this.entryName].name : null;
const inputValue = this.$refs.fieldName.value;
this.$refs.fieldName.focus();
if (name) {
this.$refs.fieldName.setSelectionRange(inputValue.indexOf(name), inputValue.length);
}
},
closedModal() {
this.name = '';
......
......@@ -215,15 +215,27 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
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];
commit(types.RENAME_ENTRY, { path, name, entryPath });
commit(types.RENAME_ENTRY, { path, name, entryPath, parentPath });
if (entry.type === 'tree') {
state.entries[entryPath || path].tree.forEach(f =>
dispatch('renameEntry', { path, name, entryPath: f.path }),
);
const slashedParentPath = parentPath ? `${parentPath}/` : '';
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) {
......
......@@ -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 nameRegex =
!entryPath && oldEntry.type === 'blob'
? new RegExp(`${oldEntry.name}$`)
: new RegExp(`^${path}`);
const newPath = oldEntry.path.replace(nameRegex, name);
const parentPath = oldEntry.parentPath ? oldEntry.parentPath.replace(nameRegex, name) : '';
const slashedParentPath = parentPath ? `${parentPath}/` : '';
const newPath = entryPath
? `${slashedParentPath}${oldEntry.name}`
: `${slashedParentPath}${name}`;
state.entries[newPath] = {
...oldEntry,
id: newPath,
key: `${name}-${oldEntry.type}-${oldEntry.id}`,
key: `${newPath}-${oldEntry.type}-${oldEntry.id}`,
path: newPath,
name: entryPath ? oldEntry.name : name,
tempFile: true,
......@@ -228,6 +226,7 @@ export default {
parentPath,
raw: '',
};
oldEntry.moved = true;
oldEntry.movedPath = newPath;
......@@ -256,6 +255,7 @@ export default {
Vue.delete(state.entries, oldEntry.path);
}
},
...projectMutations,
...mergeRequestMutation,
...fileMutations,
......
---
title: Resolve Move files in the Web IDE
merge_request: 25431
author:
type: added
......@@ -3133,6 +3133,9 @@ msgstr ""
msgid "Error Tracking"
msgstr ""
msgid "Error creating a new path"
msgstr ""
msgid "Error deleting %{issuableType}"
msgstr ""
......@@ -7384,6 +7387,9 @@ msgstr ""
msgid "The maximum file size allowed is 200KB."
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>"
msgstr ""
......
......@@ -18,6 +18,9 @@ describe('new file modal component', () => {
store.state.entryModal = {
type,
path: '',
entry: {
path: '',
},
};
vm = createComponentWithStore(Component, store).$mount();
......@@ -74,6 +77,7 @@ describe('new file modal component', () => {
entry: {
name: 'test',
type: 'blob',
path: 'test-path',
},
};
......@@ -97,7 +101,7 @@ describe('new file modal component', () => {
describe('entryName', () => {
it('returns entries name', () => {
expect(vm.entryName).toBe('test');
expect(vm.entryName).toBe('test-path');
});
it('updated name', () => {
......@@ -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', () => {
testAction(
renameEntry,
{ path: 'test', name: 'new-name' },
{ path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
store.state,
[
{
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' }],
......@@ -527,17 +527,33 @@ describe('Multi-file store actions', () => {
testAction(
renameEntry,
{ path: 'test', name: 'new-name' },
{ path: 'test', name: 'new-name', parentPath: 'parent-path' },
store.state,
[
{
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' },
],
done,
......
......@@ -298,7 +298,12 @@ describe('Multi-file store mutations', () => {
});
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({
...localState.entries.oldPath,
......@@ -335,7 +340,12 @@ describe('Multi-file store mutations', () => {
...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);
});
......
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