diff options
author | Phil Hughes <me@iamphill.com> | 2019-03-04 14:39:55 +0300 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2019-03-04 14:39:55 +0300 |
commit | 751c8c8e47eb3acb225d633eb9dcdaf4151822f7 (patch) | |
tree | 4001b7228e99c23d0a5a27f04c29d36b566d0206 | |
parent | c082ce08c842516c0c3fc85bbf0167281db201c6 (diff) | |
parent | d9ba40aa044e63b95de2e1dae1d54da7f500cd6b (diff) |
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
-rw-r--r-- | app/assets/javascripts/ide/components/new_dropdown/index.vue | 2 | ||||
-rw-r--r-- | app/assets/javascripts/ide/components/new_dropdown/modal.vue | 56 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/actions.js | 22 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/mutations.js | 16 | ||||
-rw-r--r-- | changelogs/unreleased/49397-move-files-in-ide.yml | 5 | ||||
-rw-r--r-- | locale/gitlab.pot | 6 | ||||
-rw-r--r-- | spec/javascripts/ide/components/new_dropdown/modal_spec.js | 55 | ||||
-rw-r--r-- | spec/javascripts/ide/stores/actions_spec.js | 28 | ||||
-rw-r--r-- | spec/javascripts/ide/stores/mutations_spec.js | 14 |
9 files changed, 172 insertions, 32 deletions
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index d7a7b1b4d78..593a9162a06 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -1,7 +1,6 @@ <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, }, diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index c9c4e9e86f8..ba6bbdfef4b 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,6 +1,7 @@ <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) { - this.renameEntry({ - path: this.entryModal.entry.path, - name: this.entryName, - }); + 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: 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 = ''; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index e10a132ab4b..95d91e08757 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -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) { diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 78cdfda74f0..9b9f4b21f1c 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -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, diff --git a/changelogs/unreleased/49397-move-files-in-ide.yml b/changelogs/unreleased/49397-move-files-in-ide.yml new file mode 100644 index 00000000000..488091d383c --- /dev/null +++ b/changelogs/unreleased/49397-move-files-in-ide.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Move files in the Web IDE +merge_request: 25431 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cb599e4744c..ec36341be61 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index d94cc1a8faa..d1a0964ccdd 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -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', + }); + }); + }); }); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index df291ade3f7..0b5587d02ae 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -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, diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 41dd3d3c67f..5ee098bf17f 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -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); }); |