diff options
36 files changed, 545 insertions, 116 deletions
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue index a4e06bbbe3c..720ae11aaa6 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; +import { getCommitIconMap } from '../utils'; export default { components: { @@ -34,16 +35,14 @@ export default { }, computed: { changedIcon() { - const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; - return this.file.tempFile && !this.forceModifiedIcon - ? `file-addition${suffix}` - : `file-modified${suffix}`; - }, - stagedIcon() { - return `${this.changedIcon}-solid`; + const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : ''; + + if (this.forceModifiedIcon) return `file-modified${suffix}`; + + return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { - return `multi-${this.changedIcon} float-left`; + return `ide-${this.changedIcon} float-left`; }, tooltipTitle() { if (!this.showTooltip) return undefined; @@ -66,6 +65,9 @@ export default { return undefined; }, + showIcon() { + return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted; + }, }, }; </script> @@ -79,7 +81,7 @@ export default { class="ide-file-changed-icon" > <icon - v-if="file.changed || file.tempFile || file.staged" + v-if="showIcon" :name="changedIcon" :size="12" :css-classes="changedIconClass" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index ee21eeda3cd..391004dcd3c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; import { viewerTypes } from '../../constants'; +import { getCommitIconMap } from '../../utils'; export default { components: { @@ -42,11 +43,12 @@ export default { }, computed: { iconName() { - const prefix = this.stagedList ? '-solid' : ''; - return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`; + const suffix = this.stagedList ? '-solid' : ''; + + return `${getCommitIconMap(this.file).icon}${suffix}`; }, iconClass() { - return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; + return `${getCommitIconMap(this.file).class} append-right-8`; }, fullKey() { return `${this.keyPrefix}-${this.file.key}`; @@ -67,6 +69,8 @@ export default { 'stageChange', ]), openFileInEditor() { + if (this.file.type === 'tree') return null; + return this.openPendingTab({ file: this.file, keyPrefix: this.keyPrefix, diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index f9978762c45..d09c99050fe 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -10,7 +10,7 @@ export default { EditorModeDropdown, }, computed: { - ...mapGetters(['currentMergeRequest']), + ...mapGetters(['currentMergeRequest', 'activeFile']), ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; @@ -23,12 +23,20 @@ export default { }, }, mounted() { + if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) { + this.$router.push(`/project${this.activeFile.url}`, () => { + this.updateViewer('editor'); + }); + } else if (this.activeFile && this.activeFile.deleted) { + this.resetOpenFiles(); + } + this.$nextTick(() => { this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); }); }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'resetOpenFiles']), }, }; </script> @@ -36,7 +44,6 @@ export default { <template> <ide-tree-list :viewer-type="viewer" - :disable-action-dropdown="true" header-class="ide-review-header" > <template diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 0a95c0bb30d..e996dd9059e 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -17,14 +17,18 @@ export default { ...mapGetters(['currentProject', 'currentTree', 'activeFile']), }, mounted() { - if (this.activeFile && this.activeFile.pending) { + if (!this.activeFile) return; + + if (this.activeFile.pending && !this.activeFile.deleted) { this.$router.push(`/project${this.activeFile.url}`, () => { this.updateViewer('editor'); }); + } else if (this.activeFile.deleted) { + this.resetOpenFiles(); } }, methods: { - ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']), + ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']), }, }; </script> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 0df99798d21..2e7226b727c 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -22,11 +22,6 @@ export default { required: false, default: null, }, - disableActionDropdown: { - type: Boolean, - required: false, - default: false, - }, }, computed: { ...mapState(['currentBranchId']), @@ -69,7 +64,6 @@ export default { :key="file.key" :file="file" :level="0" - :disable-action-dropdown="disableActionDropdown" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index c29e49ba766..440e480d596 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -13,7 +13,7 @@ export default { ItemButton, }, props: { - branch: { + type: { type: String, required: true, }, @@ -45,7 +45,7 @@ export default { }, }, methods: { - ...mapActions(['createTempEntry', 'openNewEntryModal']), + ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']), createNewItem(type) { this.openNewEntryModal({ type, path: this.path }); this.dropdownOpen = false; @@ -82,28 +82,40 @@ export default { ref="dropdownMenu" class="dropdown-menu dropdown-menu-right" > + <template v-if="type === 'tree'"> + <li> + <item-button + :label="__('New file')" + class="d-flex" + icon="doc-new" + icon-classes="mr-2" + @click="createNewItem('blob')" + /> + </li> + <li> + <upload + :path="path" + @create="createTempEntry" + /> + </li> + <li> + <item-button + :label="__('New directory')" + class="d-flex" + icon="folder-new" + icon-classes="mr-2" + @click="createNewItem('tree')" + /> + </li> + <li class="divider"></li> + </template> <li> <item-button - :label="__('New file')" + :label="__('Delete')" class="d-flex" - icon="doc-new" + icon="remove" icon-classes="mr-2" - @click="createNewItem('blob')" - /> - </li> - <li> - <upload - :path="path" - @create="createTempEntry" - /> - </li> - <li> - <item-button - :label="__('New directory')" - class="d-flex" - icon="folder-new" - icon-classes="mr-2" - @click="createNewItem('tree')" + @click="deleteEntry(path)" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 50ab242ba2a..6f1a941fbc4 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -44,7 +44,7 @@ export default { }, }, mounted() { - if (this.lastOpenedFile) { + if (this.lastOpenedFile && this.lastOpenedFile.type !== 'tree') { this.openPendingTab({ file: this.lastOpenedFile, keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 08ee12fd98f..f9badb01535 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -87,7 +87,9 @@ export default { this.editor.updateDimensions(); }, viewer() { - this.createEditorInstance(); + if (!this.file.pending) { + this.createEditorInstance(); + } }, panelResizing() { if (!this.panelResizing) { @@ -109,6 +111,7 @@ export default { }, methods: { ...mapActions([ + 'getFileData', 'getRawFileData', 'changeFileContent', 'setFileLanguage', @@ -123,10 +126,16 @@ export default { this.editor.clearEditor(); - this.getRawFileData({ + this.getFileData({ path: this.file.path, - baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', + makeFileActive: false, }) + .then(() => + this.getRawFileData({ + path: this.file.path, + baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', + }), + ) .then(() => { this.createEditorInstance(); }) @@ -246,6 +255,8 @@ export default { ref="editor" :class="{ 'is-readonly': isCommitModeActive, + 'is-deleted': file.deleted, + 'is-added': file.tempFile }" class="multi-file-editor-holder" > diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 3b4dd5ae9aa..eb4a927fe0d 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -34,11 +34,6 @@ export default { type: Number, required: true, }, - disableActionDropdown: { - type: Boolean, - required: false, - default: false, - }, }, data() { return { @@ -212,8 +207,7 @@ export default { /> </span> <new-dropdown - v-if="isTree && !disableActionDropdown" - :project-id="file.projectId" + :type="file.type" :branch="file.branchId" :path="file.path" :mouse-over="mouseOver" diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 03772ae4a4c..db47b75ec5c 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -37,7 +37,7 @@ export default { return this.fileHasChanged ? !this.tabMouseOver : false; }, fileHasChanged() { - return this.tab.changed || this.tab.tempFile || this.tab.staged; + return this.tab.changed || this.tab.tempFile || this.tab.staged || this.tab.deleted; }, }, @@ -71,7 +71,8 @@ export default { <template> <li :class="{ - active: tab.active + active: tab.active, + disabled: tab.pending }" @click="clickFile(tab)" @mouseover="mouseOverTab" @@ -105,7 +106,6 @@ export default { <changed-file-icon v-else :file="tab" - :force-modified-icon="true" /> </button> </li> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 45d36f6f42c..0b514f31467 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -38,3 +38,18 @@ export const stageKeys = { unstaged: 'unstaged', staged: 'staged', }; + +export const commitItemIconMap = { + addition: { + icon: 'file-addition', + class: 'ide-file-addition', + }, + modified: { + icon: 'file-modified', + class: 'ide-file-modified', + }, + deleted: { + icon: 'file-deletion', + class: 'ide-file-deletion', + }, +}; diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index 78e6f632728..60bddb34977 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -7,7 +7,7 @@ export default class Model { this.disposable = new Disposable(); this.file = file; this.head = head; - this.content = file.content !== '' ? file.content : file.raw; + this.content = file.content !== '' || file.deleted ? file.content : file.raw; this.disposable.add( (this.originalModel = monacoEditor.createModel( diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index b5bd6f5a6bb..2765acada48 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { $('#ide-new-entry').modal('show'); }; +export const deleteEntry = ({ commit, dispatch, state }, path) => { + dispatch('burstUnusedSeal'); + dispatch('closeFile', state.entries[path]); + commit(types.DELETE_ENTRY, path); +}; + +export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); + 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 6c0887e11ee..b343750f789 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -61,7 +61,11 @@ 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(); + commit(types.TOGGLE_LOADING, { entry: file }); + return service .getFileData( `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, @@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); commit(types.SET_FILE_DATA, { data, file }); - commit(types.TOGGLE_FILE_OPEN, path); + if (makeFileActive) commit(types.TOGGLE_FILE_OPEN, path); if (makeFileActive) dispatch('setFileActive', path); commit(types.TOGGLE_LOADING, { entry: file }); }) @@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = service .getRawFileData(file) .then(raw => { - commit(types.SET_FILE_RAW_DATA, { file, raw }); + if (!file.tempFile) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (file.mrChange && file.mrChange.new_file === false) { service .getBaseRawFileData(file, baseSha) diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index ffaaaabff17..acb6ef5e6d4 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => { if (row.type === 'tree') { dispatch('toggleTreeOpen', row.path); - } else if (row.type === 'blob' && (row.opened || row.changed)) { - if (row.changed && !row.opened) { + } else if (row.type === 'blob') { + if (!row.opened) { commit(types.TOGGLE_FILE_OPEN, row.path); } dispatch('setFileActive', row.path); - } else { - dispatch('getFileData', { path: row.path }); } dispatch('showTreeEntry', row.path); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 7828c31f20e..462ca45db9b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo dispatch('updateActivityBarView', activityBarViews.edit, { root: true }); dispatch('updateViewer', 'editor', { root: true }); - router.push( - `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ - rootGetters.activeFile.path - }`, - ); + if (rootGetters.activeFile) { + router.push( + `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ + rootGetters.activeFile.path + }`, + ); + } } }) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 3db4b2f903e..03777e6c10b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -1,7 +1,15 @@ -import { sprintf, n__ } from '../../../../locale'; +import { sprintf, n__, __ } from '../../../../locale'; import * as consts from './constants'; const BRANCH_SUFFIX_COUNT = 5; +const createTranslatedTextForFiles = (files, text) => { + if (!files.length) return null; + + return sprintf(n__('%{text} %{files}', '%{text} %{files} files', files.length), { + files: files.reduce((acc, val) => acc.concat(val.path), []).join(', '), + text, + }); +}; export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; @@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => { export const preBuiltCommitMessage = (state, _, rootState) => { if (state.commitMessage) return state.commitMessage; - const files = (rootState.stagedFiles.length - ? rootState.stagedFiles - : rootState.changedFiles - ).reduce((acc, val) => acc.concat(val.path), []); + const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles; + const modifiedFiles = files.filter(f => !f.deleted); + const deletedFiles = files.filter(f => f.deleted); - return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), { - files: files.join(', '), - }); + return [ + createTranslatedTextForFiles(modifiedFiles, __('Update')), + createTranslatedTextForFiles(deletedFiles, __('Deleted')), + ] + .filter(t => t) + .join('\n'); }; // prevent babel-plugin-rewire from generating an invalid default during karma tests diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 8d6f9ccaf34..dae60f4d65a 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; +export const DELETE_ENTRY = 'DELETE_ENTRY'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index f8091f5b5e0..799c2f51e8d 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; import projectMutations from './mutations/project'; import mergeRequestMutation from './mutations/merge_request'; @@ -171,6 +172,16 @@ export default { newEntryModal: { type, path }, }); }, + [types.DELETE_ENTRY](state, path) { + const entry = state.entries[path]; + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : 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); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 46547820425..9a87d50d6d5 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; +import { sortTree } from '../utils'; import { diffModes } from '../../constants'; export default { @@ -51,9 +52,17 @@ export default { }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { + const openPendingFile = state.openFiles.find( + f => f.path === file.path && f.pending && !f.tempFile, + ); + Object.assign(state.entries[file.path], { raw, }); + + if (openPendingFile) { + openPendingFile.raw = raw; + } }, [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { Object.assign(state.entries[file.path], { @@ -109,11 +118,22 @@ 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; Object.assign(state.entries[path], { content: stagedFile ? stagedFile.content : state.entries[path].raw, changed: false, + deleted: false, }); + + if (deleted) { + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + + parent.tree = sortTree(parent.tree.concat(entry)); + } }, [types.ADD_FILE_TO_CHANGED](state, path) { Object.assign(state, { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 9e6b86dd844..bf7ab93ff5e 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -46,6 +46,7 @@ export const dataStructure = () => ({ parentPath: null, lastOpenedAt: 0, mrChange: null, + deleted: false, }); export const decorateData = entity => { @@ -105,15 +106,37 @@ export const setPageTitle = title => { document.title = title; }; +export const commitActionForFile = file => { + if (file.deleted) { + return 'delete'; + } else if (file.tempFile) { + return 'create'; + } + + return 'update'; +}; + +export const getCommitFiles = (stagedFiles, deleteTree = false) => + stagedFiles.reduce((acc, file) => { + if ((file.deleted || deleteTree) && file.type === 'tree') { + return acc.concat(getCommitFiles(file.tree, true)); + } + + return acc.concat({ + ...file, + deleted: deleteTree || file.deleted, + }); + }, []); + export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({ branch, commit_message: state.commitMessage || getters.preBuiltCommitMessage, - actions: rootState.stagedFiles.map(f => ({ - action: f.tempFile ? 'create' : 'update', + actions: getCommitFiles(rootState.stagedFiles).map(f => ({ + action: commitActionForFile(f), file_path: f.path, content: f.content, encoding: f.base64 ? 'base64' : 'text', - last_commit_id: newBranch ? undefined : f.lastCommitSha, + last_commit_id: newBranch || f.deleted ? undefined : f.lastCommitSha, })), start_branch: newBranch ? rootState.currentBranchId : undefined, }); diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js new file mode 100644 index 00000000000..92b15cf232d --- /dev/null +++ b/app/assets/javascripts/ide/utils.js @@ -0,0 +1,12 @@ +import { commitItemIconMap } from './constants'; + +// eslint-disable-next-line import/prefer-default-export +export const getCommitIconMap = file => { + if (file.deleted) { + return commitItemIconMap.deleted; + } else if (file.tempFile) { + return commitItemIconMap.addition; + } + + return commitItemIconMap.modified; +}; diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 442a5e07a86..a346bd04ad3 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -77,6 +77,7 @@ .ide-file-icon-holder { display: flex; align-items: center; + color: $theme-gray-700; } .ide-file-changed-icon { @@ -164,12 +165,23 @@ background-color: $white-light; border-bottom-color: $white-light; } + + &:not(.disabled) { + .multi-file-tab { + cursor: pointer; + } + } + + &.disabled { + .multi-file-tab-close { + cursor: default; + } + } } } .multi-file-tab { @include str-truncated(141px); - cursor: pointer; svg { vertical-align: middle; @@ -244,6 +256,38 @@ } } + .is-deleted { + .editor.modified { + .margin-view-overlays, + .lines-content, + .decorationsOverviewRuler { + // !important to override monaco inline styles + display: none !important; + } + } + + .diffOverviewRuler.modified { + // !important to override monaco inline styles + display: none !important; + } + } + + .is-added { + .editor.original { + .margin-view-overlays, + .lines-content, + .decorationsOverviewRuler { + // !important to override monaco inline styles + display: none !important; + } + } + + .diffOverviewRuler.original { + // !important to override monaco inline styles + display: none !important; + } + } + .monaco-diff-editor.vs { .editor.modified { box-shadow: none; @@ -560,16 +604,21 @@ } } -.multi-file-addition, -.multi-file-addition-solid { +.ide-file-addition, +.ide-file-addition-solid { color: $green-500; } -.multi-file-modified, -.multi-file-modified-solid { +.ide-file-modified, +.ide-file-modified-solid { color: $orange-500; } +.ide-file-deletion, +.ide-file-deletion-solid { + color: $red-500; +} + .multi-file-commit-list-collapsed { display: flex; flex-direction: column; @@ -1017,6 +1066,10 @@ .ide-new-btn { margin-left: auto; } + + button { + color: $gl-text-color; + } } .ide-sidebar-branch-title { diff --git a/changelogs/unreleased/ide-delete-entries.yml b/changelogs/unreleased/ide-delete-entries.yml new file mode 100644 index 00000000000..8cbc0739406 --- /dev/null +++ b/changelogs/unreleased/ide-delete-entries.yml @@ -0,0 +1,5 @@ +--- +title: Enabled deletion of files in the Web IDE +merge_request: +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a5bbe8938ff..deeeea90dd0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -122,6 +122,11 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts msgstr[0] "" msgstr[1] "" +msgid "%{text} %{files}" +msgid_plural "%{text} %{files} files" +msgstr[0] "" +msgstr[1] "" + msgid "%{text} is available" msgstr "" @@ -1992,6 +1997,9 @@ msgstr "" msgid "Delete list" msgstr "" +msgid "Deleted" +msgstr "" + msgid "Deny" msgstr "" @@ -5537,10 +5545,8 @@ msgstr "" msgid "Up to date" msgstr "" -msgid "Update %{files}" -msgid_plural "Update %{files} files" -msgstr[0] "" -msgstr[1] "" +msgid "Update" +msgstr "" msgid "Update your group name, description, avatar, and other general settings." msgstr "" diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js index 541864e912e..7308219f705 100644 --- a/spec/javascripts/ide/components/changed_file_icon_spec.js +++ b/spec/javascripts/ide/components/changed_file_icon_spec.js @@ -33,14 +33,14 @@ describe('IDE changed file icon', () => { }); describe('changedIconClass', () => { - it('includes multi-file-modified when not a temp file', () => { - expect(vm.changedIconClass).toContain('multi-file-modified'); + it('includes ide-file-modified when not a temp file', () => { + expect(vm.changedIconClass).toContain('ide-file-modified'); }); - it('includes multi-file-addition when a temp file', () => { + it('includes ide-file-addition when a temp file', () => { vm.file.tempFile = true; - expect(vm.changedIconClass).toContain('multi-file-addition'); + expect(vm.changedIconClass).toContain('ide-file-addition'); }); }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index bf96170f703..41d8bfff7e7 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -76,17 +76,29 @@ describe('Multi-file editor commit sidebar list item', () => { expect(vm.iconName).toBe('file-addition'); }); + + it('returns deletion', () => { + f.deleted = true; + + expect(vm.iconName).toBe('file-deletion'); + }); }); describe('iconClass', () => { it('returns modified when not a tempFile', () => { - expect(vm.iconClass).toContain('multi-file-modified'); + expect(vm.iconClass).toContain('ide-file-modified'); }); it('returns addition when not a tempFile', () => { f.tempFile = true; - expect(vm.iconClass).toContain('multi-file-addition'); + expect(vm.iconClass).toContain('ide-file-addition'); + }); + + it('returns deletion', () => { + f.deleted = true; + + expect(vm.iconClass).toContain('ide-file-deletion'); }); }); }); diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index 4d704b80209..092c405a70b 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -14,6 +14,7 @@ describe('new dropdown component', () => { branch: 'master', path: '', mouseOver: false, + type: 'tree', }); vm.$store.state.currentProjectId = 'abcproject'; @@ -67,4 +68,14 @@ describe('new dropdown component', () => { }); }); }); + + describe('delete entry', () => { + it('calls delete action', () => { + spyOn(vm, 'deleteEntry'); + + vm.$el.querySelectorAll('.dropdown-menu button')[3].click(); + + expect(vm.deleteEntry).toHaveBeenCalledWith(''); + }); + }); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index 2256deb7dac..0e2e246defd 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; +import '~/behaviors/markdown/render_gfm'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; import repoEditor from '~/ide/components/repo_editor.vue'; @@ -25,6 +26,8 @@ describe('RepoEditor', () => { vm.$store.state.openFiles.push(f); Vue.set(vm.$store.state.entries, f.path, f); + spyOn(vm, 'getFileData').and.returnValue(Promise.resolve()); + vm.$mount(); Vue.nextTick(() => setTimeout(done)); diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js index 156233653ab..f99d1f9890a 100644 --- a/spec/javascripts/ide/components/repo_file_spec.js +++ b/spec/javascripts/ide/components/repo_file_spec.js @@ -91,25 +91,6 @@ describe('RepoFile', () => { done(); }); }); - - it('disables action dropdown', done => { - createComponent({ - file: { - ...file('t4'), - type: 'tree', - branchId: 'master', - projectId: 'project', - }, - level: 0, - disableActionDropdown: true, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.ide-new-btn')).toBeNull(); - - done(); - }); - }); }); describe('locked file', () => { diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js index fc0695a4263..278a0753322 100644 --- a/spec/javascripts/ide/components/repo_tab_spec.js +++ b/spec/javascripts/ide/components/repo_tab_spec.js @@ -93,13 +93,13 @@ describe('RepoTab', () => { Vue.nextTick() .then(() => { - expect(vm.$el.querySelector('.multi-file-modified')).toBeNull(); + expect(vm.$el.querySelector('.ide-file-modified')).toBeNull(); vm.$el.dispatchEvent(new Event('mouseout')); }) .then(Vue.nextTick) .then(() => { - expect(vm.$el.querySelector('.multi-file-modified')).not.toBeNull(); + expect(vm.$el.querySelector('.ide-file-modified')).not.toBeNull(); done(); }) diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 8b665a6d79e..792a716565c 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -7,6 +7,7 @@ import actions, { updateActivityBarView, updateTempFlagForEntry, setErrorMessage, + deleteEntry, } from '~/ide/stores/actions'; import store from '~/ide/stores'; import * as types from '~/ide/stores/mutation_types'; @@ -457,4 +458,19 @@ describe('Multi-file store actions', () => { ); }); }); + + describe('deleteEntry', () => { + it('commits entry deletion', done => { + store.state.entries.path = 'testing'; + + testAction( + deleteEntry, + 'path', + store.state, + [{ type: types.DELETE_ENTRY, payload: 'path' }], + [{ type: 'burstUnusedSeal' }, { type: 'closeFile', payload: store.state.entries.path }], + done, + ); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index 44c941d6dbb..3f4bf407a1f 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -123,6 +123,22 @@ describe('IDE commit module getters', () => { 'Update test-file, index.js files', ); }); + + it('returns commitMessage with deleted files', () => { + rootState[key].push( + { + path: 'test-file', + deleted: true, + }, + { + path: 'index.js', + }, + ); + + expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe( + 'Update index.js\nDeleted test-file', + ); + }); }); }); }); diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index 52f83be8e8c..efd0d86552b 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -94,6 +94,35 @@ describe('IDE store file mutations', () => { expect(localFile.raw).toBe('testing'); }); + + it('adds raw data to open pending file', () => { + localState.openFiles.push({ + ...localFile, + pending: true, + }); + + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localState.openFiles[0].raw).toBe('testing'); + }); + + it('does not add raw data to open pending tempFile file', () => { + localState.openFiles.push({ + ...localFile, + pending: true, + tempFile: true, + }); + + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localState.openFiles[0].raw).not.toBe('testing'); + }); }); describe('SET_FILE_BASE_RAW_DATA', () => { @@ -205,6 +234,11 @@ describe('IDE store file mutations', () => { beforeEach(() => { localFile.content = 'test'; localFile.changed = true; + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.trees['gitlab-ce/master'] = { + tree: [], + }; }); it('resets content and changed', () => { @@ -213,6 +247,36 @@ describe('IDE store file mutations', () => { expect(localFile.content).toBe(''); expect(localFile.changed).toBeFalsy(); }); + + it('adds to root tree if deleted', () => { + localFile.deleted = true; + + mutations.DISCARD_FILE_CHANGES(localState, localFile.path); + + expect(localState.trees['gitlab-ce/master'].tree).toEqual([ + { + ...localFile, + deleted: false, + }, + ]); + }); + + it('adds to parent tree if deleted', () => { + localFile.deleted = true; + localFile.parentPath = 'parentPath'; + localState.entries.parentPath = { + tree: [], + }; + + mutations.DISCARD_FILE_CHANGES(localState, localFile.path); + + expect(localState.entries.parentPath.tree).toEqual([ + { + ...localFile, + deleted: false, + }, + ]); + }); }); describe('ADD_FILE_TO_CHANGED', () => { diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 98016f593aa..8b5f2d0bdfa 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -156,4 +156,61 @@ describe('Multi-file store mutations', () => { expect(localState.errorMessage).toBe('error'); }); }); + + describe('DELETE_ENTRY', () => { + beforeEach(() => { + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.trees['gitlab-ce/master'] = { + tree: [], + }; + }); + + it('sets deleted flag', () => { + localState.entries.filePath = { + deleted: false, + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.entries.filePath.deleted).toBe(true); + }); + + it('removes from root tree', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + }; + localState.trees['gitlab-ce/master'].tree.push(localState.entries.filePath); + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.trees['gitlab-ce/master'].tree).toEqual([]); + }); + + it('removes from parent tree', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + parentPath: 'parentPath', + }; + localState.entries.parentPath = { + tree: [localState.entries.filePath], + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.entries.parentPath.tree).toEqual([]); + }); + + it('adds to changedFiles', () => { + localState.entries.filePath = { + deleted: false, + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([localState.entries.filePath]); + }); + }); }); diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index 6c5980cfae4..89db50b8874 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -86,6 +86,11 @@ describe('Multi-file store utils', () => { base64: true, lastCommitSha: '123456789', }, + { + ...file('deletedFile'), + path: 'deletedFile', + deleted: true, + }, ], currentBranchId: 'master', }; @@ -115,6 +120,13 @@ describe('Multi-file store utils', () => { encoding: 'base64', last_commit_id: '123456789', }, + { + action: 'delete', + file_path: 'deletedFile', + content: '', + encoding: 'text', + last_commit_id: undefined, + }, ], start_branch: undefined, }); @@ -173,4 +185,65 @@ describe('Multi-file store utils', () => { }); }); }); + + describe('commitActionForFile', () => { + it('returns deleted for deleted file', () => { + expect(utils.commitActionForFile({ deleted: true })).toBe('delete'); + }); + + it('returns create for tempFile', () => { + expect(utils.commitActionForFile({ tempFile: true })).toBe('create'); + }); + + it('returns update by default', () => { + expect(utils.commitActionForFile({})).toBe('update'); + }); + }); + + describe('getCommitFiles', () => { + it('returns flattened list of files and folders', () => { + const files = [ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'b', + type: 'tree', + deleted: true, + tree: [ + { + path: 'c', + type: 'blob', + }, + { + path: 'd', + type: 'blob', + }, + ], + }, + ]; + + const flattendFiles = utils.getCommitFiles(files); + + expect(flattendFiles).toEqual([ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'c', + type: 'blob', + deleted: true, + }, + { + path: 'd', + type: 'blob', + deleted: true, + }, + ]); + }); + }); }); |