diff options
26 files changed, 504 insertions, 93 deletions
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 8eddfa5e455..f02fd6cf7ea 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -4,6 +4,7 @@ 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'; export default { components: { @@ -56,6 +57,7 @@ export default { this.dropdownOpen = !this.dropdownOpen; }, }, + modalTypes, }; </script> @@ -74,7 +76,7 @@ export default { @click.stop="openDropdown()" > <icon - name="hamburger" + name="ellipsis_v" /> <icon name="arrow-down" @@ -106,13 +108,22 @@ export default { class="d-flex" icon="folder-new" icon-classes="mr-2" - @click="createNewItem('tree')" + @click="createNewItem($options.modalTypes.tree)" /> </li> <li class="divider"></li> </template> <li> <item-button + :label="__('Rename')" + class="d-flex" + icon="pencil" + icon-classes="mr-2" + @click="createNewItem($options.modalTypes.rename)" + /> + </li> + <li> + <item-button :label="__('Delete')" class="d-flex" icon="remove" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 833c4b027df..e500ef0e1b5 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -2,6 +2,7 @@ import { __ } from '~/locale'; import { mapActions, mapState } from 'vuex'; import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { modalTypes } from '../../constants'; export default { components: { @@ -13,42 +14,58 @@ export default { }; }, computed: { - ...mapState(['newEntryModal']), + ...mapState(['entryModal']), entryName: { get() { - return this.name || (this.newEntryModal.path !== '' ? `${this.newEntryModal.path}/` : ''); + if (this.entryModal.type === modalTypes.rename) { + return this.name || this.entryModal.entry.name; + } + + return this.name || (this.entryModal.path !== '' ? `${this.entryModal.path}/` : ''); }, set(val) { this.name = val; }, }, modalTitle() { - if (this.newEntryModal.type === 'tree') { + if (this.entryModal.type === modalTypes.tree) { return __('Create new directory'); + } else if (this.entryModal.type === modalTypes.rename) { + return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); } return __('Create new file'); }, buttonLabel() { - if (this.newEntryModal.type === 'tree') { + if (this.entryModal.type === modalTypes.tree) { return __('Create directory'); + } else if (this.entryModal.type === modalTypes.rename) { + return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); } return __('Create file'); }, }, methods: { - ...mapActions(['createTempEntry']), - createEntryInStore() { - this.createTempEntry({ - name: this.name, - type: this.newEntryModal.type, - }); + ...mapActions(['createTempEntry', 'renameEntry']), + submitForm() { + if (this.entryModal.type === modalTypes.rename) { + this.renameEntry({ + path: this.entryModal.entry.path, + name: this.entryName, + }); + } else { + this.createTempEntry({ + name: this.name, + type: this.entryModal.type, + }); + } }, focusInput() { - setTimeout(() => { - this.$refs.fieldName.focus(); - }); + this.$refs.fieldName.focus(); + }, + closedModal() { + this.name = ''; }, }, }; @@ -60,8 +77,9 @@ export default { :header-title-text="modalTitle" :footer-primary-button-text="buttonLabel" footer-primary-button-variant="success" - @submit="createEntryInStore" + @submit="submitForm" @open="focusInput" + @closed="closedModal" > <div class="form-group row" diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index eb4a927fe0d..dbdf0be2809 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -134,8 +134,7 @@ export default { .replace(/[/]$/g, ''); // - strip ending "/" - const filePath = this.file.path - .replace(/[/]$/g, ''); + const filePath = this.file.path.replace(/[/]$/g, ''); return filePath === routePath; }, @@ -194,7 +193,7 @@ export default { data-container="body" data-placement="right" name="file-modified" - css-classes="prepend-left-5 multi-file-modified" + css-classes="prepend-left-5 ide-file-modified" /> </span> <changed-file-icon @@ -208,7 +207,6 @@ export default { </span> <new-dropdown :type="file.type" - :branch="file.branchId" :path="file.path" :mouse-over="mouseOver" class="float-right prepend-left-8" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 0b514f31467..d3ac57471c9 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -53,3 +53,8 @@ export const commitItemIconMap = { class: 'ide-file-deletion', }, }; + +export const modalTypes = { + rename: 'rename', + tree: 'tree', +}; diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index cb93fba1665..f0193d8e8ea 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -8,7 +8,7 @@ export default { }); }, getRawFileData(file) { - if (file.tempFile) { + if (file.tempFile && !file.prevPath) { return Promise.resolve(file.content); } diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 2765acada48..aa02dfbddc4 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -186,13 +186,39 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { }; export const deleteEntry = ({ commit, dispatch, state }, path) => { - dispatch('burstUnusedSeal'); - dispatch('closeFile', state.entries[path]); + const entry = state.entries[path]; + + if (state.unusedSeal) dispatch('burstUnusedSeal'); + if (entry.opened) dispatch('closeFile', entry); + + if (entry.type === 'tree') { + entry.tree.forEach(f => dispatch('deleteEntry', f.path)); + } + commit(types.DELETE_ENTRY, path); + + if (entry.parentPath && state.entries[entry.parentPath].tree.length === 0) { + dispatch('deleteEntry', entry.parentPath); + } }; export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); +export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => { + const entry = state.entries[entryPath || path]; + commit(types.RENAME_ENTRY, { path, name, entryPath }); + + if (entry.type === 'tree') { + state.entries[entryPath || path].tree.forEach(f => + dispatch('renameEntry', { path, name, entryPath: f.path }), + ); + } + + if (!entryPath) { + dispatch('deleteEntry', path); + } +}; + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index b343750f789..9e3f5da4676 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -62,14 +62,14 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { const file = state.entries[path]; - if (file.raw || file.tempFile) return Promise.resolve(); + if (file.raw || (file.tempFile && !file.prevPath)) return Promise.resolve(); commit(types.TOGGLE_LOADING, { entry: file }); + const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url; + return service - .getFileData( - `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, - ) + .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${url.replace('/-/', '/')}`) .then(({ data, headers }) => { const normalizedHeaders = normalizeHeaders(headers); setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); @@ -101,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = service .getRawFileData(file) .then(raw => { - if (!file.tempFile) commit(types.SET_FILE_RAW_DATA, { file, raw }); + if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (file.mrChange && file.mrChange.new_file === false) { service .getBaseRawFileData(file, baseSha) @@ -176,9 +176,22 @@ export const setFileViewMode = ({ commit }, { file, viewMode }) => { export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => { const file = state.entries[path]; + if (file.deleted && 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.REMOVE_FILE_FROM_CHANGED, path); + if (file.prevPath) { + dispatch('discardFileChanges', file.prevPath); + } + if (file.tempFile && file.opened) { commit(types.TOGGLE_FILE_OPEN, path); } else if (getters.activeFile && file.path === getters.activeFile.path) { diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index acb6ef5e6d4..9288bbe32f5 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -89,3 +89,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = resolve(); } }); + +export const restoreTree = ({ dispatch, commit, state }, path) => { + const entry = state.entries[path]; + + commit(types.RESTORE_TREE, path); + + if (entry.parentPath) { + dispatch('restoreTree', entry.parentPath); + } +}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 5ce268b0d05..79cdb494e5a 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -67,9 +67,9 @@ export const someUncommitedChanges = state => !!(state.changedFiles.length || state.stagedFiles.length); export const getChangesInFolder = state => path => { - const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length; + const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f.path, path)).length; const stagedFilesCount = state.stagedFiles.filter( - f => filePathMatches(f, path) && !getChangedFile(state)(f.path), + f => filePathMatches(f.path, path) && !getChangedFile(state)(f.path), ).length; return changedFilesCount + stagedFilesCount; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index dae60f4d65a..5a7991d2fa7 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -77,3 +77,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; export const DELETE_ENTRY = 'DELETE_ENTRY'; +export const RENAME_ENTRY = 'RENAME_ENTRY'; + +export const RESTORE_TREE = 'RESTORE_TREE'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 799c2f51e8d..d0bf847dbde 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -131,11 +131,14 @@ export default { }, [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { const changedFile = state.changedFiles.find(f => f.path === file.path); + const { prevPath } = file; Object.assign(state.entries[file.path], { raw: file.content, changed: !!changedFile, staged: false, + prevPath: '', + moved: false, lastCommit: Object.assign(state.entries[file.path].lastCommit, { id: lastCommit.commit.id, url: lastCommit.commit_path, @@ -144,6 +147,18 @@ export default { updatedAt: lastCommit.commit.authored_date, }), }); + + if (prevPath) { + // Update URLs after file has moved + const regex = new RegExp(`${prevPath}$`); + + Object.assign(state.entries[file.path], { + rawPath: file.rawPath.replace(regex, file.path), + permalink: file.permalink.replace(regex, file.path), + commitsPath: file.commitsPath.replace(regex, file.path), + blamePath: file.blamePath.replace(regex, file.path), + }); + } }, [types.BURST_UNUSED_SEAL](state) { Object.assign(state, { @@ -169,7 +184,11 @@ export default { }, [types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) { Object.assign(state, { - newEntryModal: { type, path }, + entryModal: { + type, + path, + entry: { ...state.entries[path] }, + }, }); }, [types.DELETE_ENTRY](state, path) { @@ -179,8 +198,48 @@ export default { : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; entry.deleted = true; - state.changedFiles = state.changedFiles.concat(entry); + parent.tree = parent.tree.filter(f => f.path !== entry.path); + + if (entry.type === 'blob') { + state.changedFiles = state.changedFiles.concat(entry); + } + }, + [types.RENAME_ENTRY](state, { path, name, entryPath = null }) { + 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) : ''; + + state.entries[newPath] = { + ...oldEntry, + id: newPath, + key: `${name}-${oldEntry.type}-${oldEntry.id}`, + path: newPath, + name: entryPath ? oldEntry.name : name, + tempFile: true, + prevPath: oldEntry.path, + url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath), + tree: [], + parentPath, + raw: '', + }; + oldEntry.moved = true; + oldEntry.movedPath = newPath; + + const parent = parentPath + ? state.entries[parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + const newEntry = state.entries[newPath]; + + parent.tree = sortTree(parent.tree.concat(newEntry)); + + if (newEntry.type === 'blob') { + state.changedFiles = state.changedFiles.concat(newEntry); + } }, ...projectMutations, ...mergeRequestMutation, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 9a87d50d6d5..c75add39bcd 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -53,15 +53,25 @@ export default { }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { const openPendingFile = state.openFiles.find( - f => f.path === file.path && f.pending && !f.tempFile, + f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath), ); - Object.assign(state.entries[file.path], { - raw, - }); + if (file.tempFile) { + Object.assign(state.entries[file.path], { + content: raw, + }); + } else { + Object.assign(state.entries[file.path], { + raw, + }); + } - if (openPendingFile) { + if (!openPendingFile) return; + + if (!openPendingFile.tempFile) { openPendingFile.raw = raw; + } else if (openPendingFile.tempFile) { + openPendingFile.content = raw; } }, [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { @@ -119,12 +129,14 @@ export default { [types.DISCARD_FILE_CHANGES](state, path) { const stagedFile = state.stagedFiles.find(f => f.path === path); const entry = state.entries[path]; - const { deleted } = entry; + const { deleted, prevPath } = entry; Object.assign(state.entries[path], { content: stagedFile ? stagedFile.content : state.entries[path].raw, changed: false, deleted: false, + moved: false, + movedPath: '', }); if (deleted) { @@ -133,6 +145,12 @@ export default { : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; parent.tree = sortTree(parent.tree.concat(entry)); + } else if (prevPath) { + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + + parent.tree = parent.tree.filter(f => f.path !== path); } }, [types.ADD_FILE_TO_CHANGED](state, path) { diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index 2cf34af9274..eac7441ee54 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -1,4 +1,5 @@ import * as types from '../mutation_types'; +import { sortTree } from '../utils'; export default { [types.TOGGLE_TREE_OPEN](state, path) { @@ -36,4 +37,14 @@ export default { changedFiles: [], }); }, + [types.RESTORE_TREE](state, path) { + const entry = state.entries[path]; + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + + if (!parent.tree.find(f => f.path === path)) { + parent.tree = sortTree(parent.tree.concat(entry)); + } + }, }; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 0f32a267469..2371b201f8c 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -26,8 +26,9 @@ export default () => ({ rightPane: null, links: {}, errorMessage: null, - newEntryModal: { + entryModal: { type: '', path: '', + entry: {}, }, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index bf7ab93ff5e..0ede76fd1e0 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -47,6 +47,9 @@ export const dataStructure = () => ({ lastOpenedAt: 0, mrChange: null, deleted: false, + prevPath: '', + movedPath: '', + moved: false, }); export const decorateData = entity => { @@ -107,7 +110,9 @@ export const setPageTitle = title => { }; export const commitActionForFile = file => { - if (file.deleted) { + if (file.prevPath) { + return 'move'; + } else if (file.deleted) { return 'delete'; } else if (file.tempFile) { return 'create'; @@ -116,15 +121,12 @@ export const commitActionForFile = file => { return 'update'; }; -export const getCommitFiles = (stagedFiles, deleteTree = false) => +export const getCommitFiles = stagedFiles => stagedFiles.reduce((acc, file) => { - if ((file.deleted || deleteTree) && file.type === 'tree') { - return acc.concat(getCommitFiles(file.tree, true)); - } + if (file.moved) return acc; return acc.concat({ ...file, - deleted: deleteTree || file.deleted, }); }, []); @@ -134,9 +136,10 @@ export const createCommitPayload = ({ branch, getters, newBranch, state, rootSta actions: getCommitFiles(rootState.stagedFiles).map(f => ({ action: commitActionForFile(f), file_path: f.path, - content: f.content, + previous_path: f.prevPath === '' ? undefined : f.prevPath, + content: f.content || undefined, encoding: f.base64 ? 'base64' : 'text', - last_commit_id: newBranch || f.deleted ? undefined : f.lastCommitSha, + last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha, })), start_branch: newBranch ? rootState.currentBranchId : undefined, }); @@ -164,8 +167,7 @@ export const sortTree = sortedTree => ) .sort(sortTreesByTypeAndName); -export const filePathMatches = (f, path) => - f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0; +export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`) === 0; export const getChangesCountForFiles = (files, path) => - files.filter(f => filePathMatches(f, path)).length; + files.filter(f => filePathMatches(f.path, path)).length; diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index 416eda796a7..b023c5cfeb1 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -1,4 +1,6 @@ <script> +import $ from 'jquery'; + const buttonVariants = ['danger', 'primary', 'success', 'warning']; const sizeVariants = ['sm', 'md', 'lg', 'xl']; @@ -38,6 +40,12 @@ export default { return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`; }, }, + mounted() { + $(this.$el).on('shown.bs.modal', this.opened).on('hidden.bs.modal', this.closed); + }, + beforeDestroy() { + $(this.$el).off('shown.bs.modal', this.opened).off('hidden.bs.modal', this.closed); + }, methods: { emitCancel(event) { this.$emit('cancel', event); @@ -45,10 +53,11 @@ export default { emitSubmit(event) { this.$emit('submit', event); }, - opened({ propertyName }) { - if (propertyName === 'opacity') { - this.$emit('open'); - } + opened() { + this.$emit('open'); + }, + closed() { + this.$emit('closed'); }, }, }; @@ -60,7 +69,6 @@ export default { class="modal fade" tabindex="-1" role="dialog" - @transitionend="opened" > <div :class="modalSizeClass" diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 442aef124d3..58ed5bf6455 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1377,6 +1377,7 @@ .ide-entry-dropdown-toggle { padding: $gl-padding-4; + color: $gl-text-color; background-color: $theme-gray-100; &:hover { @@ -1389,6 +1390,10 @@ background-color: $blue-500; outline: 0; } + + svg { + fill: currentColor; + } } .ide-new-btn .dropdown.show .ide-entry-dropdown-toggle { diff --git a/changelogs/unreleased/ide-rename-files.yml b/changelogs/unreleased/ide-rename-files.yml new file mode 100644 index 00000000000..c2db284e07c --- /dev/null +++ b/changelogs/unreleased/ide-rename-files.yml @@ -0,0 +1,5 @@ +--- +title: Enable renaming files and folders in Web IDE +merge_request: 20835 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2d700fe8773..dd3c8692954 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4378,6 +4378,15 @@ msgstr "" msgid "Remove project" msgstr "" +msgid "Rename" +msgstr "" + +msgid "Rename file" +msgstr "" + +msgid "Rename folder" +msgstr "" + msgid "Reply to this email directly or %{view_it_on_gitlab}." msgstr "" diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index 5cb8b177fc9..8a8cbd2cee4 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -75,7 +75,7 @@ describe('new dropdown component', () => { it('calls delete action', () => { spyOn(vm, 'deleteEntry'); - vm.$el.querySelectorAll('.dropdown-menu button')[3].click(); + vm.$el.querySelectorAll('.dropdown-menu button')[4].click(); expect(vm.deleteEntry).toHaveBeenCalledWith(''); }); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index 70651535e87..595a2f927e9 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -15,7 +15,7 @@ describe('new file modal component', () => { describe(type, () => { beforeEach(() => { const store = createStore(); - store.state.newEntryModal = { + store.state.entryModal = { type, path: '', }; @@ -45,7 +45,7 @@ describe('new file modal component', () => { it('$emits create', () => { spyOn(vm, 'createTempEntry'); - vm.createEntryInStore(); + vm.submitForm(); expect(vm.createTempEntry).toHaveBeenCalledWith({ name: 'testing', @@ -55,4 +55,47 @@ describe('new file modal component', () => { }); }); }); + + describe('rename entry', () => { + beforeEach(() => { + const store = createStore(); + store.state.entryModal = { + type: 'rename', + path: '', + entry: { + name: 'test', + type: 'blob', + }, + }; + + vm = createComponentWithStore(Component, store).$mount(); + }); + + ['tree', 'blob'].forEach(type => { + it(`renders title and button for renaming ${type}`, done => { + const text = type === 'tree' ? 'folder' : 'file'; + + vm.$store.state.entryModal.entry.type = type; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Rename ${text}`); + expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Rename ${text}`); + + done(); + }); + }); + }); + + describe('entryName', () => { + it('returns entries name', () => { + expect(vm.entryName).toBe('test'); + }); + + it('updated name', () => { + vm.name = 'index.js'; + + expect(vm.entryName).toBe('index.js'); + }); + }); + }); }); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 792a716565c..d84f1717a61 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -8,6 +8,7 @@ import actions, { updateTempFlagForEntry, setErrorMessage, deleteEntry, + renameEntry, } from '~/ide/stores/actions'; import store from '~/ide/stores'; import * as types from '~/ide/stores/mutation_types'; @@ -468,7 +469,61 @@ describe('Multi-file store actions', () => { 'path', store.state, [{ type: types.DELETE_ENTRY, payload: 'path' }], - [{ type: 'burstUnusedSeal' }, { type: 'closeFile', payload: store.state.entries.path }], + [{ type: 'burstUnusedSeal' }], + done, + ); + }); + }); + + describe('renameEntry', () => { + it('renames entry', done => { + store.state.entries.test = { + tree: [], + }; + + testAction( + renameEntry, + { path: 'test', name: 'new-name' }, + store.state, + [ + { + type: types.RENAME_ENTRY, + payload: { path: 'test', name: 'new-name', entryPath: null }, + }, + ], + [{ type: 'deleteEntry', payload: 'test' }], + done, + ); + }); + + it('renames all entries in tree', done => { + store.state.entries.test = { + type: 'tree', + tree: [ + { + path: 'tree-1', + }, + { + path: 'tree-2', + }, + ], + }; + + testAction( + renameEntry, + { path: 'test', name: 'new-name' }, + store.state, + [ + { + type: types.RENAME_ENTRY, + payload: { path: 'test', name: 'new-name', entryPath: null }, + }, + ], + [ + { type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-1' } }, + { type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-2' } }, + { type: 'deleteEntry', payload: 'test' }, + ], done, ); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 133ad627f34..24a7d76f30b 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -294,9 +294,10 @@ describe('IDE commit module actions', () => { { action: 'update', file_path: jasmine.anything(), - content: jasmine.anything(), + content: undefined, encoding: jasmine.anything(), last_commit_id: undefined, + previous_path: undefined, }, ], start_branch: 'master', @@ -320,9 +321,10 @@ describe('IDE commit module actions', () => { { action: 'update', file_path: jasmine.anything(), - content: jasmine.anything(), + content: undefined, encoding: jasmine.anything(), last_commit_id: '123456789', + previous_path: undefined, }, ], start_branch: undefined, diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 8b5f2d0bdfa..1e836dbc3f9 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -206,6 +206,7 @@ describe('Multi-file store mutations', () => { it('adds to changedFiles', () => { localState.entries.filePath = { deleted: false, + type: 'blob', }; mutations.DELETE_ENTRY(localState, 'filePath'); @@ -213,4 +214,103 @@ describe('Multi-file store mutations', () => { expect(localState.changedFiles).toEqual([localState.entries.filePath]); }); }); + + describe('UPDATE_FILE_AFTER_COMMIT', () => { + it('updates URLs if prevPath is set', () => { + const f = { + ...file(), + path: 'test', + prevPath: 'testing-123', + rawPath: `${gl.TEST_HOST}/testing-123`, + permalink: `${gl.TEST_HOST}/testing-123`, + commitsPath: `${gl.TEST_HOST}/testing-123`, + blamePath: `${gl.TEST_HOST}/testing-123`, + }; + localState.entries.test = f; + localState.changedFiles.push(f); + + mutations.UPDATE_FILE_AFTER_COMMIT(localState, { file: f, lastCommit: { commit: {} } }); + + expect(f.rawPath).toBe(`${gl.TEST_HOST}/test`); + expect(f.permalink).toBe(`${gl.TEST_HOST}/test`); + expect(f.commitsPath).toBe(`${gl.TEST_HOST}/test`); + expect(f.blamePath).toBe(`${gl.TEST_HOST}/test`); + }); + }); + + describe('OPEN_NEW_ENTRY_MODAL', () => { + it('sets entryModal', () => { + localState.entries.testPath = { + ...file(), + }; + + mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' }); + + expect(localState.entryModal).toEqual({ + type: 'test', + path: 'testPath', + entry: localState.entries.testPath, + }); + }); + }); + + describe('RENAME_ENTRY', () => { + beforeEach(() => { + localState.trees = { + 'gitlab-ce/master': { tree: [] }, + }; + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.entries.oldPath = { + ...file(), + type: 'blob', + name: 'oldPath', + path: 'oldPath', + url: `${gl.TEST_HOST}/oldPath`, + }; + }); + + it('creates new renamed entry', () => { + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.entries.newPath).toEqual({ + ...localState.entries.oldPath, + id: 'newPath', + name: 'newPath', + key: 'newPath-blob-name', + path: 'newPath', + tempFile: true, + prevPath: 'oldPath', + tree: [], + parentPath: '', + url: `${gl.TEST_HOST}/newPath`, + moved: jasmine.anything(), + movedPath: jasmine.anything(), + }); + }); + + it('adds new entry to changedFiles', () => { + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.changedFiles.length).toBe(1); + expect(localState.changedFiles[0].path).toBe('newPath'); + }); + + it('sets oldEntry as moved', () => { + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.entries.oldPath.moved).toBe(true); + }); + + it('adds to parents tree', () => { + localState.entries.oldPath.parentPath = 'parentPath'; + localState.entries.parentPath = { + ...file(), + }; + + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.entries.parentPath.tree.length).toBe(1); + }); + }); }); diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index 89db50b8874..9f18034f8a3 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -112,6 +112,7 @@ describe('Multi-file store utils', () => { content: 'updated file content', encoding: 'text', last_commit_id: '123456789', + previous_path: undefined, }, { action: 'create', @@ -119,13 +120,15 @@ describe('Multi-file store utils', () => { content: 'new file content', encoding: 'base64', last_commit_id: '123456789', + previous_path: undefined, }, { action: 'delete', file_path: 'deletedFile', - content: '', + content: undefined, encoding: 'text', last_commit_id: undefined, + previous_path: undefined, }, ], start_branch: undefined, @@ -172,6 +175,7 @@ describe('Multi-file store utils', () => { content: 'updated file content', encoding: 'text', last_commit_id: '123456789', + previous_path: undefined, }, { action: 'create', @@ -179,6 +183,7 @@ describe('Multi-file store utils', () => { content: 'new file content', encoding: 'base64', last_commit_id: '123456789', + previous_path: undefined, }, ], start_branch: undefined, @@ -195,13 +200,17 @@ describe('Multi-file store utils', () => { expect(utils.commitActionForFile({ tempFile: true })).toBe('create'); }); + it('returns move for moved file', () => { + expect(utils.commitActionForFile({ prevPath: 'test' })).toBe('move'); + }); + it('returns update by default', () => { expect(utils.commitActionForFile({})).toBe('update'); }); }); describe('getCommitFiles', () => { - it('returns flattened list of files and folders', () => { + it('returns list of files excluding moved files', () => { const files = [ { path: 'a', @@ -209,19 +218,9 @@ describe('Multi-file store utils', () => { deleted: true, }, { - path: 'b', - type: 'tree', - deleted: true, - tree: [ - { - path: 'c', - type: 'blob', - }, - { - path: 'd', - type: 'blob', - }, - ], + path: 'c', + type: 'blob', + moved: true, }, ]; @@ -233,16 +232,6 @@ describe('Multi-file store utils', () => { type: 'blob', deleted: true, }, - { - path: 'c', - type: 'blob', - deleted: true, - }, - { - path: 'd', - type: 'blob', - deleted: true, - }, ]); }); }); diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js index e4737714312..263824a102a 100644 --- a/spec/javascripts/vue_shared/components/gl_modal_spec.js +++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js @@ -29,7 +29,7 @@ describe('GlModal', () => { describe('without id', () => { beforeEach(() => { - vm = mountComponent(modalComponent, { }); + vm = mountComponent(modalComponent, {}); }); it('does not add an id attribute to the modal', () => { @@ -83,7 +83,7 @@ describe('GlModal', () => { }); }); - it('works with data-toggle="modal"', (done) => { + it('works with data-toggle="modal"', done => { setFixtures(` <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> <div id="modal-container"></div> @@ -91,9 +91,13 @@ describe('GlModal', () => { const modalContainer = document.getElementById('modal-container'); const modalButton = document.getElementById('modal-button'); - vm = mountComponent(modalComponent, { - id: 'my-modal', - }, modalContainer); + vm = mountComponent( + modalComponent, + { + id: 'my-modal', + }, + modalContainer, + ); $(vm.$el).on('shown.bs.modal', () => done()); modalButton.click(); @@ -103,7 +107,7 @@ describe('GlModal', () => { const dummyEvent = 'not really an event'; beforeEach(() => { - vm = mountComponent(modalComponent, { }); + vm = mountComponent(modalComponent, {}); spyOn(vm, '$emit'); }); @@ -122,11 +126,27 @@ describe('GlModal', () => { expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent); }); }); + + describe('opened', () => { + it('emits a open event', () => { + vm.opened(); + + expect(vm.$emit).toHaveBeenCalledWith('open'); + }); + }); + + describe('closed', () => { + it('emits a closed event', () => { + vm.closed(); + + expect(vm.$emit).toHaveBeenCalledWith('closed'); + }); + }); }); describe('slots', () => { const slotContent = 'this should go into the slot'; - const modalWithSlot = (slotName) => { + const modalWithSlot = slotName => { let template; if (slotName) { template = ` |