Commit 2fbafe1a authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'ide-delete-entries' into 'master'

Enable deleting files in the Web IDE

See merge request gitlab-org/gitlab-ce!20595
parents 88738408 cded268c
...@@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; ...@@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility'; import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { getCommitIconMap } from '../utils';
export default { export default {
components: { components: {
...@@ -34,16 +35,14 @@ export default { ...@@ -34,16 +35,14 @@ export default {
}, },
computed: { computed: {
changedIcon() { changedIcon() {
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile && !this.forceModifiedIcon
? `file-addition${suffix}` if (this.forceModifiedIcon) return `file-modified${suffix}`;
: `file-modified${suffix}`;
}, return `${getCommitIconMap(this.file).icon}${suffix}`;
stagedIcon() {
return `${this.changedIcon}-solid`;
}, },
changedIconClass() { changedIconClass() {
return `multi-${this.changedIcon} float-left`; return `ide-${this.changedIcon} float-left`;
}, },
tooltipTitle() { tooltipTitle() {
if (!this.showTooltip) return undefined; if (!this.showTooltip) return undefined;
...@@ -66,6 +65,9 @@ export default { ...@@ -66,6 +65,9 @@ export default {
return undefined; return undefined;
}, },
showIcon() {
return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted;
},
}, },
}; };
</script> </script>
...@@ -79,7 +81,7 @@ export default { ...@@ -79,7 +81,7 @@ export default {
class="ide-file-changed-icon" class="ide-file-changed-icon"
> >
<icon <icon
v-if="file.changed || file.tempFile || file.staged" v-if="showIcon"
:name="changedIcon" :name="changedIcon"
:size="12" :size="12"
:css-classes="changedIconClass" :css-classes="changedIconClass"
......
...@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants'; import { viewerTypes } from '../../constants';
import { getCommitIconMap } from '../../utils';
export default { export default {
components: { components: {
...@@ -42,11 +43,12 @@ export default { ...@@ -42,11 +43,12 @@ export default {
}, },
computed: { computed: {
iconName() { iconName() {
const prefix = this.stagedList ? '-solid' : ''; const suffix = this.stagedList ? '-solid' : '';
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
return `${getCommitIconMap(this.file).icon}${suffix}`;
}, },
iconClass() { iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; return `${getCommitIconMap(this.file).class} append-right-8`;
}, },
fullKey() { fullKey() {
return `${this.keyPrefix}-${this.file.key}`; return `${this.keyPrefix}-${this.file.key}`;
...@@ -67,6 +69,8 @@ export default { ...@@ -67,6 +69,8 @@ export default {
'stageChange', 'stageChange',
]), ]),
openFileInEditor() { openFileInEditor() {
if (this.file.type === 'tree') return null;
return this.openPendingTab({ return this.openPendingTab({
file: this.file, file: this.file,
keyPrefix: this.keyPrefix, keyPrefix: this.keyPrefix,
......
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
EditorModeDropdown, EditorModeDropdown,
}, },
computed: { computed: {
...mapGetters(['currentMergeRequest']), ...mapGetters(['currentMergeRequest', 'activeFile']),
...mapState(['viewer', 'currentMergeRequestId']), ...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() { showLatestChangesText() {
return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
...@@ -23,12 +23,20 @@ export default { ...@@ -23,12 +23,20 @@ export default {
}, },
}, },
mounted() { 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.$nextTick(() => {
this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
}); });
}, },
methods: { methods: {
...mapActions(['updateViewer']), ...mapActions(['updateViewer', 'resetOpenFiles']),
}, },
}; };
</script> </script>
...@@ -36,7 +44,6 @@ export default { ...@@ -36,7 +44,6 @@ export default {
<template> <template>
<ide-tree-list <ide-tree-list
:viewer-type="viewer" :viewer-type="viewer"
:disable-action-dropdown="true"
header-class="ide-review-header" header-class="ide-review-header"
> >
<template <template
......
...@@ -17,14 +17,18 @@ export default { ...@@ -17,14 +17,18 @@ export default {
...mapGetters(['currentProject', 'currentTree', 'activeFile']), ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
}, },
mounted() { 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.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor'); this.updateViewer('editor');
}); });
} else if (this.activeFile.deleted) {
this.resetOpenFiles();
} }
}, },
methods: { methods: {
...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']), ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']),
}, },
}; };
</script> </script>
......
...@@ -22,11 +22,6 @@ export default { ...@@ -22,11 +22,6 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapState(['currentBranchId']), ...mapState(['currentBranchId']),
...@@ -69,7 +64,6 @@ export default { ...@@ -69,7 +64,6 @@ export default {
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
:disable-action-dropdown="disableActionDropdown"
/> />
</template> </template>
</div> </div>
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
ItemButton, ItemButton,
}, },
props: { props: {
branch: { type: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -45,7 +45,7 @@ export default { ...@@ -45,7 +45,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['createTempEntry', 'openNewEntryModal']), ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']),
createNewItem(type) { createNewItem(type) {
this.openNewEntryModal({ type, path: this.path }); this.openNewEntryModal({ type, path: this.path });
this.dropdownOpen = false; this.dropdownOpen = false;
...@@ -82,28 +82,40 @@ export default { ...@@ -82,28 +82,40 @@ export default {
ref="dropdownMenu" ref="dropdownMenu"
class="dropdown-menu dropdown-menu-right" 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> <li>
<item-button <item-button
:label="__('New file')" :label="__('Delete')"
class="d-flex" class="d-flex"
icon="doc-new" icon="remove"
icon-classes="mr-2" icon-classes="mr-2"
@click="createNewItem('blob')" @click="deleteEntry(path)"
/>
</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>
</ul> </ul>
......
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
}, },
}, },
mounted() { mounted() {
if (this.lastOpenedFile) { if (this.lastOpenedFile && this.lastOpenedFile.type !== 'tree') {
this.openPendingTab({ this.openPendingTab({
file: this.lastOpenedFile, file: this.lastOpenedFile,
keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged,
......
...@@ -87,7 +87,9 @@ export default { ...@@ -87,7 +87,9 @@ export default {
this.editor.updateDimensions(); this.editor.updateDimensions();
}, },
viewer() { viewer() {
this.createEditorInstance(); if (!this.file.pending) {
this.createEditorInstance();
}
}, },
panelResizing() { panelResizing() {
if (!this.panelResizing) { if (!this.panelResizing) {
...@@ -109,6 +111,7 @@ export default { ...@@ -109,6 +111,7 @@ export default {
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'getFileData',
'getRawFileData', 'getRawFileData',
'changeFileContent', 'changeFileContent',
'setFileLanguage', 'setFileLanguage',
...@@ -123,10 +126,16 @@ export default { ...@@ -123,10 +126,16 @@ export default {
this.editor.clearEditor(); this.editor.clearEditor();
this.getRawFileData({ this.getFileData({
path: this.file.path, 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(() => { .then(() => {
this.createEditorInstance(); this.createEditorInstance();
}) })
...@@ -246,6 +255,8 @@ export default { ...@@ -246,6 +255,8 @@ export default {
ref="editor" ref="editor"
:class="{ :class="{
'is-readonly': isCommitModeActive, 'is-readonly': isCommitModeActive,
'is-deleted': file.deleted,
'is-added': file.tempFile
}" }"
class="multi-file-editor-holder" class="multi-file-editor-holder"
> >
......
...@@ -34,11 +34,6 @@ export default { ...@@ -34,11 +34,6 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -212,8 +207,7 @@ export default { ...@@ -212,8 +207,7 @@ export default {
/> />
</span> </span>
<new-dropdown <new-dropdown
v-if="isTree && !disableActionDropdown" :type="file.type"
:project-id="file.projectId"
:branch="file.branchId" :branch="file.branchId"
:path="file.path" :path="file.path"
:mouse-over="mouseOver" :mouse-over="mouseOver"
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
return this.fileHasChanged ? !this.tabMouseOver : false; return this.fileHasChanged ? !this.tabMouseOver : false;
}, },
fileHasChanged() { 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 { ...@@ -71,7 +71,8 @@ export default {
<template> <template>
<li <li
:class="{ :class="{
active: tab.active active: tab.active,
disabled: tab.pending
}" }"
@click="clickFile(tab)" @click="clickFile(tab)"
@mouseover="mouseOverTab" @mouseover="mouseOverTab"
...@@ -105,7 +106,6 @@ export default { ...@@ -105,7 +106,6 @@ export default {
<changed-file-icon <changed-file-icon
v-else v-else
:file="tab" :file="tab"
:force-modified-icon="true"
/> />
</button> </button>
</li> </li>
......
...@@ -38,3 +38,18 @@ export const stageKeys = { ...@@ -38,3 +38,18 @@ export const stageKeys = {
unstaged: 'unstaged', unstaged: 'unstaged',
staged: 'staged', 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',
},
};
...@@ -7,7 +7,7 @@ export default class Model { ...@@ -7,7 +7,7 @@ export default class Model {
this.disposable = new Disposable(); this.disposable = new Disposable();
this.file = file; this.file = file;
this.head = head; 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.disposable.add(
(this.originalModel = monacoEditor.createModel( (this.originalModel = monacoEditor.createModel(
......
...@@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { ...@@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
$('#ide-new-entry').modal('show'); $('#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/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -61,7 +61,11 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -61,7 +61,11 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
const file = state.entries[path]; const file = state.entries[path];
if (file.raw || file.tempFile) return Promise.resolve();
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
return service return service
.getFileData( .getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
...@@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive ...@@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
commit(types.SET_FILE_DATA, { data, file }); 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); if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
}) })
...@@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = ...@@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
service service
.getRawFileData(file) .getRawFileData(file)
.then(raw => { .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) { if (file.mrChange && file.mrChange.new_file === false) {
service service
.getBaseRawFileData(file, baseSha) .getBaseRawFileData(file, baseSha)
......
...@@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => { ...@@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => {
export const handleTreeEntryAction = ({ commit, dispatch }, row) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') { if (row.type === 'tree') {
dispatch('toggleTreeOpen', row.path); dispatch('toggleTreeOpen', row.path);
} else if (row.type === 'blob' && (row.opened || row.changed)) { } else if (row.type === 'blob') {
if (row.changed && !row.opened) { if (!row.opened) {
commit(types.TOGGLE_FILE_OPEN, row.path); commit(types.TOGGLE_FILE_OPEN, row.path);
} }
dispatch('setFileActive', row.path); dispatch('setFileActive', row.path);
} else {
dispatch('getFileData', { path: row.path });
} }
dispatch('showTreeEntry', row.path); dispatch('showTreeEntry', row.path);
......
...@@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateActivityBarView', activityBarViews.edit, { root: true }); dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
dispatch('updateViewer', 'editor', { root: true }); dispatch('updateViewer', 'editor', { root: true });
router.push( if (rootGetters.activeFile) {
`/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ router.push(
rootGetters.activeFile.path `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${
}`, rootGetters.activeFile.path
); }`,
);
}
} }
}) })
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
......
import { sprintf, n__ } from '../../../../locale'; import { sprintf, n__, __ } from '../../../../locale';
import * as consts from './constants'; import * as consts from './constants';
const BRANCH_SUFFIX_COUNT = 5; 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 => export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading; state.commitMessage === '' || state.submitCommitLoading;
...@@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => { ...@@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => {
export const preBuiltCommitMessage = (state, _, rootState) => { export const preBuiltCommitMessage = (state, _, rootState) => {
if (state.commitMessage) return state.commitMessage; if (state.commitMessage) return state.commitMessage;
const files = (rootState.stagedFiles.length const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles;
? rootState.stagedFiles const modifiedFiles = files.filter(f => !f.deleted);
: rootState.changedFiles const deletedFiles = files.filter(f => f.deleted);
).reduce((acc, val) => acc.concat(val.path), []);
return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), { return [
files: files.join(', '), createTranslatedTextForFiles(modifiedFiles, __('Update')),
}); createTranslatedTextForFiles(deletedFiles, __('Deleted')),
]
.filter(t => t)
.join('\n');
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
......
...@@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; ...@@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY';
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request'; import mergeRequestMutation from './mutations/merge_request';
...@@ -171,6 +172,16 @@ export default { ...@@ -171,6 +172,16 @@ export default {
newEntryModal: { type, path }, 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, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { sortTree } from '../utils';
import { diffModes } from '../../constants'; import { diffModes } from '../../constants';
export default { export default {
...@@ -51,9 +52,17 @@ export default { ...@@ -51,9 +52,17 @@ export default {
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [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], { Object.assign(state.entries[file.path], {
raw, raw,
}); });
if (openPendingFile) {
openPendingFile.raw = raw;
}
}, },
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
Object.assign(state.entries[file.path], { Object.assign(state.entries[file.path], {
...@@ -109,11 +118,22 @@ export default { ...@@ -109,11 +118,22 @@ export default {
}, },
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
const entry = state.entries[path];
const { deleted } = entry;
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
content: stagedFile ? stagedFile.content : state.entries[path].raw, content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false, 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) { [types.ADD_FILE_TO_CHANGED](state, path) {
Object.assign(state, { Object.assign(state, {
......
...@@ -46,6 +46,7 @@ export const dataStructure = () => ({ ...@@ -46,6 +46,7 @@ export const dataStructure = () => ({
parentPath: null, parentPath: null,
lastOpenedAt: 0, lastOpenedAt: 0,
mrChange: null, mrChange: null,
deleted: false,
}); });
export const decorateData = entity => { export const decorateData = entity => {
...@@ -105,15 +106,37 @@ export const setPageTitle = title => { ...@@ -105,15 +106,37 @@ export const setPageTitle = title => {
document.title = 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 }) => ({ export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({
branch, branch,
commit_message: state.commitMessage || getters.preBuiltCommitMessage, commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: rootState.stagedFiles.map(f => ({ actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: f.tempFile ? 'create' : 'update', action: commitActionForFile(f),
file_path: f.path, file_path: f.path,
content: f.content, content: f.content,
encoding: f.base64 ? 'base64' : 'text', 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, start_branch: newBranch ? rootState.currentBranchId : undefined,
}); });
......
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;
};
...@@ -77,6 +77,7 @@ ...@@ -77,6 +77,7 @@
.ide-file-icon-holder { .ide-file-icon-holder {
display: flex; display: flex;
align-items: center; align-items: center;
color: $theme-gray-700;
} }
.ide-file-changed-icon { .ide-file-changed-icon {
...@@ -164,12 +165,23 @@ ...@@ -164,12 +165,23 @@
background-color: $white-light; background-color: $white-light;
border-bottom-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 { .multi-file-tab {
@include str-truncated(141px); @include str-truncated(141px);
cursor: pointer;
svg { svg {
vertical-align: middle; vertical-align: middle;
...@@ -244,6 +256,38 @@ ...@@ -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 { .monaco-diff-editor.vs {
.editor.modified { .editor.modified {
box-shadow: none; box-shadow: none;
...@@ -560,16 +604,21 @@ ...@@ -560,16 +604,21 @@
} }
} }
.multi-file-addition, .ide-file-addition,
.multi-file-addition-solid { .ide-file-addition-solid {
color: $green-500; color: $green-500;
} }
.multi-file-modified, .ide-file-modified,
.multi-file-modified-solid { .ide-file-modified-solid {
color: $orange-500; color: $orange-500;
} }
.ide-file-deletion,
.ide-file-deletion-solid {
color: $red-500;
}
.multi-file-commit-list-collapsed { .multi-file-commit-list-collapsed {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -1017,6 +1066,10 @@ ...@@ -1017,6 +1066,10 @@
.ide-new-btn { .ide-new-btn {
margin-left: auto; margin-left: auto;
} }
button {
color: $gl-text-color;
}
} }
.ide-sidebar-branch-title { .ide-sidebar-branch-title {
......
---
title: Enabled deletion of files in the Web IDE
merge_request:
author:
type: added
...@@ -122,6 +122,11 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts ...@@ -122,6 +122,11 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{text} %{files}"
msgid_plural "%{text} %{files} files"
msgstr[0] ""
msgstr[1] ""
msgid "%{text} is available" msgid "%{text} is available"
msgstr "" msgstr ""
...@@ -1992,6 +1997,9 @@ msgstr "" ...@@ -1992,6 +1997,9 @@ msgstr ""
msgid "Delete list" msgid "Delete list"
msgstr "" msgstr ""
msgid "Deleted"
msgstr ""
msgid "Deny" msgid "Deny"
msgstr "" msgstr ""
...@@ -5537,10 +5545,8 @@ msgstr "" ...@@ -5537,10 +5545,8 @@ msgstr ""
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
msgid "Update %{files}" msgid "Update"
msgid_plural "Update %{files} files" msgstr ""
msgstr[0] ""
msgstr[1] ""
msgid "Update your group name, description, avatar, and other general settings." msgid "Update your group name, description, avatar, and other general settings."
msgstr "" msgstr ""
......
...@@ -33,14 +33,14 @@ describe('IDE changed file icon', () => { ...@@ -33,14 +33,14 @@ describe('IDE changed file icon', () => {
}); });
describe('changedIconClass', () => { describe('changedIconClass', () => {
it('includes multi-file-modified when not a temp file', () => { it('includes ide-file-modified when not a temp file', () => {
expect(vm.changedIconClass).toContain('multi-file-modified'); 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; vm.file.tempFile = true;
expect(vm.changedIconClass).toContain('multi-file-addition'); expect(vm.changedIconClass).toContain('ide-file-addition');
}); });
}); });
}); });
...@@ -76,17 +76,29 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -76,17 +76,29 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.iconName).toBe('file-addition'); expect(vm.iconName).toBe('file-addition');
}); });
it('returns deletion', () => {
f.deleted = true;
expect(vm.iconName).toBe('file-deletion');
});
}); });
describe('iconClass', () => { describe('iconClass', () => {
it('returns modified when not a tempFile', () => { 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', () => { it('returns addition when not a tempFile', () => {
f.tempFile = true; 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');
}); });
}); });
}); });
......
...@@ -14,6 +14,7 @@ describe('new dropdown component', () => { ...@@ -14,6 +14,7 @@ describe('new dropdown component', () => {
branch: 'master', branch: 'master',
path: '', path: '',
mouseOver: false, mouseOver: false,
type: 'tree',
}); });
vm.$store.state.currentProjectId = 'abcproject'; vm.$store.state.currentProjectId = 'abcproject';
...@@ -67,4 +68,14 @@ describe('new dropdown component', () => { ...@@ -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('');
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores'; import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue'; import repoEditor from '~/ide/components/repo_editor.vue';
...@@ -25,6 +26,8 @@ describe('RepoEditor', () => { ...@@ -25,6 +26,8 @@ describe('RepoEditor', () => {
vm.$store.state.openFiles.push(f); vm.$store.state.openFiles.push(f);
Vue.set(vm.$store.state.entries, f.path, f); Vue.set(vm.$store.state.entries, f.path, f);
spyOn(vm, 'getFileData').and.returnValue(Promise.resolve());
vm.$mount(); vm.$mount();
Vue.nextTick(() => setTimeout(done)); Vue.nextTick(() => setTimeout(done));
......
...@@ -91,25 +91,6 @@ describe('RepoFile', () => { ...@@ -91,25 +91,6 @@ describe('RepoFile', () => {
done(); 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', () => { describe('locked file', () => {
......
...@@ -93,13 +93,13 @@ describe('RepoTab', () => { ...@@ -93,13 +93,13 @@ describe('RepoTab', () => {
Vue.nextTick() Vue.nextTick()
.then(() => { .then(() => {
expect(vm.$el.querySelector('.multi-file-modified')).toBeNull(); expect(vm.$el.querySelector('.ide-file-modified')).toBeNull();
vm.$el.dispatchEvent(new Event('mouseout')); vm.$el.dispatchEvent(new Event('mouseout'));
}) })
.then(Vue.nextTick) .then(Vue.nextTick)
.then(() => { .then(() => {
expect(vm.$el.querySelector('.multi-file-modified')).not.toBeNull(); expect(vm.$el.querySelector('.ide-file-modified')).not.toBeNull();
done(); done();
}) })
......
...@@ -7,6 +7,7 @@ import actions, { ...@@ -7,6 +7,7 @@ import actions, {
updateActivityBarView, updateActivityBarView,
updateTempFlagForEntry, updateTempFlagForEntry,
setErrorMessage, setErrorMessage,
deleteEntry,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import store from '~/ide/stores'; import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
...@@ -457,4 +458,19 @@ describe('Multi-file store actions', () => { ...@@ -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,
);
});
});
}); });
...@@ -123,6 +123,22 @@ describe('IDE commit module getters', () => { ...@@ -123,6 +123,22 @@ describe('IDE commit module getters', () => {
'Update test-file, index.js files', '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',
);
});
}); });
}); });
}); });
...@@ -94,6 +94,35 @@ describe('IDE store file mutations', () => { ...@@ -94,6 +94,35 @@ describe('IDE store file mutations', () => {
expect(localFile.raw).toBe('testing'); 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', () => { describe('SET_FILE_BASE_RAW_DATA', () => {
...@@ -205,6 +234,11 @@ describe('IDE store file mutations', () => { ...@@ -205,6 +234,11 @@ describe('IDE store file mutations', () => {
beforeEach(() => { beforeEach(() => {
localFile.content = 'test'; localFile.content = 'test';
localFile.changed = true; localFile.changed = true;
localState.currentProjectId = 'gitlab-ce';
localState.currentBranchId = 'master';
localState.trees['gitlab-ce/master'] = {
tree: [],
};
}); });
it('resets content and changed', () => { it('resets content and changed', () => {
...@@ -213,6 +247,36 @@ describe('IDE store file mutations', () => { ...@@ -213,6 +247,36 @@ describe('IDE store file mutations', () => {
expect(localFile.content).toBe(''); expect(localFile.content).toBe('');
expect(localFile.changed).toBeFalsy(); 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', () => { describe('ADD_FILE_TO_CHANGED', () => {
......
...@@ -156,4 +156,61 @@ describe('Multi-file store mutations', () => { ...@@ -156,4 +156,61 @@ describe('Multi-file store mutations', () => {
expect(localState.errorMessage).toBe('error'); 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]);
});
});
}); });
...@@ -86,6 +86,11 @@ describe('Multi-file store utils', () => { ...@@ -86,6 +86,11 @@ describe('Multi-file store utils', () => {
base64: true, base64: true,
lastCommitSha: '123456789', lastCommitSha: '123456789',
}, },
{
...file('deletedFile'),
path: 'deletedFile',
deleted: true,
},
], ],
currentBranchId: 'master', currentBranchId: 'master',
}; };
...@@ -115,6 +120,13 @@ describe('Multi-file store utils', () => { ...@@ -115,6 +120,13 @@ describe('Multi-file store utils', () => {
encoding: 'base64', encoding: 'base64',
last_commit_id: '123456789', last_commit_id: '123456789',
}, },
{
action: 'delete',
file_path: 'deletedFile',
content: '',
encoding: 'text',
last_commit_id: undefined,
},
], ],
start_branch: undefined, start_branch: undefined,
}); });
...@@ -173,4 +185,65 @@ describe('Multi-file store utils', () => { ...@@ -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,
},
]);
});
});
}); });
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