Commit b901df22 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'ide-rename-files' into 'master'

Enable renaming files & folders in the Web IDE

Closes #44845

See merge request gitlab-org/gitlab-ce!20835
parents 97285407 19eecd01
......@@ -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,11 +108,20 @@ 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')"
......
......@@ -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"
......
......@@ -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"
......
......@@ -53,3 +53,8 @@ export const commitItemIconMap = {
class: 'ide-file-deletion',
},
};
export const modalTypes = {
rename: 'rename',
tree: 'tree',
};
......@@ -8,7 +8,7 @@ export default {
});
},
getRawFileData(file) {
if (file.tempFile) {
if (file.tempFile && !file.prevPath) {
return Promise.resolve(file.content);
}
......
......@@ -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';
......
......@@ -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) {
......
......@@ -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);
}
};
......@@ -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;
......
......@@ -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';
......@@ -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,
......
......@@ -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) {
......
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));
}
},
};
......@@ -26,8 +26,9 @@ export default () => ({
rightPane: null,
links: {},
errorMessage: null,
newEntryModal: {
entryModal: {
type: '',
path: '',
entry: {},
},
});
......@@ -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;
<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"
......
......@@ -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 {
......
---
title: Enable renaming files and folders in Web IDE
merge_request: 20835
author:
type: added
......@@ -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 ""
......
......@@ -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('');
});
......
......@@ -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');
});
});
});
});
......@@ -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,
);
});
......
......@@ -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,
......
......@@ -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);
});
});
});
......@@ -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,
},
]);
});
});
......
......@@ -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 = `
......
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