Welcome to mirror list, hosted at ThFree Co, Russian Federation.

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