summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml30
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml19
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue7
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue3
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue1
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue24
-rw-r--r--app/assets/javascripts/ide/lib/files.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js71
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js9
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js84
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js107
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js18
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js70
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue2
-rw-r--r--app/assets/stylesheets/framework/animations.scss15
-rw-r--r--app/graphql/resolvers/issues_resolver.rb2
-rw-r--r--app/graphql/types/issuable_sort_enum.rb10
-rw-r--r--app/graphql/types/issue_sort_enum.rb10
-rw-r--r--changelogs/unreleased/17596-show-all-matching-labels-when-using-label-quick-action.yml5
-rw-r--r--jest.config.js5
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb4
-rw-r--r--lib/gitlab/sidekiq_config.rb14
-rw-r--r--lib/tasks/frontend.rake5
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb4
-rw-r--r--spec/frontend/fixtures/autocomplete_sources.rb4
-rw-r--r--spec/frontend/ide/lib/files_spec.js4
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js34
-rw-r--r--spec/javascripts/ide/components/file_row_extra_spec.js21
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js14
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js83
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js197
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js351
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js92
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js19
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js389
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js225
-rw-r--r--spec/javascripts/lazy_loader_spec.js18
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js13
-rw-r--r--spec/lib/gitlab_spec.rb1
47 files changed, 1527 insertions, 499 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 1feda7ed4d4..16b85696727 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -123,7 +123,7 @@ compile-assets pull-cache:
- .use-pg9
dependencies: ["compile-assets", "compile-assets pull-cache", "setup-test-env"]
-karma:
+.karma-base:
extends: .only-code-frontend-job-base
variables:
# we override the max_old_space_size to prevent OOM errors
@@ -134,6 +134,9 @@ karma:
- scripts/gitaly-test-spawn
- date
- bundle exec rake karma
+
+karma:
+ extends: .karma-base
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
name: coverage-javascript
@@ -146,7 +149,12 @@ karma:
reports:
junit: junit_karma.xml
-jest:
+karma-foss:
+ extends:
+ - .karma-base
+ - .only-ee-as-if-foss
+
+.jest-base:
extends: .only-code-frontend-job-base
script:
- scripts/gitaly-test-spawn
@@ -154,6 +162,14 @@ jest:
- bundle exec rake frontend:fixtures
- date
- yarn jest --ci --coverage
+ cache:
+ key: jest
+ paths:
+ - tmp/jest/jest/
+ policy: pull-push
+
+jest:
+ extends: .jest-base
artifacts:
name: coverage-frontend
expire_in: 31d
@@ -164,11 +180,13 @@ jest:
- tmp/tests/frontend/
reports:
junit: junit_jest.xml
+
+jest-foss:
+ extends:
+ - .jest-base
+ - .only-ee-as-if-foss
cache:
- key: jest
- paths:
- - tmp/jest/jest/
- policy: pull-push
+ policy: pull
.qa-job-base:
extends:
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index e082d584b0c..0c0591d3fdc 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -149,3 +149,8 @@
variables:
- $CI_PROJECT_NAME == "gitlab"
- $CI_PROJECT_NAME == "gitlab-ee" # Support former project name for forks/mirrors
+
+.only-ee-as-if-foss:
+ extends: .only-ee
+ variables:
+ IS_GITLAB_EE: '0'
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index c315501b0ba..73b649b4d14 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -74,6 +74,12 @@ setup-test-env:
- .rspec-base
- .use-pg9
+.rspec-base-pg9-foss:
+ extends:
+ - .rspec-base
+ - .use-pg9
+ - .only-ee-as-if-foss
+
.rspec-base-pg10:
extends:
- .rspec-base
@@ -84,14 +90,27 @@ rspec unit pg9:
extends: .rspec-base-pg9
parallel: 20
+rspec unit pg9-foss:
+ extends: .rspec-base-pg9-foss
+ parallel: 20
+
rspec integration pg9:
extends: .rspec-base-pg9
parallel: 6
+rspec integration pg9-foss:
+ extends: .rspec-base-pg9-foss
+ parallel: 6
+
rspec system pg9:
extends: .rspec-base-pg9
parallel: 24
+# TODO: This requires FOSS assets
+# rspec system pg9-foss:
+# extends: .rspec-base-pg9-foss
+# parallel: 24
+
rspec unit pg10:
extends: .rspec-base-pg10
parallel: 20
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index b308cd9c236..db3ad0bb4c9 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -337,6 +337,7 @@ class GfmAutoComplete {
},
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${title}',
+ limit: 20,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index 11d5d9639b6..6b2ef34c960 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -43,7 +43,12 @@ export default {
<template>
<div class="d-flex ide-commit-editor-header align-items-center">
<file-icon :file-name="activeFile.name" :size="16" class="mr-2" />
- <strong class="mr-2"> {{ activeFile.path }} </strong>
+ <strong class="mr-2">
+ <template v-if="activeFile.prevPath && activeFile.prevPath !== activeFile.path">
+ {{ activeFile.prevPath }} &#x2192;
+ </template>
+ {{ activeFile.path }}
+ </strong>
<changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto">
<button
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 47b205f0a75..230dfaf047b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -110,6 +110,9 @@ export default {
>
<span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon :file-name="file.name" class="append-right-8" />
+ <template v-if="file.prevName && file.prevName !== file.name">
+ {{ file.prevName }} &#x2192;
+ </template>
{{ file.name }}
</span>
<div class="ml-auto d-flex align-items-center">
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index 5819999a459..f0bedcfbd6b 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -34,6 +34,9 @@ export default {
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
+ isTree() {
+ return this.file.type === 'tree';
+ },
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
@@ -58,10 +61,13 @@ export default {
});
},
showTreeChangesCount() {
- return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened;
+ return this.isTree && this.changesCount > 0 && !this.file.opened;
+ },
+ isModified() {
+ return this.file.changed || this.file.tempFile || this.file.staged || this.file.prevPath;
},
showChangedFileIcon() {
- return this.file.changed || this.file.tempFile || this.file.staged;
+ return !this.isTree && this.isModified;
},
},
};
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 1af86a94482..95782b2c88a 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -30,9 +30,6 @@ export default {
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
- actualTreeList() {
- return this.currentTree.tree.filter(entry => !entry.moved);
- },
},
mounted() {
this.updateViewer(this.viewerType);
@@ -57,9 +54,9 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
- <template v-if="actualTreeList.length">
+ <template v-if="currentTree.tree.length">
<file-row
- v-for="file in actualTreeList"
+ v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index a2dd31aebd4..d2ed1fe3e55 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -91,7 +91,6 @@ export default {
this.renameEntry({
path: this.entryModal.entry.path,
name: entryName,
- entryPath: null,
parentPath,
}),
)
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 802b7f1fa6f..3bf8308ccea 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -155,15 +155,7 @@ export default {
this.editor.clearEditor();
- this.getFileData({
- path: this.file.path,
- makeFileActive: false,
- })
- .then(() =>
- this.getRawFileData({
- path: this.file.path,
- }),
- )
+ this.fetchFileData()
.then(() => {
this.createEditorInstance();
})
@@ -179,6 +171,20 @@ export default {
throw err;
});
},
+ fetchFileData() {
+ if (this.file.tempFile) {
+ return Promise.resolve();
+ }
+
+ return this.getFileData({
+ path: this.file.path,
+ makeFileActive: false,
+ }).then(() =>
+ this.getRawFileData({
+ path: this.file.path,
+ }),
+ );
+ },
createEditorInstance() {
this.editor.dispose();
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index 51278640b5b..e86dac20104 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -1,7 +1,5 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
-import { decorateData, sortTree } from '../stores/utils';
-
-export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
+import { decorateData, sortTree, escapeFileUrl } from '../stores/utils';
export const splitParent = path => {
const idx = path.lastIndexOf('/');
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 8c0119a1fed..4e18ec58feb 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -9,6 +9,7 @@ import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
import service from '../services';
import router from '../ide_router';
+import eventHub from '../eventhub';
export const redirectToUrl = (self, url) => visitUrl(url);
@@ -171,8 +172,10 @@ export const setCurrentBranchId = ({ commit }, currentBranchId) => {
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
- if (file.parentPath) {
- dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
+ const parent = file.parentPath && state.entries[file.parentPath];
+
+ if (parent) {
+ dispatch('updateTempFlagForEntry', { file: parent, tempFile });
}
};
@@ -199,51 +202,71 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path];
-
+ const { prevPath, prevName, prevParentPath } = entry;
+ const isTree = entry.type === 'tree';
+
+ if (prevPath) {
+ dispatch('renameEntry', {
+ path,
+ name: prevName,
+ parentPath: prevParentPath,
+ });
+ dispatch('deleteEntry', prevPath);
+ return;
+ }
if (state.unusedSeal) dispatch('burstUnusedSeal');
if (entry.opened) dispatch('closeFile', entry);
- if (entry.type === 'tree') {
+ if (isTree) {
entry.tree.forEach(f => dispatch('deleteEntry', f.path));
}
commit(types.DELETE_ENTRY, path);
- dispatch('stageChange', path);
+
+ // Only stage if we're not a directory or a new file
+ if (!isTree && !entry.tempFile) {
+ dispatch('stageChange', path);
+ }
dispatch('triggerFilesChange');
};
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
-export const renameEntry = (
- { dispatch, commit, state },
- { path, name, entryPath = null, parentPath },
-) => {
- const entry = state.entries[entryPath || path];
+export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => {
+ const entry = state.entries[path];
+ const newPath = parentPath ? `${parentPath}/${name}` : name;
- commit(types.RENAME_ENTRY, { path, name, entryPath, parentPath });
+ commit(types.RENAME_ENTRY, { path, name, parentPath });
if (entry.type === 'tree') {
- const slashedParentPath = parentPath ? `${parentPath}/` : '';
- const targetEntry = entryPath ? entryPath.split('/').pop() : name;
- const newParentPath = `${slashedParentPath}${targetEntry}`;
-
- state.entries[entryPath || path].tree.forEach(f => {
+ state.entries[newPath].tree.forEach(f => {
dispatch('renameEntry', {
- path,
- name,
- entryPath: f.path,
- parentPath: newParentPath,
+ path: f.path,
+ name: f.name,
+ parentPath: newPath,
});
});
} else {
- const newPath = parentPath ? `${parentPath}/${name}` : name;
const newEntry = state.entries[newPath];
- commit(types.TOGGLE_FILE_CHANGED, { file: newEntry, changed: true });
+ const isRevert = newPath === entry.prevPath;
+ const isReset = isRevert && !newEntry.changed && !newEntry.tempFile;
+ const isInChanges = state.changedFiles
+ .concat(state.stagedFiles)
+ .some(({ key }) => key === newEntry.key);
+
+ if (isReset) {
+ commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
+ } else if (!isInChanges) {
+ commit(types.ADD_FILE_TO_CHANGED, newPath);
+ }
+
+ if (!newEntry.tempFile) {
+ eventHub.$emit(`editor.update.model.dispose.${entry.key}`);
+ }
- if (entry.opened) {
+ if (newEntry.opened) {
router.push(`/project${newEntry.url}`);
- commit(types.TOGGLE_FILE_OPEN, entry.path);
}
}
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 7627b6e03af..59445afc7a4 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,7 +5,7 @@ import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
-import { setPageTitle } from '../utils';
+import { setPageTitle, replaceFileUrl } from '../utils';
import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
@@ -67,7 +67,7 @@ export const getFileData = (
commit(types.TOGGLE_LOADING, { entry: file });
- const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url;
+ const url = file.prevPath ? replaceFileUrl(file.url, file.path, file.prevPath) : file.url;
return service
.getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/')))
@@ -186,11 +186,6 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
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);
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index dd8f17e4f3a..20887e7d0ac 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -92,13 +92,27 @@ export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
});
};
-export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
- dispatch('setCurrentBranchId', branchId);
+export const loadFile = ({ dispatch, state }, { basePath }) => {
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
+ const treeEntryKey = Object.keys(state.entries).find(
+ key => key === path && !state.entries[key].pending,
+ );
+ const treeEntry = state.entries[treeEntryKey];
- if (getters.emptyRepo) {
- return dispatch('showEmptyState', { projectId, branchId });
+ if (treeEntry) {
+ dispatch('handleTreeEntryAction', treeEntry);
+ } else {
+ dispatch('createTempEntry', {
+ name: path,
+ type: 'blob',
+ });
+ }
}
- return dispatch('getBranchData', {
+};
+
+export const loadBranch = ({ dispatch }, { projectId, branchId }) =>
+ dispatch('getBranchData', {
projectId,
branchId,
})
@@ -107,42 +121,38 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId,
projectId,
branchId,
});
- dispatch('getFiles', {
+ return dispatch('getFiles', {
projectId,
branchId,
- })
- .then(() => {
- if (basePath) {
- const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
- const treeEntryKey = Object.keys(state.entries).find(
- key => key === path && !state.entries[key].pending,
- );
- const treeEntry = state.entries[treeEntryKey];
-
- if (treeEntry) {
- dispatch('handleTreeEntryAction', treeEntry);
- } else {
- dispatch('createTempEntry', {
- name: path,
- type: 'blob',
- });
- }
- }
- })
- .catch(
- () =>
- new Error(
- sprintf(
- __('An error occurred whilst getting files for - %{branchId}'),
- {
- branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
- },
- false,
- ),
- ),
- );
+ });
})
.catch(() => {
dispatch('showBranchNotFoundError', branchId);
+ return Promise.reject();
});
+
+export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
+ const currentProject = state.projects[projectId];
+ if (getters.emptyRepo) {
+ return dispatch('showEmptyState', { projectId, branchId });
+ }
+ if (!currentProject || !currentProject.branches[branchId]) {
+ dispatch('setCurrentBranchId', branchId);
+
+ return dispatch('loadBranch', { projectId, branchId })
+ .then(() => dispatch('loadFile', { basePath }))
+ .catch(
+ () =>
+ new Error(
+ sprintf(
+ __('An error occurred whilst getting files for - %{branchId}'),
+ {
+ branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ },
+ false,
+ ),
+ ),
+ );
+ }
+ return Promise.resolve(dispatch('loadFile', { basePath }));
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index f767ca92a56..e89ed49318b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -154,8 +154,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
.then(() => {
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
- commit(rootTypes.CLEAR_REPLACED_FILES, null, { root: true });
-
setTimeout(() => {
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index f021729c451..f0b4718d025 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -59,8 +59,7 @@ export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
export const STAGE_CHANGE = 'STAGE_CHANGE';
export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
-
-export const CLEAR_REPLACED_FILES = 'CLEAR_REPLACED_FILES';
+export const REMOVE_FILE_FROM_STAGED_AND_CHANGED = 'REMOVE_FILE_FROM_STAGED_AND_CHANGED';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
@@ -79,5 +78,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 REVERT_RENAME_ENTRY = 'REVERT_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 ea125214ebb..2587b57a817 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -5,7 +5,14 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
-import { sortTree } from './utils';
+import {
+ sortTree,
+ replaceFileUrl,
+ swapInParentTreeWithSorting,
+ updateFileCollections,
+ removeFromParentTree,
+ pathsAreEqual,
+} from './utils';
export default {
[types.SET_INITIAL_DATA](state, data) {
@@ -56,11 +63,6 @@ export default {
stagedFiles: [],
});
},
- [types.CLEAR_REPLACED_FILES](state) {
- Object.assign(state, {
- replacedFiles: [],
- });
- },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@@ -157,9 +159,14 @@ export default {
changed: Boolean(changedFile),
staged: false,
replaces: false,
- prevPath: '',
- moved: false,
lastCommitSha: lastCommit.commit.id,
+
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ prevParentPath: undefined,
});
if (prevPath) {
@@ -209,7 +216,9 @@ export default {
entry.deleted = true;
- parent.tree = parent.tree.filter(f => f.path !== entry.path);
+ if (parent) {
+ parent.tree = parent.tree.filter(f => f.path !== entry.path);
+ }
if (entry.type === 'blob') {
if (tempFile) {
@@ -219,51 +228,61 @@ export default {
}
}
},
- [types.RENAME_ENTRY](state, { path, name, entryPath = null, parentPath }) {
- const oldEntry = state.entries[entryPath || path];
- const slashedParentPath = parentPath ? `${parentPath}/` : '';
- const newPath = entryPath
- ? `${slashedParentPath}${oldEntry.name}`
- : `${slashedParentPath}${name}`;
+ [types.RENAME_ENTRY](state, { path, name, parentPath }) {
+ const oldEntry = state.entries[path];
+ const newPath = parentPath ? `${parentPath}/${name}` : name;
+ const isRevert = newPath === oldEntry.prevPath;
- Vue.set(state.entries, newPath, {
+ const newUrl = replaceFileUrl(oldEntry.url, oldEntry.path, newPath);
+
+ const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath);
+
+ const baseProps = {
...oldEntry,
+ name,
id: newPath,
- key: `${newPath}-${oldEntry.type}-${oldEntry.path}`,
path: newPath,
- name: entryPath ? oldEntry.name : name,
- tempFile: true,
- prevPath: oldEntry.tempFile ? null : oldEntry.path,
- url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
- tree: [],
- raw: '',
- opened: false,
- parentPath,
- });
+ url: newUrl,
+ key: newKey,
+ parentPath: parentPath || '',
+ };
- oldEntry.moved = true;
- oldEntry.movedPath = newPath;
+ const prevProps =
+ oldEntry.tempFile || isRevert
+ ? {
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ prevParentPath: undefined,
+ }
+ : {
+ prevId: oldEntry.prevId || oldEntry.id,
+ prevPath: oldEntry.prevPath || oldEntry.path,
+ prevName: oldEntry.prevName || oldEntry.name,
+ prevUrl: oldEntry.prevUrl || oldEntry.url,
+ prevKey: oldEntry.prevKey || oldEntry.key,
+ prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath,
+ };
- const parent = parentPath
- ? state.entries[parentPath]
- : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
- const newEntry = state.entries[newPath];
-
- parent.tree = sortTree(parent.tree.concat(newEntry));
+ Vue.set(state.entries, newPath, {
+ ...baseProps,
+ ...prevProps,
+ });
- if (newEntry.type === 'blob') {
- state.changedFiles = state.changedFiles.concat(newEntry);
+ if (pathsAreEqual(oldEntry.parentPath, parentPath)) {
+ swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath);
+ } else {
+ removeFromParentTree(state, oldEntry.key, oldEntry.parentPath);
+ swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath);
}
- if (oldEntry.tempFile) {
- const filterMethod = f => f.path !== oldEntry.path;
-
- state.openFiles = state.openFiles.filter(filterMethod);
- state.changedFiles = state.changedFiles.filter(filterMethod);
- parent.tree = parent.tree.filter(filterMethod);
-
- Vue.delete(state.entries, oldEntry.path);
+ if (oldEntry.type === 'blob') {
+ updateFileCollections(state, oldEntry.key, newPath);
}
+
+ Vue.delete(state.entries, oldEntry.path);
},
...projectMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 1442ea7dbfa..8caeb2d73b2 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -138,8 +138,6 @@ export default {
content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
deleted: false,
- moved: false,
- movedPath: '',
});
if (deleted) {
@@ -179,11 +177,6 @@ export default {
});
if (stagedFile) {
- Object.assign(state, {
- replacedFiles: state.replacedFiles.concat({
- ...stagedFile,
- }),
- });
Object.assign(stagedFile, {
...state.entries[path],
});
@@ -252,4 +245,15 @@ export default {
openFiles: state.openFiles.filter(f => f.key !== file.key),
});
},
+ [types.REMOVE_FILE_FROM_STAGED_AND_CHANGED](state, file) {
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.key !== file.key),
+ stagedFiles: state.stagedFiles.filter(f => f.key !== file.key),
+ });
+
+ Object.assign(state.entries[file.path], {
+ changed: false,
+ staged: false,
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index c4da482bf0a..d400b9831a9 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -6,7 +6,6 @@ export default () => ({
currentMergeRequestId: '',
changedFiles: [],
stagedFiles: [],
- replacedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 52200ce7847..a8d8ff31afe 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -50,9 +50,7 @@ export const dataStructure = () => ({
lastOpenedAt: 0,
mrChange: null,
deleted: false,
- prevPath: '',
- movedPath: '',
- moved: false,
+ prevPath: undefined,
});
export const decorateData = entity => {
@@ -129,7 +127,7 @@ export const commitActionForFile = file => {
export const getCommitFiles = stagedFiles =>
stagedFiles.reduce((acc, file) => {
- if (file.moved || file.type === 'tree') return acc;
+ if (file.type === 'tree') return acc;
return acc.concat({
...file,
@@ -148,9 +146,9 @@ export const createCommitPayload = ({
commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: commitActionForFile(f),
- file_path: f.moved ? f.movedPath : f.path,
- previous_path: f.prevPath === '' ? undefined : f.prevPath,
- content: f.prevPath ? null : f.content || undefined,
+ file_path: f.path,
+ previous_path: f.prevPath || undefined,
+ content: f.prevPath && !f.changed ? null : f.content || undefined,
encoding: f.base64 ? 'base64' : 'text',
last_commit_id:
newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha,
@@ -213,3 +211,61 @@ export const mergeTrees = (fromTree, toTree) => {
return toTree;
};
+
+export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
+
+export const replaceFileUrl = (url, oldPath, newPath) => {
+ // Add `/-/` so that we don't accidentally replace project path
+ const result = url.replace(`/-/${escapeFileUrl(oldPath)}`, `/-/${escapeFileUrl(newPath)}`);
+
+ return result;
+};
+
+export const swapInStateArray = (state, arr, key, entryPath) =>
+ Object.assign(state, {
+ [arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)),
+ });
+
+export const getEntryOrRoot = (state, path) =>
+ path ? state.entries[path] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
+
+export const swapInParentTreeWithSorting = (state, oldKey, newPath, parentPath) => {
+ if (!newPath) {
+ return;
+ }
+
+ const parent = getEntryOrRoot(state, parentPath);
+
+ if (parent) {
+ const tree = parent.tree
+ // filter out old entry && new entry
+ .filter(({ key, path }) => key !== oldKey && path !== newPath)
+ // concat new entry
+ .concat(state.entries[newPath]);
+
+ parent.tree = sortTree(tree);
+ }
+};
+
+export const removeFromParentTree = (state, oldKey, parentPath) => {
+ const parent = getEntryOrRoot(state, parentPath);
+
+ if (parent) {
+ parent.tree = sortTree(parent.tree.filter(({ key }) => key !== oldKey));
+ }
+};
+
+export const updateFileCollections = (state, key, entryPath) => {
+ ['openFiles', 'changedFiles', 'stagedFiles'].forEach(fileCollection => {
+ swapInStateArray(state, fileCollection, key, entryPath);
+ });
+};
+
+export const cleanTrailingSlash = path => path.replace(/\/$/, '');
+
+export const pathsAreEqual = (a, b) => {
+ const cleanA = a ? cleanTrailingSlash(a) : '';
+ const cleanB = b ? cleanTrailingSlash(b) : '';
+
+ return cleanA === cleanB;
+};
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 54cd0c9c642..75c3c544c77 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -70,7 +70,13 @@ export default {
return undefined;
},
showIcon() {
- return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted;
+ return (
+ this.file.changed ||
+ this.file.tempFile ||
+ this.file.staged ||
+ this.file.deleted ||
+ this.file.prevPath
+ );
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index f49e69c473b..341c9534763 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -131,7 +131,7 @@ export default {
</script>
<template>
- <div v-if="!file.moved">
+ <div>
<file-header v-if="file.isHeader" :path="file.path" />
<div
v-else
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 6f5a2e561af..d222fc4aefe 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -11,25 +11,10 @@
@include webkit-prefix(animation-duration, 1s);
@include webkit-prefix(animation-fill-mode, both);
- &.infinite {
- @include webkit-prefix(animation-iteration-count, infinite);
- }
-
&.once {
@include webkit-prefix(animation-iteration-count, 1);
}
- &.hinge {
- @include webkit-prefix(animation-duration, 2s);
- }
-
- &.flipOutX,
- &.flipOutY,
- &.bounceIn,
- &.bounceOut {
- @include webkit-prefix(animation-duration, 0.75s);
- }
-
&.short {
@include webkit-prefix(animation-duration, 321ms);
@include webkit-prefix(animation-fill-mode, none);
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 850df2885aa..1fbc61cd950 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -35,7 +35,7 @@ module Resolvers
description: 'Issues closed after this date'
argument :search, GraphQL::STRING_TYPE, # rubocop:disable Graphql/Descriptions
required: false
- argument :sort, Types::SortEnum,
+ argument :sort, Types::IssueSortEnum,
description: 'Sort issues by this criteria',
required: false,
default_value: 'created_desc'
diff --git a/app/graphql/types/issuable_sort_enum.rb b/app/graphql/types/issuable_sort_enum.rb
new file mode 100644
index 00000000000..932e90c2d22
--- /dev/null
+++ b/app/graphql/types/issuable_sort_enum.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class IssuableSortEnum < SortEnum
+ graphql_name 'IssuableSort'
+ description 'Values for sorting issuables'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb
new file mode 100644
index 00000000000..ad919b55481
--- /dev/null
+++ b/app/graphql/types/issue_sort_enum.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class IssueSortEnum < IssuableSortEnum
+ graphql_name 'IssueSort'
+ description 'Values for sorting issues'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/changelogs/unreleased/17596-show-all-matching-labels-when-using-label-quick-action.yml b/changelogs/unreleased/17596-show-all-matching-labels-when-using-label-quick-action.yml
new file mode 100644
index 00000000000..7618e89b7c9
--- /dev/null
+++ b/changelogs/unreleased/17596-show-all-matching-labels-when-using-label-quick-action.yml
@@ -0,0 +1,5 @@
+---
+title: Show 20 labels in dropdown instead of 5
+merge_request: 17596
+author:
+type: fixed
diff --git a/jest.config.js b/jest.config.js
index 435dca876ce..c2a512e8afa 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -15,7 +15,10 @@ if (process.env.CI) {
]);
}
-let testMatch = ['<rootDir>/spec/frontend/**/*_spec.js', '<rootDir>/ee/spec/frontend/**/*_spec.js'];
+let testMatch = ['<rootDir>/spec/frontend/**/*_spec.js'];
+if (IS_EE) {
+ testMatch.push('<rootDir>/ee/spec/frontend/**/*_spec.js');
+}
// workaround for eslint-import-resolver-jest only resolving in test files
// see https://github.com/JoinColony/eslint-import-resolver-jest#note
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index edee4ba2486..9beb84a2422 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -120,7 +120,7 @@ module Gitlab
end
end
- def remove_feature_dependent_sub_relations(_relation_item)
+ def remove_feature_dependent_sub_relations!(_relation_item)
# no-op
end
@@ -191,7 +191,7 @@ module Gitlab
# Avoid keeping a possible heavy object in memory once we are done with it
while relation_item = tree_array.shift
- remove_feature_dependent_sub_relations(relation_item)
+ remove_feature_dependent_sub_relations!(relation_item)
# The transaction at this level is less speedy than one single transaction
# But we can't have it in the upper level or GC won't get rid of the AR objects
diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb
index c102fa14cfc..ffceeb68f20 100644
--- a/lib/gitlab/sidekiq_config.rb
+++ b/lib/gitlab/sidekiq_config.rb
@@ -5,7 +5,11 @@ require 'set'
module Gitlab
module SidekiqConfig
- QUEUE_CONFIG_PATHS = %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml].freeze
+ QUEUE_CONFIG_PATHS = begin
+ result = %w[app/workers/all_queues.yml]
+ result << 'ee/app/workers/all_queues.yml' if Gitlab.ee?
+ result
+ end.freeze
# This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside
# of bundler/Rails context, so we cannot use any gem or Rails methods.
@@ -48,9 +52,11 @@ module Gitlab
end
def self.workers
- @workers ||=
- find_workers(Rails.root.join('app', 'workers')) +
- find_workers(Rails.root.join('ee', 'app', 'workers'))
+ @workers ||= begin
+ result = find_workers(Rails.root.join('app', 'workers'))
+ result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'))) if Gitlab.ee?
+ result
+ end
end
def self.find_workers(root)
diff --git a/lib/tasks/frontend.rake b/lib/tasks/frontend.rake
index 1cac7520227..6e90229830d 100644
--- a/lib/tasks/frontend.rake
+++ b/lib/tasks/frontend.rake
@@ -2,7 +2,10 @@ unless Rails.env.production?
namespace :frontend do
desc 'GitLab | Frontend | Generate fixtures for JavaScript tests'
RSpec::Core::RakeTask.new(:fixtures, [:pattern]) do |t, args|
- args.with_defaults(pattern: '{spec,ee/spec}/frontend/fixtures/*.rb')
+ directories = %w[spec]
+ directories << 'ee/spec' if Gitlab.ee?
+ directory_glob = "{#{directories.join(',')}}"
+ args.with_defaults(pattern: "#{directory_glob}/frontend/fixtures/*.rb")
ENV['NO_KNAPSACK'] = 'true'
t.pattern = args[:pattern]
t.rspec_opts = '--format documentation'
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index d8e3ea8ba39..3fe5ff5feee 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -256,7 +256,7 @@ describe Projects::EnvironmentsController do
it 'loads the terminals for the environment' do
# In EE we have to stub EE::Environment since it overwrites the
# "terminals" method.
- expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ expect_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
.to receive(:terminals)
get :terminal, params: environment_params
@@ -282,7 +282,7 @@ describe Projects::EnvironmentsController do
it 'returns the first terminal for the environment' do
# In EE we have to stub EE::Environment since it overwrites the
# "terminals" method.
- expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ expect_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
.to receive(:terminals)
.and_return([:fake_terminal])
diff --git a/spec/frontend/fixtures/autocomplete_sources.rb b/spec/frontend/fixtures/autocomplete_sources.rb
index 9e04328e2b9..382eff02b0f 100644
--- a/spec/frontend/fixtures/autocomplete_sources.rb
+++ b/spec/frontend/fixtures/autocomplete_sources.rb
@@ -24,6 +24,10 @@ describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type:
create(:label, project: project, title: 'feature')
create(:label, project: project, title: 'documentation')
+ create(:label, project: project, title: 'P1')
+ create(:label, project: project, title: 'P2')
+ create(:label, project: project, title: 'P3')
+ create(:label, project: project, title: 'P4')
get :labels,
format: :json,
diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js
index 08a31318544..654dc6c13c8 100644
--- a/spec/frontend/ide/lib/files_spec.js
+++ b/spec/frontend/ide/lib/files_spec.js
@@ -1,6 +1,6 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
-import { decorateFiles, splitParent, escapeFileUrl } from '~/ide/lib/files';
-import { decorateData } from '~/ide/stores/utils';
+import { decorateFiles, splitParent } from '~/ide/lib/files';
+import { decorateData, escapeFileUrl } from '~/ide/stores/utils';
const TEST_BRANCH_ID = 'lorem-ipsum';
const TEST_PROJECT_ID = 10;
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index bf48d7bfdad..c1dcd4928a0 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -2,12 +2,14 @@ import Vue from 'vue';
import store from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
+import { trimText } from 'spec/helpers/text_helper';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
let vm;
let f;
+ let findPathEl;
beforeEach(() => {
const Component = Vue.extend(listItem);
@@ -21,6 +23,8 @@ describe('Multi-file editor commit sidebar list item', () => {
actionComponent: 'stage-button',
activeFileKey: `staged-${f.key}`,
}).$mount();
+
+ findPathEl = vm.$el.querySelector('.multi-file-commit-list-path');
});
afterEach(() => {
@@ -29,15 +33,39 @@ describe('Multi-file editor commit sidebar list item', () => {
resetStore(store);
});
+ const findPathText = () => trimText(findPathEl.textContent);
+
it('renders file path', () => {
- expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path);
+ expect(findPathText()).toContain(f.path);
+ });
+
+ it('correctly renders renamed entries', done => {
+ Vue.set(vm.file, 'prevName', 'Old name');
+
+ vm.$nextTick()
+ .then(() => {
+ expect(findPathText()).toEqual(`Old name → ${f.name}`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('correctly renders entry, the name of which did not change after rename (as within a folder)', done => {
+ Vue.set(vm.file, 'prevName', f.name);
+
+ vm.$nextTick()
+ .then(() => {
+ expect(findPathText()).toEqual(f.name);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('opens a closed file in the editor when clicking the file path', done => {
spyOn(vm, 'openPendingTab').and.callThrough();
spyOn(router, 'push');
- vm.$el.querySelector('.multi-file-commit-list-path').click();
+ findPathEl.click();
setTimeout(() => {
expect(vm.openPendingTab).toHaveBeenCalled();
@@ -52,7 +80,7 @@ describe('Multi-file editor commit sidebar list item', () => {
spyOn(vm, 'updateViewer').and.callThrough();
spyOn(router, 'push');
- vm.$el.querySelector('.multi-file-commit-list-path').click();
+ findPathEl.click();
setTimeout(() => {
expect(vm.updateViewer).toHaveBeenCalledWith('diff');
diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js
index d7fed3f0681..86146fcef69 100644
--- a/spec/javascripts/ide/components/file_row_extra_spec.js
+++ b/spec/javascripts/ide/components/file_row_extra_spec.js
@@ -139,6 +139,27 @@ describe('IDE extra file row component', () => {
done();
});
});
+
+ it('shows when file is renamed', done => {
+ vm.file.prevPath = 'original-file';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('hides when file is renamed', done => {
+ vm.file.prevPath = 'original-file';
+ vm.file.type = 'tree';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
+
+ done();
+ });
+ });
});
describe('merge request icon', () => {
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
index 554bd1ae3b5..f63007c7dd2 100644
--- a/spec/javascripts/ide/components/ide_tree_list_spec.js
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -58,20 +58,6 @@ describe('IDE tree list', () => {
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
-
- it('does not render moved entries', done => {
- const tree = [file('moved entry'), file('normal entry')];
- tree[0].moved = true;
- store.state.trees['abcproject/master'].tree = tree;
- const container = vm.$el.querySelector('.ide-tree-body');
-
- vm.$nextTick(() => {
- expect(container.children.length).toBe(1);
- expect(vm.$el.textContent).not.toContain('moved entry');
- expect(vm.$el.textContent).toContain('normal entry');
- done();
- });
- });
});
describe('empty-branch state', () => {
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index 0701b773e17..d1b43df74b9 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -410,10 +410,23 @@ describe('RepoEditor', () => {
describe('initEditor', () => {
beforeEach(() => {
+ vm.file.tempFile = false;
spyOn(vm.editor, 'createInstance');
spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
});
+ it('does not fetch file information for temp entries', done => {
+ vm.file.tempFile = true;
+
+ vm.initEditor();
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.getFileData).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
it('is being initialised for files without content even if shouldHideEditor is `true`', done => {
vm.file.content = '';
vm.file.raw = '';
@@ -429,16 +442,13 @@ describe('RepoEditor', () => {
});
it('does not initialize editor for files already with content', done => {
- expect(vm.getFileData.calls.count()).toEqual(1);
- expect(vm.getRawFileData.calls.count()).toEqual(1);
-
vm.file.content = 'foo';
vm.initEditor();
vm.$nextTick()
.then(() => {
- expect(vm.getFileData.calls.count()).toEqual(1);
- expect(vm.getRawFileData.calls.count()).toEqual(1);
+ expect(vm.getFileData).not.toHaveBeenCalled();
+ expect(vm.getRawFileData).not.toHaveBeenCalled();
expect(vm.editor.createInstance).not.toHaveBeenCalled();
})
.then(done)
@@ -446,23 +456,56 @@ describe('RepoEditor', () => {
});
});
- it('calls removePendingTab when old file is pending', done => {
- spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
- spyOn(vm, 'removePendingTab');
+ describe('updates on file changes', () => {
+ beforeEach(() => {
+ spyOn(vm, 'initEditor');
+ });
- vm.file.pending = true;
+ it('calls removePendingTab when old file is pending', done => {
+ spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
+ spyOn(vm, 'removePendingTab');
- vm.$nextTick()
- .then(() => {
- vm.file = file('testing');
- vm.file.content = 'foo'; // need to prevent full cycle of initEditor
+ vm.file.pending = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.file = file('testing');
+ vm.file.content = 'foo'; // need to prevent full cycle of initEditor
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.removePendingTab).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.removePendingTab).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not call initEditor if the file did not change', done => {
+ Vue.set(vm, 'file', vm.file);
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.initEditor).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls initEditor when file key is changed', done => {
+ expect(vm.initEditor).not.toHaveBeenCalled();
+
+ Vue.set(vm, 'file', {
+ ...vm.file,
+ key: 'new',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.initEditor).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index 8ecb6129c63..bcc7b5d5e46 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -6,8 +6,10 @@ import {
createNewBranchFromDefault,
showEmptyState,
openBranch,
+ loadFile,
+ loadBranch,
} from '~/ide/stores/actions';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import service from '~/ide/services';
import api from '~/api';
import router from '~/ide/ide_router';
@@ -16,8 +18,10 @@ import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => {
let mock;
+ let store;
beforeEach(() => {
+ store = createStore();
mock = new MockAdapter(axios);
store.state.projects['abc/def'] = {
@@ -231,28 +235,139 @@ describe('IDE store project actions', () => {
});
});
+ describe('loadFile', () => {
+ beforeEach(() => {
+ Object.assign(store.state, {
+ entries: {
+ foo: { pending: false },
+ 'foo/bar-pending': { pending: true },
+ 'foo/bar': { pending: false },
+ },
+ });
+ spyOn(store, 'dispatch');
+ });
+
+ it('does nothing, if basePath is not given', () => {
+ loadFile(store, { basePath: undefined });
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ it('handles tree entry action, if basePath is given and the entry is not pending', () => {
+ loadFile(store, { basePath: 'foo/bar/' });
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ store.state.entries['foo/bar'],
+ );
+ });
+
+ it('does not handle tree entry action, if entry is pending', () => {
+ loadFile(store, { basePath: 'foo/bar-pending/' });
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything());
+ });
+
+ it('creates a new temp file supplied via URL if the file does not exist yet', () => {
+ loadFile(store, { basePath: 'not-existent.md' });
+
+ expect(store.dispatch.calls.count()).toBe(1);
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything());
+
+ expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
+ name: 'not-existent.md',
+ type: 'blob',
+ });
+ });
+ });
+
+ describe('loadBranch', () => {
+ const projectId = 'abc/def';
+ const branchId = '123-lorem';
+
+ it('fetches branch data', done => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
+
+ loadBranch(store, { projectId, branchId })
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['getMergeRequestsForBranch', { projectId, branchId }],
+ ['getFiles', { projectId, branchId }],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('shows an error if branch can not be fetched', done => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.reject());
+
+ loadBranch(store, { projectId, branchId })
+ .then(done.fail)
+ .catch(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['showBranchNotFoundError', branchId],
+ ]);
+ done();
+ });
+ });
+ });
+
describe('openBranch', () => {
+ const projectId = 'abc/def';
+ const branchId = '123-lorem';
+
const branch = {
- projectId: 'abc/def',
- branchId: '123-lorem',
+ projectId,
+ branchId,
};
beforeEach(() => {
- store.state.entries = {
- foo: { pending: false },
- 'foo/bar-pending': { pending: true },
- 'foo/bar': { pending: false },
- };
+ Object.assign(store.state, {
+ entries: {
+ foo: { pending: false },
+ 'foo/bar-pending': { pending: true },
+ 'foo/bar': { pending: false },
+ },
+ });
+ });
+
+ it('loads file right away if the branch has already been fetched', done => {
+ spyOn(store, 'dispatch');
+
+ Object.assign(store.state, {
+ projects: {
+ [projectId]: {
+ branches: {
+ [branchId]: { foo: 'bar' },
+ },
+ },
+ },
+ });
+
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([['loadFile', { basePath: undefined }]]);
+ })
+ .then(done)
+ .catch(done.fail);
});
describe('empty repo', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
- store.state.currentProjectId = 'abc/def';
- store.state.projects['abc/def'] = {
- empty_repo: true,
- };
+ Object.assign(store.state, {
+ currentProjectId: 'abc/def',
+ projects: {
+ 'abc/def': {
+ empty_repo: true,
+ },
+ },
+ });
});
afterEach(() => {
@@ -262,10 +377,7 @@ describe('IDE store project actions', () => {
it('dispatches showEmptyState action right away', done => {
openBranch(store, branch)
.then(() => {
- expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['showEmptyState', branch],
- ]);
+ expect(store.dispatch.calls.allArgs()).toEqual([['showEmptyState', branch]]);
done();
})
.catch(done.fail);
@@ -281,56 +393,14 @@ describe('IDE store project actions', () => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['getBranchData', branch],
- ['getMergeRequestsForBranch', branch],
- ['getFiles', branch],
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
+ ['loadFile', { basePath: undefined }],
]);
})
.then(done)
.catch(done.fail);
});
-
- it('handles tree entry action, if basePath is given', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar/' })
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- store.state.entries['foo/bar'],
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not handle tree entry action, if entry is pending', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
- .then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- jasmine.anything(),
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('creates a new file supplied via URL if the file does not exist yet', done => {
- openBranch(store, { ...branch, basePath: 'not-existent.md' })
- .then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- jasmine.anything(),
- );
-
- expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
- name: 'not-existent.md',
- type: 'blob',
- });
- })
- .then(done)
- .catch(done.fail);
- });
});
describe('non-existent branch', () => {
@@ -342,9 +412,8 @@ describe('IDE store project actions', () => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['getBranchData', branch],
- ['showBranchNotFoundError', branch.branchId],
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
]);
})
.then(done)
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 8504fb3f42b..7e77b859fdd 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -13,12 +13,15 @@ import actions, {
createTempEntry,
} from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter';
+import eventHub from '~/ide/eventhub';
+
+const store = createStore();
describe('Multi-file store actions', () => {
beforeEach(() => {
@@ -451,6 +454,24 @@ describe('Multi-file store actions', () => {
done,
);
});
+
+ it('does not dispatch for parent, if parent does not exist', done => {
+ const f = {
+ ...file(),
+ path: 'test',
+ parentPath: 'testing',
+ };
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [],
+ done,
+ );
+ });
});
describe('setCurrentBranchId', () => {
@@ -540,82 +561,298 @@ describe('Multi-file store actions', () => {
done,
);
});
- });
- describe('renameEntry', () => {
- it('renames entry', done => {
- store.state.entries.test = {
- tree: [],
+ it('if renamed, reverts the rename before deleting', () => {
+ const testEntry = {
+ path: 'test',
+ name: 'test',
+ prevPath: 'lorem/ipsum',
+ prevName: 'ipsum',
+ prevParentPath: 'lorem',
};
+ store.state.entries = { test: testEntry };
testAction(
- renameEntry,
- { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
+ deleteEntry,
+ testEntry.path,
store.state,
+ [],
[
{
- type: types.RENAME_ENTRY,
- payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
- },
- {
- type: types.TOGGLE_FILE_CHANGED,
+ type: 'renameEntry',
payload: {
- file: store.state.entries['parent-path/new-name'],
- changed: true,
+ path: testEntry.path,
+ name: testEntry.prevName,
+ parentPath: testEntry.prevParentPath,
},
},
+ {
+ type: 'deleteEntry',
+ payload: testEntry.prevPath,
+ },
],
- [{ type: 'triggerFilesChange' }],
- done,
);
});
+ });
- it('renames all entries in tree', done => {
- store.state.entries.test = {
- type: 'tree',
- tree: [
- {
- path: 'tree-1',
- },
- {
- path: 'tree-2',
+ describe('renameEntry', () => {
+ describe('purging of file model cache', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+ });
+
+ it('does not purge model cache for temporary entries that got renamed', done => {
+ Object.assign(store.state.entries, {
+ test: {
+ ...file('test'),
+ key: 'foo-key',
+ type: 'blob',
+ tempFile: true,
},
- ],
- };
+ });
- testAction(
- renameEntry,
- { path: 'test', name: 'new-name', parentPath: 'parent-path' },
- store.state,
- [
- {
- type: types.RENAME_ENTRY,
- payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
+ store
+ .dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ })
+ .then(() => {
+ expect(eventHub.$emit.calls.allArgs()).not.toContain(
+ 'editor.update.model.dispose.foo-bar',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('purges model cache for renamed entry', done => {
+ Object.assign(store.state.entries, {
+ test: {
+ ...file('test'),
+ key: 'foo-key',
+ type: 'blob',
+ tempFile: false,
},
- ],
- [
- {
- type: 'renameEntry',
- payload: {
- path: 'test',
- name: 'new-name',
- entryPath: 'tree-1',
- parentPath: 'parent-path/new-name',
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ })
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.foo-key`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('single entry', () => {
+ let origEntry;
+ let renamedEntry;
+
+ beforeEach(() => {
+ // Need to insert both because `testAction` doesn't actually call the mutation
+ origEntry = file('orig', 'orig', 'blob');
+ renamedEntry = {
+ ...file('renamed', 'renamed', 'blob'),
+ prevKey: origEntry.key,
+ prevName: origEntry.name,
+ prevPath: origEntry.path,
+ };
+
+ Object.assign(store.state.entries, {
+ orig: origEntry,
+ renamed: renamedEntry,
+ });
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ it('by default renames an entry and adds to changed', done => {
+ testAction(
+ renameEntry,
+ { path: 'orig', name: 'renamed' },
+ store.state,
+ [
+ {
+ type: types.RENAME_ENTRY,
+ payload: {
+ path: 'orig',
+ name: 'renamed',
+ parentPath: undefined,
+ },
},
- },
- {
- type: 'renameEntry',
- payload: {
- path: 'test',
- name: 'new-name',
- entryPath: 'tree-2',
- parentPath: 'parent-path/new-name',
+ {
+ type: types.ADD_FILE_TO_CHANGED,
+ payload: 'renamed',
+ },
+ ],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('if not changed, completely unstages entry if renamed to original', done => {
+ testAction(
+ renameEntry,
+ { path: 'renamed', name: 'orig' },
+ store.state,
+ [
+ {
+ type: types.RENAME_ENTRY,
+ payload: {
+ path: 'renamed',
+ name: 'orig',
+ parentPath: undefined,
+ },
+ },
+ {
+ type: types.REMOVE_FILE_FROM_STAGED_AND_CHANGED,
+ payload: origEntry,
+ },
+ ],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('if already in changed, does not add to change', done => {
+ store.state.changedFiles.push(renamedEntry);
+
+ testAction(
+ renameEntry,
+ { path: 'orig', name: 'renamed' },
+ store.state,
+ [jasmine.objectContaining({ type: types.RENAME_ENTRY })],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('routes to the renamed file if the original file has been opened', done => {
+ Object.assign(store.state.entries.orig, {
+ opened: true,
+ url: '/foo-bar.md',
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'orig',
+ name: 'renamed',
+ })
+ .then(() => {
+ expect(router.push.calls.count()).toBe(1);
+ expect(router.push).toHaveBeenCalledWith(`/project/foo-bar.md`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('folder', () => {
+ let folder;
+ let file1;
+ let file2;
+
+ beforeEach(() => {
+ folder = file('folder', 'folder', 'tree');
+ file1 = file('file-1', 'file-1', 'blob', folder);
+ file2 = file('file-2', 'file-2', 'blob', folder);
+
+ folder.tree = [file1, file2];
+
+ Object.assign(store.state.entries, {
+ [folder.path]: folder,
+ [file1.path]: file1,
+ [file2.path]: file2,
+ });
+ });
+
+ it('updates entries in a folder correctly, when folder is renamed', done => {
+ store
+ .dispatch('renameEntry', {
+ path: 'folder',
+ name: 'new-folder',
+ })
+ .then(() => {
+ const keys = Object.keys(store.state.entries);
+
+ expect(keys.length).toBe(3);
+ expect(keys.indexOf('new-folder')).toBe(0);
+ expect(keys.indexOf('new-folder/file-1')).toBe(1);
+ expect(keys.indexOf('new-folder/file-2')).toBe(2);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('discards renaming of an entry if the root folder is renamed back to a previous name', done => {
+ const rootFolder = file('old-folder', 'old-folder', 'tree');
+ const testEntry = file('test', 'test', 'blob', rootFolder);
+
+ Object.assign(store.state, {
+ entries: {
+ 'old-folder': {
+ ...rootFolder,
+ tree: [testEntry],
},
+ 'old-folder/test': testEntry,
},
- { type: 'triggerFilesChange' },
- ],
- done,
- );
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'old-folder',
+ name: 'new-folder',
+ })
+ .then(() => {
+ const { entries } = store.state;
+
+ expect(Object.keys(entries).length).toBe(2);
+ expect(entries['old-folder']).toBeUndefined();
+ expect(entries['old-folder/test']).toBeUndefined();
+
+ expect(entries['new-folder']).toBeDefined();
+ expect(entries['new-folder/test']).toEqual(
+ jasmine.objectContaining({
+ path: 'new-folder/test',
+ name: 'test',
+ prevPath: 'old-folder/test',
+ prevName: 'test',
+ }),
+ );
+ })
+ .then(() =>
+ store.dispatch('renameEntry', {
+ path: 'new-folder',
+ name: 'old-folder',
+ }),
+ )
+ .then(() => {
+ const { entries } = store.state;
+
+ expect(Object.keys(entries).length).toBe(2);
+ expect(entries['new-folder']).toBeUndefined();
+ expect(entries['new-folder/test']).toBeUndefined();
+
+ expect(entries['old-folder']).toBeDefined();
+ expect(entries['old-folder/test']).toEqual(
+ jasmine.objectContaining({
+ path: 'old-folder/test',
+ name: 'test',
+ prevPath: undefined,
+ prevName: undefined,
+ }),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index ffb97c85326..95d927065f0 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -1,5 +1,5 @@
import rootActions from '~/ide/stores/actions';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
@@ -11,6 +11,7 @@ import { resetStore, file } from 'spec/ide/helpers';
import testAction from '../../../../helpers/vuex_action_helper';
const TEST_COMMIT_SHA = '123456789';
+const store = createStore();
describe('IDE commit module actions', () => {
beforeEach(() => {
@@ -59,7 +60,9 @@ describe('IDE commit module actions', () => {
});
it('sets shouldCreateMR to true if "Create new MR" option is visible', done => {
- store.state.shouldHideNewMrOption = false;
+ Object.assign(store.state, {
+ shouldHideNewMrOption: false,
+ });
testAction(
actions.updateCommitAction,
@@ -78,7 +81,9 @@ describe('IDE commit module actions', () => {
});
it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => {
- store.state.shouldHideNewMrOption = true;
+ Object.assign(store.state, {
+ shouldHideNewMrOption: true,
+ });
testAction(
actions.updateCommitAction,
@@ -172,24 +177,31 @@ describe('IDE commit module actions', () => {
content: 'file content',
});
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
- web_url: 'web_url',
- branches: {
- master: {
- workingReference: '',
- commit: {
- short_id: TEST_COMMIT_SHA,
+ Object.assign(store.state, {
+ currentProjectId: 'abcproject',
+ currentBranchId: 'master',
+ projects: {
+ abcproject: {
+ web_url: 'web_url',
+ branches: {
+ master: {
+ workingReference: '',
+ commit: {
+ short_id: TEST_COMMIT_SHA,
+ },
+ },
},
},
},
- };
- store.state.stagedFiles.push(f, {
- ...file('changedFile2'),
- changed: true,
+ stagedFiles: [
+ f,
+ {
+ ...file('changedFile2'),
+ changed: true,
+ },
+ ],
+ openFiles: store.state.stagedFiles,
});
- store.state.openFiles = store.state.stagedFiles;
store.state.stagedFiles.forEach(stagedFile => {
store.state.entries[stagedFile.path] = stagedFile;
@@ -275,40 +287,40 @@ describe('IDE commit module actions', () => {
document.body.innerHTML += '<div class="flash-container"></div>';
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
- web_url: 'webUrl',
- branches: {
- master: {
- workingReference: '1',
- commit: {
- id: TEST_COMMIT_SHA,
- },
- },
- },
- };
-
const f = {
...file('changed'),
type: 'blob',
active: true,
lastCommitSha: TEST_COMMIT_SHA,
};
- store.state.stagedFiles.push(f);
- store.state.changedFiles = [
- {
- ...f,
- },
- ];
- store.state.openFiles = store.state.changedFiles;
- store.state.openFiles.forEach(localF => {
- store.state.entries[localF.path] = localF;
+ Object.assign(store.state, {
+ stagedFiles: [f],
+ changedFiles: [f],
+ openFiles: [f],
+ currentProjectId: 'abcproject',
+ currentBranchId: 'master',
+ projects: {
+ abcproject: {
+ web_url: 'webUrl',
+ branches: {
+ master: {
+ workingReference: '1',
+ commit: {
+ id: TEST_COMMIT_SHA,
+ },
+ },
+ },
+ },
+ },
});
store.state.commit.commitAction = '2';
store.state.commit.commitMessage = 'testing 123';
+
+ store.state.openFiles.forEach(localF => {
+ store.state.entries[localF.path] = localF;
+ });
});
afterEach(() => {
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 064e66cef64..7c46bf55318 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -356,16 +356,16 @@ describe('IDE store file mutations', () => {
});
describe('STAGE_CHANGE', () => {
- it('adds file into stagedFiles array', () => {
+ beforeEach(() => {
mutations.STAGE_CHANGE(localState, localFile.path);
+ });
+ it('adds file into stagedFiles array', () => {
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0]).toEqual(localFile);
});
it('updates stagedFile if it is already staged', () => {
- mutations.STAGE_CHANGE(localState, localFile.path);
-
localFile.raw = 'testing 123';
mutations.STAGE_CHANGE(localState, localFile.path);
@@ -373,19 +373,6 @@ describe('IDE store file mutations', () => {
expect(localState.stagedFiles.length).toBe(1);
expect(localState.stagedFiles[0].raw).toEqual('testing 123');
});
-
- it('adds already-staged file to `replacedFiles`', () => {
- localFile.raw = 'already-staged';
-
- mutations.STAGE_CHANGE(localState, localFile.path);
-
- localFile.raw = 'testing 123';
-
- mutations.STAGE_CHANGE(localState, localFile.path);
-
- expect(localState.replacedFiles.length).toBe(1);
- expect(localState.replacedFiles[0].raw).toEqual('already-staged');
- });
});
describe('UNSTAGE_CHANGE', () => {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 2470c99e300..7dd5d323f69 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -79,16 +79,6 @@ describe('Multi-file store mutations', () => {
});
});
- describe('CLEAR_REPLACED_FILES', () => {
- it('clears replacedFiles array', () => {
- localState.replacedFiles.push('a');
-
- mutations.CLEAR_REPLACED_FILES(localState);
-
- expect(localState.replacedFiles.length).toBe(0);
- });
- });
-
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
@@ -311,8 +301,7 @@ describe('Multi-file store mutations', () => {
describe('UPDATE_FILE_AFTER_COMMIT', () => {
it('updates URLs if prevPath is set', () => {
const f = {
- ...file(),
- path: 'test',
+ ...file('test'),
prevPath: 'testing-123',
rawPath: `${gl.TEST_HOST}/testing-123`,
permalink: `${gl.TEST_HOST}/testing-123`,
@@ -325,19 +314,26 @@ describe('Multi-file store mutations', () => {
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`);
- expect(f.replaces).toBe(false);
+ expect(f).toEqual(
+ jasmine.objectContaining({
+ rawPath: `${gl.TEST_HOST}/test`,
+ permalink: `${gl.TEST_HOST}/test`,
+ commitsPath: `${gl.TEST_HOST}/test`,
+ blamePath: `${gl.TEST_HOST}/test`,
+ replaces: false,
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ }),
+ );
});
});
describe('OPEN_NEW_ENTRY_MODAL', () => {
it('sets entryModal', () => {
- localState.entries.testPath = {
- ...file(),
- };
+ localState.entries.testPath = file();
mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' });
@@ -356,58 +352,178 @@ describe('Multi-file store mutations', () => {
};
localState.currentProjectId = 'gitlab-ce';
localState.currentBranchId = 'master';
- localState.entries.oldPath = {
- ...file(),
- type: 'blob',
- name: 'oldPath',
- path: 'oldPath',
- url: `${gl.TEST_HOST}/oldPath`,
+ localState.entries = {
+ oldPath: file('oldPath', 'oldPath', 'blob'),
};
});
- it('creates new renamed entry', () => {
+ it('updates existing entry without creating a new one', () => {
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ parentPath: '',
+ });
+
+ expect(localState.entries).toEqual({
+ newPath: jasmine.objectContaining({
+ path: 'newPath',
+ prevPath: 'oldPath',
+ }),
+ });
+ });
+
+ it('correctly handles consecutive renames for the same entry', () => {
mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
name: 'newPath',
+ parentPath: '',
+ });
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'newPath',
+ name: 'newestPath',
+ parentPath: '',
+ });
+
+ expect(localState.entries).toEqual({
+ newestPath: jasmine.objectContaining({
+ path: 'newestPath',
+ prevPath: 'oldPath',
+ }),
+ });
+ });
+
+ it('correctly handles the same entry within a consecutively renamed folder', () => {
+ const oldPath = file('root-folder/oldPath', 'root-folder/oldPath', 'blob');
+ localState.entries = {
+ 'root-folder': {
+ ...file('root-folder', 'root-folder', 'tree'),
+ tree: [oldPath],
+ },
+ 'root-folder/oldPath': oldPath,
+ };
+ Object.assign(localState.entries['root-folder/oldPath'], {
+ parentPath: 'root-folder',
+ url: 'root-folder/oldPath-blob-root-folder/oldPath',
+ });
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'root-folder/oldPath',
+ name: 'renamed-folder/oldPath',
entryPath: null,
parentPath: '',
});
+ mutations.RENAME_ENTRY(localState, {
+ path: 'renamed-folder/oldPath',
+ name: 'simply-renamed/oldPath',
+ entryPath: null,
+ parentPath: '',
+ });
+
+ expect(localState.entries).toEqual({
+ 'root-folder': jasmine.objectContaining({
+ path: 'root-folder',
+ }),
+ 'simply-renamed/oldPath': jasmine.objectContaining({
+ path: 'simply-renamed/oldPath',
+ prevPath: 'root-folder/oldPath',
+ }),
+ });
+ });
+
+ it('renames entry, preserving old parameters', () => {
+ Object.assign(localState.entries.oldPath, {
+ url: `project/-/oldPath`,
+ });
+ const oldPathData = localState.entries.oldPath;
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ parentPath: '',
+ });
+
expect(localState.entries.newPath).toEqual({
- ...localState.entries.oldPath,
+ ...oldPathData,
id: 'newPath',
- name: 'newPath',
- key: 'newPath-blob-oldPath',
path: 'newPath',
- tempFile: true,
+ name: 'newPath',
+ url: `project/-/newPath`,
+ key: jasmine.stringMatching('newPath'),
+
+ prevId: 'oldPath',
+ prevName: 'oldPath',
prevPath: 'oldPath',
- tree: [],
- parentPath: '',
- url: `${gl.TEST_HOST}/newPath`,
- moved: jasmine.anything(),
- movedPath: jasmine.anything(),
- opened: false,
+ prevUrl: `project/-/oldPath`,
+ prevKey: oldPathData.key,
+ prevParentPath: oldPathData.parentPath,
});
});
- it('adds new entry to changedFiles', () => {
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+ it('does not store previous attributes on temp files', () => {
+ Object.assign(localState.entries.oldPath, {
+ tempFile: true,
+ });
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ entryPath: null,
+ parentPath: '',
+ });
- expect(localState.changedFiles.length).toBe(1);
- expect(localState.changedFiles[0].path).toBe('newPath');
- });
+ expect(localState.entries.newPath).not.toEqual(
+ jasmine.objectContaining({
+ prevId: jasmine.anything(),
+ prevName: jasmine.anything(),
+ prevPath: jasmine.anything(),
+ prevUrl: jasmine.anything(),
+ prevKey: jasmine.anything(),
+ prevParentPath: jasmine.anything(),
+ }),
+ );
+ });
+
+ it('properly handles files with spaces in name', () => {
+ const path = 'my fancy path';
+ const newPath = 'new path';
+ const oldEntry = {
+ ...file(path, path, 'blob'),
+ url: `project/-/${encodeURI(path)}`,
+ };
- it('sets oldEntry as moved', () => {
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+ localState.entries[path] = oldEntry;
- expect(localState.entries.oldPath.moved).toBe(true);
+ mutations.RENAME_ENTRY(localState, {
+ path,
+ name: newPath,
+ entryPath: null,
+ parentPath: '',
+ });
+
+ expect(localState.entries[newPath]).toEqual({
+ ...oldEntry,
+ id: newPath,
+ path: newPath,
+ name: newPath,
+ url: `project/-/new%20path`,
+ key: jasmine.stringMatching(newPath),
+
+ prevId: path,
+ prevName: path,
+ prevPath: path,
+ prevUrl: `project/-/my%20fancy%20path`,
+ prevKey: oldEntry.key,
+ prevParentPath: oldEntry.parentPath,
+ });
});
- it('adds to parents tree', () => {
- localState.entries.oldPath.parentPath = 'parentPath';
- localState.entries.parentPath = {
- ...file(),
+ it('adds to parent tree', () => {
+ const parentEntry = {
+ ...file('parentPath', 'parentPath', 'tree'),
+ tree: [localState.entries.oldPath],
};
+ localState.entries.parentPath = parentEntry;
mutations.RENAME_ENTRY(localState, {
path: 'oldPath',
@@ -416,7 +532,180 @@ describe('Multi-file store mutations', () => {
parentPath: 'parentPath',
});
- expect(localState.entries.parentPath.tree.length).toBe(1);
+ expect(parentEntry.tree.length).toBe(1);
+ expect(parentEntry.tree[0].name).toBe('newPath');
+ });
+
+ it('sorts tree after renaming an entry', () => {
+ const alpha = file('alpha', 'alpha', 'blob');
+ const beta = file('beta', 'beta', 'blob');
+ const gamma = file('gamma', 'gamma', 'blob');
+ localState.entries = { alpha, beta, gamma };
+
+ localState.trees['gitlab-ce/master'].tree = [alpha, beta, gamma];
+
+ mutations.RENAME_ENTRY(localState, {
+ path: 'alpha',
+ name: 'theta',
+ entryPath: null,
+ parentPath: '',
+ });
+
+ expect(localState.trees['gitlab-ce/master'].tree).toEqual([
+ jasmine.objectContaining({ name: 'beta' }),
+ jasmine.objectContaining({ name: 'gamma' }),
+ jasmine.objectContaining({
+ path: 'theta',
+ name: 'theta',
+ }),
+ ]);
+ });
+
+ it('updates openFiles with the renamed one if the original one is open', () => {
+ Object.assign(localState.entries.oldPath, {
+ opened: true,
+ type: 'blob',
+ });
+ Object.assign(localState, {
+ openFiles: [localState.entries.oldPath],
+ });
+
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.openFiles.length).toBe(1);
+ expect(localState.openFiles[0].path).toBe('newPath');
+ });
+
+ it('does not add renamed entry to changedFiles', () => {
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+
+ it('updates existing changedFiles entry with the renamed one', () => {
+ const origFile = {
+ ...file('oldPath', 'oldPath', 'blob'),
+ content: 'Foo',
+ };
+
+ Object.assign(localState, {
+ changedFiles: [origFile],
+ });
+ Object.assign(localState.entries, {
+ oldPath: origFile,
+ });
+
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.changedFiles).toEqual([
+ jasmine.objectContaining({
+ path: 'newPath',
+ content: 'Foo',
+ }),
+ ]);
+ });
+
+ it('correctly saves original values if an entry is renamed multiple times', () => {
+ const original = { ...localState.entries.oldPath };
+ const paramsToCheck = ['prevId', 'prevPath', 'prevName', 'prevUrl'];
+ const expectedObj = paramsToCheck.reduce(
+ (o, param) => ({ ...o, [param]: original[param.replace('prev', '').toLowerCase()] }),
+ {},
+ );
+
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+ expect(localState.entries.newPath).toEqual(jasmine.objectContaining(expectedObj));
+
+ mutations.RENAME_ENTRY(localState, { path: 'newPath', name: 'newer' });
+
+ expect(localState.entries.newer).toEqual(jasmine.objectContaining(expectedObj));
+ });
+
+ describe('renaming back to original', () => {
+ beforeEach(() => {
+ const renamedEntry = {
+ ...file('renamed', 'renamed', 'blob'),
+ prevId: 'lorem/orig',
+ prevPath: 'lorem/orig',
+ prevName: 'orig',
+ prevUrl: 'project/-/loren/orig',
+ prevKey: 'lorem/orig',
+ prevParentPath: 'lorem',
+ };
+
+ localState.entries = {
+ renamed: renamedEntry,
+ };
+
+ mutations.RENAME_ENTRY(localState, { path: 'renamed', name: 'orig', parentPath: 'lorem' });
+ });
+
+ it('renames entry and clears prev properties', () => {
+ expect(localState.entries).toEqual({
+ 'lorem/orig': jasmine.objectContaining({
+ id: 'lorem/orig',
+ path: 'lorem/orig',
+ name: 'orig',
+ prevId: undefined,
+ prevPath: undefined,
+ prevName: undefined,
+ prevUrl: undefined,
+ prevKey: undefined,
+ prevParentPath: undefined,
+ }),
+ });
+ });
+ });
+
+ describe('key updates', () => {
+ beforeEach(() => {
+ const rootFolder = file('rootFolder', 'rootFolder', 'tree');
+ localState.entries = {
+ rootFolder,
+ oldPath: file('oldPath', 'oldPath', 'blob'),
+ 'oldPath.txt': file('oldPath.txt', 'oldPath.txt', 'blob'),
+ 'rootFolder/oldPath.md': file('oldPath.md', 'oldPath.md', 'blob', rootFolder),
+ };
+ });
+
+ it('sets properly constucted key while preserving the original one', () => {
+ const key = 'oldPath.txt-blob-oldPath.txt';
+ localState.entries['oldPath.txt'].key = key;
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath.md' });
+
+ expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md');
+ expect(localState.entries['newPath.md'].prevKey).toBe(key);
+ });
+
+ it('correctly updates key for an entry without an extension', () => {
+ localState.entries.oldPath.key = 'oldPath-blob-oldPath';
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath.md' });
+
+ expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md');
+ });
+
+ it('correctly updates key when new name does not have an extension', () => {
+ localState.entries['oldPath.txt'].key = 'oldPath.txt-blob-oldPath.txt';
+ mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath' });
+
+ expect(localState.entries.newPath.key).toBe('newPath-blob-newPath');
+ });
+
+ it('correctly updates key when renaming an entry in a folder', () => {
+ localState.entries['rootFolder/oldPath.md'].key =
+ 'rootFolder/oldPath.md-blob-rootFolder/oldPath.md';
+ mutations.RENAME_ENTRY(localState, {
+ path: 'rootFolder/oldPath.md',
+ name: 'newPath.md',
+ entryPath: null,
+ parentPath: 'rootFolder',
+ });
+
+ expect(localState.entries['rootFolder/newPath.md'].key).toBe(
+ 'rootFolder/newPath.md-blob-rootFolder/newPath.md',
+ );
+ });
});
});
});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index 0fc9519a6bf..a477d4fc200 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -237,31 +237,6 @@ describe('Multi-file store utils', () => {
});
describe('getCommitFiles', () => {
- it('returns list of files excluding moved files', () => {
- const files = [
- {
- path: 'a',
- type: 'blob',
- deleted: true,
- },
- {
- path: 'c',
- type: 'blob',
- moved: true,
- },
- ];
-
- const flattendFiles = utils.getCommitFiles(files);
-
- expect(flattendFiles).toEqual([
- {
- path: 'a',
- type: 'blob',
- deleted: true,
- },
- ]);
- });
-
it('filters out folders from the list', () => {
const files = [
{
@@ -422,4 +397,204 @@ describe('Multi-file store utils', () => {
expect(res[1].tree[0].opened).toEqual(true);
});
});
+
+ describe('escapeFileUrl', () => {
+ it('encodes URL excluding the slashes', () => {
+ expect(utils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md');
+ expect(utils.escapeFileUrl('foo bar/file.md')).toBe('foo%20bar/file.md');
+ expect(utils.escapeFileUrl('foo/bar/file.md')).toBe('foo/bar/file.md');
+ });
+ });
+
+ describe('swapInStateArray', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = [];
+ });
+
+ it('swaps existing entry with a new one', () => {
+ const file1 = {
+ ...file('old'),
+ key: 'foo',
+ };
+ const file2 = file('new');
+ const arr = [file1];
+
+ Object.assign(localState, {
+ dummyArray: arr,
+ entries: {
+ new: file2,
+ },
+ });
+
+ utils.swapInStateArray(localState, 'dummyArray', 'foo', 'new');
+
+ expect(localState.dummyArray.length).toBe(1);
+ expect(localState.dummyArray[0]).toBe(file2);
+ });
+
+ it('does not add an item if it does not exist yet in array', () => {
+ const file1 = file('file');
+ Object.assign(localState, {
+ dummyArray: [],
+ entries: {
+ file: file1,
+ },
+ });
+
+ utils.swapInStateArray(localState, 'dummyArray', 'foo', 'file');
+
+ expect(localState.dummyArray.length).toBe(0);
+ });
+ });
+
+ describe('swapInParentTreeWithSorting', () => {
+ let localState;
+ let branchInfo;
+ const currentProjectId = '123-foo';
+ const currentBranchId = 'master';
+
+ beforeEach(() => {
+ localState = {
+ currentBranchId,
+ currentProjectId,
+ trees: {
+ [`${currentProjectId}/${currentBranchId}`]: {
+ tree: [],
+ },
+ },
+ entries: {
+ oldPath: file('oldPath', 'oldPath', 'blob'),
+ newPath: file('newPath', 'newPath', 'blob'),
+ parentPath: file('parentPath', 'parentPath', 'tree'),
+ },
+ };
+ branchInfo = localState.trees[`${currentProjectId}/${currentBranchId}`];
+ });
+
+ it('does not change tree if newPath is not supplied', () => {
+ branchInfo.tree = [localState.entries.oldPath];
+
+ utils.swapInParentTreeWithSorting(localState, 'oldPath', undefined, undefined);
+
+ expect(branchInfo.tree).toEqual([localState.entries.oldPath]);
+ });
+
+ describe('oldPath to replace is not defined: simple addition to tree', () => {
+ it('adds to tree on the state if there is no parent for the entry', () => {
+ expect(branchInfo.tree.length).toBe(0);
+
+ utils.swapInParentTreeWithSorting(localState, undefined, 'oldPath', undefined);
+
+ expect(branchInfo.tree.length).toBe(1);
+ expect(branchInfo.tree[0].name).toBe('oldPath');
+
+ utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', undefined);
+
+ expect(branchInfo.tree.length).toBe(2);
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'newPath' }),
+ jasmine.objectContaining({ name: 'oldPath' }),
+ ]);
+ });
+
+ it('adds to parent tree if it is supplied', () => {
+ utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath');
+
+ expect(localState.entries.parentPath.tree.length).toBe(1);
+ expect(localState.entries.parentPath.tree).toEqual([
+ jasmine.objectContaining({ name: 'newPath' }),
+ ]);
+
+ localState.entries.parentPath.tree = [localState.entries.oldPath];
+
+ utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath');
+
+ expect(localState.entries.parentPath.tree.length).toBe(2);
+ expect(localState.entries.parentPath.tree).toEqual([
+ jasmine.objectContaining({ name: 'newPath' }),
+ jasmine.objectContaining({ name: 'oldPath' }),
+ ]);
+ });
+ });
+
+ describe('swapping of the items', () => {
+ it('swaps entries if both paths are supplied', () => {
+ branchInfo.tree = [localState.entries.oldPath];
+
+ utils.swapInParentTreeWithSorting(localState, localState.entries.oldPath.key, 'newPath');
+
+ expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'newPath' })]);
+
+ utils.swapInParentTreeWithSorting(localState, localState.entries.newPath.key, 'oldPath');
+
+ expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'oldPath' })]);
+ });
+
+ it('sorts tree after swapping the entries', () => {
+ const alpha = file('alpha', 'alpha', 'blob');
+ const beta = file('beta', 'beta', 'blob');
+ const gamma = file('gamma', 'gamma', 'blob');
+ const theta = file('theta', 'theta', 'blob');
+ localState.entries = { alpha, beta, gamma, theta };
+
+ branchInfo.tree = [alpha, beta, gamma];
+
+ utils.swapInParentTreeWithSorting(localState, alpha.key, 'theta');
+
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'beta' }),
+ jasmine.objectContaining({ name: 'gamma' }),
+ jasmine.objectContaining({ name: 'theta' }),
+ ]);
+
+ utils.swapInParentTreeWithSorting(localState, gamma.key, 'alpha');
+
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'alpha' }),
+ jasmine.objectContaining({ name: 'beta' }),
+ jasmine.objectContaining({ name: 'theta' }),
+ ]);
+
+ utils.swapInParentTreeWithSorting(localState, beta.key, 'gamma');
+
+ expect(branchInfo.tree).toEqual([
+ jasmine.objectContaining({ name: 'alpha' }),
+ jasmine.objectContaining({ name: 'gamma' }),
+ jasmine.objectContaining({ name: 'theta' }),
+ ]);
+ });
+ });
+ });
+
+ describe('cleanTrailingSlash', () => {
+ [
+ { input: '', output: '' },
+ { input: 'abc', output: 'abc' },
+ { input: 'abc/', output: 'abc' },
+ { input: 'abc/def', output: 'abc/def' },
+ { input: 'abc/def/', output: 'abc/def' },
+ ].forEach(({ input, output }) => {
+ it(`cleans trailing slash from string "${input}"`, () => {
+ expect(utils.cleanTrailingSlash(input)).toEqual(output);
+ });
+ });
+ });
+
+ describe('pathsAreEqual', () => {
+ [
+ { args: ['abc', 'abc'], output: true },
+ { args: ['abc', 'def'], output: false },
+ { args: ['abc/', 'abc'], output: true },
+ { args: ['abc/abc', 'abc'], output: false },
+ { args: ['/', ''], output: true },
+ { args: ['', '/'], output: true },
+ { args: [false, '/'], output: true },
+ ].forEach(({ args, output }) => {
+ it(`cleans and tests equality (${JSON.stringify(args)})`, () => {
+ expect(utils.pathsAreEqual(...args)).toEqual(output);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/lazy_loader_spec.js b/spec/javascripts/lazy_loader_spec.js
index f3fb792c62d..82ab73c2170 100644
--- a/spec/javascripts/lazy_loader_spec.js
+++ b/spec/javascripts/lazy_loader_spec.js
@@ -62,7 +62,7 @@ describe('LazyLoader', function() {
waitForAttributeChange(newImg, ['data-src', 'src']),
])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(testPath);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
@@ -79,7 +79,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -98,7 +98,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -121,7 +121,7 @@ describe('LazyLoader', function() {
])
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
})
@@ -156,7 +156,7 @@ describe('LazyLoader', function() {
Promise.all([scrollIntoViewPromise(img), waitForAttributeChange(img, ['data-src', 'src'])])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(originalDataSrc);
expect(img).toHaveClass('js-lazy-loaded');
done();
@@ -176,7 +176,7 @@ describe('LazyLoader', function() {
waitForAttributeChange(newImg, ['data-src', 'src']),
])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(testPath);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
@@ -193,7 +193,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -212,7 +212,7 @@ describe('LazyLoader', function() {
scrollIntoViewPromise(newImg)
.then(waitForPromises)
.then(() => {
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
+ expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
})
@@ -234,7 +234,7 @@ describe('LazyLoader', function() {
waitForAttributeChange(newImg, ['data-src', 'src']),
])
.then(() => {
- expect(LazyLoader.loadImage).toHaveBeenCalled();
+ expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
})
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
index 6abcac5c0ff..7da69e3fa84 100644
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ b/spec/javascripts/vue_shared/components/file_row_spec.js
@@ -90,19 +90,6 @@ describe('File row component', () => {
expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null);
});
- it('is not rendered for `moved` entries in subfolders', () => {
- createComponent({
- file: {
- path: 't5',
- moved: true,
- tree: [],
- },
- level: 2,
- });
-
- expect(vm.$el.nodeType).not.toEqual(1);
- });
-
describe('new dropdown', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 7e318017a05..bb4f71982a0 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -150,6 +150,7 @@ describe Gitlab do
describe '.ee?' do
before do
+ stub_env('IS_GITLAB_EE', nil) # Make sure the ENV is clean
described_class.instance_variable_set(:@is_ee, nil)
end