Commit cded268c authored by Phil Hughes's avatar Phil Hughes Committed by Tim Zallmann

Enable deleting files in the Web IDE

parent 88738408
......@@ -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"
......
......@@ -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,
......
......@@ -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
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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,
......
......@@ -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"
>
......
......@@ -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"
......
......@@ -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>
......
......@@ -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',
},
};
......@@ -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(
......
......@@ -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';
......
......@@ -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)
......
......@@ -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);
......
......@@ -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))
......
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
......
......@@ -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';
/* 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,
......
/* 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, {
......
......@@ -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,
});
......
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 @@
.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;