Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0a6b019047
commit
a928c5170f
|
@ -1 +1 @@
|
|||
e4ff30e44b6ac21f33290bbe7a9cbbd42f98d4d1
|
||||
e9860f7988a2c87638abf695d8613e3096312857
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlButton,
|
||||
},
|
||||
inject: {
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
},
|
||||
svgPath: {
|
||||
type: String,
|
||||
},
|
||||
docsLink: {
|
||||
type: String,
|
||||
},
|
||||
primaryButtonPath: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath">
|
||||
<template #description>
|
||||
<gl-sprintf
|
||||
v-if="!isAdmin"
|
||||
:message="
|
||||
__(
|
||||
'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #docLink="{content}">
|
||||
<gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<template v-else
|
||||
><p>
|
||||
{{ __('Turn on usage ping to review instance-level analytics.') }}
|
||||
</p>
|
||||
|
||||
<gl-button category="primary" variant="success" :href="primaryButtonPath">
|
||||
{{ __('Turn on usage ping') }}</gl-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
|
@ -468,7 +468,7 @@ export default {
|
|||
|
||||
<div
|
||||
:data-can-create-note="getNoteableData.current_user.can_create_note"
|
||||
class="files d-flex"
|
||||
class="files d-flex gl-mt-2"
|
||||
>
|
||||
<div
|
||||
v-if="showTreeList"
|
||||
|
|
|
@ -133,6 +133,7 @@ export default {
|
|||
'toggleFileDiscussions',
|
||||
'toggleFileDiscussionWrappers',
|
||||
'toggleFullDiff',
|
||||
'toggleActiveFileByHash',
|
||||
]),
|
||||
handleToggleFile() {
|
||||
this.$emit('toggleFile');
|
||||
|
@ -149,6 +150,9 @@ export default {
|
|||
const selector = this.diffContentIDSelector;
|
||||
scrollToElement(document.querySelector(selector));
|
||||
window.location.hash = selector;
|
||||
if (!this.viewDiffsFileByFile) {
|
||||
this.toggleActiveFileByHash(this.diffFile.file_hash);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
|
||||
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
|
||||
*/
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import FileRow from '~/vue_shared/components/file_row.vue';
|
||||
import FileRowStats from './file_row_stats.vue';
|
||||
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
|
||||
import FileRowStats from './file_row_stats.vue';
|
||||
|
||||
export default {
|
||||
name: 'DiffFileRow',
|
||||
|
@ -14,6 +15,7 @@ export default {
|
|||
FileRowStats,
|
||||
ChangedFileIcon,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
|
@ -28,11 +30,28 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
viewedFiles: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showFileRowStats() {
|
||||
return !this.hideFileStats && this.file.type === 'blob';
|
||||
},
|
||||
fileClasses() {
|
||||
if (!this.glFeatures.highlightCurrentDiffRow) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.file.type === 'blob' && !this.viewedFiles[this.file.fileHash]
|
||||
? 'gl-font-weight-bold'
|
||||
: '';
|
||||
},
|
||||
isActive() {
|
||||
return this.currentDiffFileId === this.file.fileHash;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -41,8 +60,9 @@ export default {
|
|||
<file-row
|
||||
:file="file"
|
||||
v-bind="$attrs"
|
||||
:class="{ 'is-active': currentDiffFileId === file.fileHash }"
|
||||
:class="{ 'is-active': isActive }"
|
||||
class="diff-file-row"
|
||||
:file-classes="fileClasses"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<file-row-stats v-if="showFileRowStats" :file="file" class="mr-1" />
|
||||
|
|
|
@ -25,7 +25,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId']),
|
||||
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
|
||||
...mapGetters('diffs', ['allBlobs']),
|
||||
filteredTreeList() {
|
||||
const search = this.search.toLowerCase().trim();
|
||||
|
@ -93,6 +93,7 @@ export default {
|
|||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
:viewed-files="viewedDiffFileIds"
|
||||
:hide-file-stats="hideFileStats"
|
||||
:file-row-component="$options.DiffFileRow"
|
||||
:current-diff-file-id="currentDiffFileId"
|
||||
|
|
|
@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3;
|
|||
export const LENGTH_OF_AVATAR_TOOLTIP = 17;
|
||||
|
||||
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
|
||||
export const MAX_LINES_TO_BE_RENDERED = 2000;
|
||||
|
||||
export const DIFF_FILE_SYMLINK_MODE = '120000';
|
||||
export const DIFF_FILE_DELETED_MODE = '0';
|
||||
|
|
|
@ -84,7 +84,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
|
|||
commit(types.SET_BATCH_LOADING, false);
|
||||
|
||||
if (!isNoteLink && !state.currentDiffFileId) {
|
||||
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, diff_files[0].file_hash);
|
||||
commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
|
||||
}
|
||||
|
||||
if (isNoteLink) {
|
||||
|
@ -100,7 +100,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
|
|||
!state.diffFiles.some(f => f.file_hash === state.currentDiffFileId) &&
|
||||
!isNoteLink
|
||||
) {
|
||||
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, state.diffFiles[0].file_hash);
|
||||
commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash);
|
||||
}
|
||||
|
||||
if (gon.features?.codeNavigation) {
|
||||
|
@ -183,7 +183,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
|
|||
export const setHighlightedRow = ({ commit }, lineCode) => {
|
||||
const fileHash = lineCode.split('_')[0];
|
||||
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
|
||||
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
|
||||
commit(types.VIEW_DIFF_FILE, fileHash);
|
||||
};
|
||||
|
||||
// This is adding line discussions to the actual lines in the diff tree
|
||||
|
@ -428,13 +428,17 @@ export const toggleTreeOpen = ({ commit }, path) => {
|
|||
commit(types.TOGGLE_FOLDER_OPEN, path);
|
||||
};
|
||||
|
||||
export const toggleActiveFileByHash = ({ commit }, hash) => {
|
||||
commit(types.VIEW_DIFF_FILE, hash);
|
||||
};
|
||||
|
||||
export const scrollToFile = ({ state, commit }, path) => {
|
||||
if (!state.treeEntries[path]) return;
|
||||
|
||||
const { fileHash } = state.treeEntries[path];
|
||||
document.location.hash = fileHash;
|
||||
|
||||
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
|
||||
commit(types.VIEW_DIFF_FILE, fileHash);
|
||||
};
|
||||
|
||||
export const toggleShowTreeList = ({ commit, state }, saving = true) => {
|
||||
|
@ -702,7 +706,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not
|
|||
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
|
||||
|
||||
if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) {
|
||||
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
|
||||
commit(types.VIEW_DIFF_FILE, fileHash);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -710,5 +714,5 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
|
|||
const fileHash = state.diffFiles[index].file_hash;
|
||||
document.location.hash = fileHash;
|
||||
|
||||
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
|
||||
commit(types.VIEW_DIFF_FILE, fileHash);
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ export default () => ({
|
|||
showTreeList: true,
|
||||
currentDiffFileId: '',
|
||||
projectPath: '',
|
||||
viewedDiffFileIds: {},
|
||||
commentForms: [],
|
||||
highlightedRow: null,
|
||||
renderTreeList: true,
|
||||
|
|
|
@ -19,7 +19,7 @@ export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
|
|||
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
|
||||
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
|
||||
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
|
||||
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
|
||||
export const VIEW_DIFF_FILE = 'VIEW_DIFF_FILE';
|
||||
|
||||
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
|
||||
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { PARALLEL_DIFF_VIEW_TYPE } from '../constants';
|
||||
import {
|
||||
|
@ -291,8 +292,9 @@ export default {
|
|||
[types.TOGGLE_SHOW_TREE_LIST](state) {
|
||||
state.showTreeList = !state.showTreeList;
|
||||
},
|
||||
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
|
||||
[types.VIEW_DIFF_FILE](state, fileId) {
|
||||
state.currentDiffFileId = fileId;
|
||||
Vue.set(state.viewedDiffFileIds, fileId, true);
|
||||
},
|
||||
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
|
||||
state.commentForms.push({
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
OLD_LINE_TYPE,
|
||||
MATCH_LINE_TYPE,
|
||||
LINES_TO_BE_RENDERED_DIRECTLY,
|
||||
MAX_LINES_TO_BE_RENDERED,
|
||||
TREE_TYPE,
|
||||
INLINE_DIFF_VIEW_TYPE,
|
||||
PARALLEL_DIFF_VIEW_TYPE,
|
||||
|
@ -457,12 +456,10 @@ function getVisibleDiffLines(file) {
|
|||
}
|
||||
|
||||
function finalizeDiffFile(file) {
|
||||
const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
|
||||
const lines = getVisibleDiffLines(file);
|
||||
|
||||
Object.assign(file, {
|
||||
renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
|
||||
collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
|
||||
isShowingFullFile: false,
|
||||
isLoadingFullFile: false,
|
||||
discussions: [],
|
||||
|
|
|
@ -89,6 +89,14 @@ export default {
|
|||
return this.requestCount !== 0;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// This method is defined here instead of in `methods`
|
||||
// because we need to access the .cancel() method
|
||||
// lodash attaches to the function, which is
|
||||
// made inaccessible by Vue. More info:
|
||||
// https://stackoverflow.com/a/52988020/1063392
|
||||
this.debouncedSearchMilestones = debounce(this.searchMilestones, 100);
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMilestones();
|
||||
},
|
||||
|
@ -108,7 +116,7 @@ export default {
|
|||
this.requestCount -= 1;
|
||||
});
|
||||
},
|
||||
searchMilestones: debounce(function searchMilestones() {
|
||||
searchMilestones() {
|
||||
this.requestCount += 1;
|
||||
const options = {
|
||||
search: this.searchQuery,
|
||||
|
@ -133,7 +141,14 @@ export default {
|
|||
.finally(() => {
|
||||
this.requestCount -= 1;
|
||||
});
|
||||
}, 100),
|
||||
},
|
||||
onSearchBoxInput() {
|
||||
this.debouncedSearchMilestones();
|
||||
},
|
||||
onSearchBoxEnter() {
|
||||
this.debouncedSearchMilestones.cancel();
|
||||
this.searchMilestones();
|
||||
},
|
||||
toggleMilestoneSelection(clickedMilestone) {
|
||||
if (!clickedMilestone) return [];
|
||||
|
||||
|
@ -186,7 +201,8 @@ export default {
|
|||
v-model.trim="searchQuery"
|
||||
class="gl-m-3"
|
||||
:placeholder="this.$options.translations.searchMilestones"
|
||||
@input="searchMilestones"
|
||||
@input="onSearchBoxInput"
|
||||
@keydown.enter.prevent="onSearchBoxEnter"
|
||||
/>
|
||||
|
||||
<gl-new-dropdown-item @click="onMilestoneClicked(null)">
|
||||
|
|
|
@ -1,3 +1,27 @@
|
|||
import Vue from 'vue';
|
||||
import UserCallout from '~/user_callout';
|
||||
import UsagePingDisabled from '~/admin/dev_ops_score/components/usage_ping_disabled.vue';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new UserCallout());
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// eslint-disable-next-line no-new
|
||||
new UserCallout();
|
||||
|
||||
const emptyStateContainer = document.getElementById('js-devops-empty-state');
|
||||
|
||||
if (!emptyStateContainer) return false;
|
||||
|
||||
const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
|
||||
|
||||
return new Vue({
|
||||
el: emptyStateContainer,
|
||||
provide: {
|
||||
isAdmin: Boolean(isAdmin),
|
||||
svgPath: emptyStateSvgPath,
|
||||
primaryButtonPath: enableUsagePingLink,
|
||||
docsLink,
|
||||
},
|
||||
render(h) {
|
||||
return h(UsagePingDisabled);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -87,6 +87,15 @@ export default {
|
|||
},
|
||||
},
|
||||
created() {
|
||||
// This method is defined here instead of in `methods`
|
||||
// because we need to access the .cancel() method
|
||||
// lodash attaches to the function, which is
|
||||
// made inaccessible by Vue. More info:
|
||||
// https://stackoverflow.com/a/52988020/1063392
|
||||
this.debouncedSearch = debounce(function search() {
|
||||
this.search(this.query);
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
|
||||
this.setProjectId(this.projectId);
|
||||
this.search(this.query);
|
||||
},
|
||||
|
@ -95,9 +104,13 @@ export default {
|
|||
focusSearchBox() {
|
||||
this.$refs.searchBox.$el.querySelector('input').focus();
|
||||
},
|
||||
onSearchBoxInput: debounce(function search() {
|
||||
onSearchBoxEnter() {
|
||||
this.debouncedSearch.cancel();
|
||||
this.search(this.query);
|
||||
}, SEARCH_DEBOUNCE_MS),
|
||||
},
|
||||
onSearchBoxInput() {
|
||||
this.debouncedSearch();
|
||||
},
|
||||
selectRef(ref) {
|
||||
this.setSelectedRef(ref);
|
||||
this.$emit('input', this.selectedRef);
|
||||
|
@ -129,6 +142,7 @@ export default {
|
|||
class="gl-m-3"
|
||||
:placeholder="i18n.searchPlaceholder"
|
||||
@input="onSearchBoxInput"
|
||||
@keydown.enter.prevent="onSearchBoxEnter"
|
||||
/>
|
||||
|
||||
<div class="gl-flex-grow-1 gl-overflow-y-auto">
|
||||
|
|
|
@ -140,7 +140,7 @@ export default {
|
|||
class="form-control"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group class="w-50" data-testid="milestones-field" @keydown.enter.prevent.capture>
|
||||
<gl-form-group class="w-50" data-testid="milestones-field">
|
||||
<label>{{ __('Milestones') }}</label>
|
||||
<div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
|
||||
<milestone-combobox
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
const parseSourceFile = raw => {
|
||||
const frontMatterRegex = /(^---$[\s\S]*?^---$)/m;
|
||||
const preGroupedRegex = /([\s\S]*?)(^---$[\s\S]*?^---$)(\s*)([\s\S]*)/m; // preFrontMatter, frontMatter, spacing, and content
|
||||
import getFrontMatterLanguageDefinition from './parse_source_file_language_support';
|
||||
|
||||
const parseSourceFile = (raw, options = { frontMatterLanguage: 'yaml' }) => {
|
||||
const { open, close } = getFrontMatterLanguageDefinition(options.frontMatterLanguage);
|
||||
const anyChar = '[\\s\\S]';
|
||||
const frontMatterBlock = `^${open}$${anyChar}*?^${close}$`;
|
||||
const frontMatterRegex = new RegExp(`${frontMatterBlock}`, 'm');
|
||||
const preGroupedRegex = new RegExp(`(${anyChar}*?)(${frontMatterBlock})(\\s*)(${anyChar}*)`, 'm'); // preFrontMatter, frontMatter, spacing, and content
|
||||
let initial;
|
||||
let editable;
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
const frontMatterLanguageDefinitions = [
|
||||
{ name: 'yaml', open: '---', close: '---' },
|
||||
{ name: 'toml', open: '\\+\\+\\+', close: '\\+\\+\\+' },
|
||||
{ name: 'json', open: '{', close: '}' },
|
||||
];
|
||||
|
||||
const getFrontMatterLanguageDefinition = name => {
|
||||
const languageDefinition = frontMatterLanguageDefinitions.find(def => def.name === name);
|
||||
|
||||
if (!languageDefinition) {
|
||||
throw new Error(`Unsupported front matter language: ${name}`);
|
||||
}
|
||||
|
||||
return languageDefinition;
|
||||
};
|
||||
|
||||
export default getFrontMatterLanguageDefinition;
|
|
@ -18,6 +18,11 @@ export default {
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
fileClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isTree() {
|
||||
|
@ -123,6 +128,7 @@ export default {
|
|||
:style="levelIndentation"
|
||||
class="file-row-name str-truncated"
|
||||
data-qa-selector="file_name_content"
|
||||
:class="fileClasses"
|
||||
>
|
||||
<file-icon
|
||||
class="file-row-icon"
|
||||
|
|
|
@ -180,3 +180,33 @@ table {
|
|||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vulnerability-list {
|
||||
@media (min-width: $breakpoint-sm) {
|
||||
.checkbox {
|
||||
padding-left: $gl-spacing-scale-4;
|
||||
padding-right: 0;
|
||||
|
||||
+ td,
|
||||
+ th {
|
||||
padding-left: $gl-spacing-scale-4;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.severity {
|
||||
width: 9%;
|
||||
}
|
||||
|
||||
.identifier {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.scanner {
|
||||
width: 15%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1062,7 +1062,7 @@ table.code {
|
|||
.diff-tree-list {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px;
|
||||
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
|
||||
top: $top-pos;
|
||||
max-height: calc(100vh - #{$top-pos});
|
||||
z-index: 202;
|
||||
|
|
|
@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
|
||||
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
|
||||
push_frontend_feature_flag(:unified_diff_lines, @project)
|
||||
push_frontend_feature_flag(:highlight_current_diff_row, @project)
|
||||
end
|
||||
|
||||
before_action do
|
||||
|
|
|
@ -10,6 +10,8 @@ class SessionsController < Devise::SessionsController
|
|||
include KnownSignIn
|
||||
|
||||
skip_before_action :check_two_factor_requirement, only: [:destroy]
|
||||
skip_before_action :check_password_expiration, only: [:destroy]
|
||||
|
||||
# replaced with :require_no_authentication_without_flash
|
||||
skip_before_action :require_no_authentication, only: [:new, :create]
|
||||
|
||||
|
|
|
@ -445,7 +445,7 @@ class Issue < ApplicationRecord
|
|||
super
|
||||
rescue ActiveRecord::QueryCanceled => e
|
||||
# Symptom of running out of space - schedule rebalancing
|
||||
IssueRebalancingWorker.perform_async(id)
|
||||
IssueRebalancingWorker.perform_async(nil, project_id)
|
||||
raise e
|
||||
end
|
||||
|
||||
|
@ -453,7 +453,7 @@ class Issue < ApplicationRecord
|
|||
super
|
||||
rescue ActiveRecord::QueryCanceled => e
|
||||
# Symptom of running out of space - schedule rebalancing
|
||||
IssueRebalancingWorker.perform_async(id)
|
||||
IssueRebalancingWorker.perform_async(nil, project_id)
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ module Issues
|
|||
gates = [issue.project, issue.project.group].compact
|
||||
return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) }
|
||||
|
||||
IssueRebalancingWorker.perform_async(issue.id)
|
||||
IssueRebalancingWorker.perform_async(nil, issue.project_id)
|
||||
end
|
||||
|
||||
def create_assignee_note(issue, old_assignees)
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
.container.devops-empty
|
||||
.col-sm-12.justify-content-center.text-center
|
||||
= custom_icon('dev_ops_score_no_index')
|
||||
%h4= _('Usage ping is not enabled')
|
||||
- if !current_user.admin?
|
||||
%p
|
||||
- usage_ping_path = help_page_path('development/telemetry/usage_ping')
|
||||
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
|
||||
= s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
|
||||
- if current_user.admin?
|
||||
%p
|
||||
= _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
|
||||
- if current_user.admin?
|
||||
= link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary'
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
.gl-mt-3
|
||||
- if !usage_ping_enabled
|
||||
= render 'disabled'
|
||||
#js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/telemetry/usage_ping') } }
|
||||
- elsif @metric.blank?
|
||||
= render 'no_data'
|
||||
- else
|
||||
|
|
|
@ -7,11 +7,14 @@ class IssueRebalancingWorker
|
|||
urgency :low
|
||||
feature_category :issue_tracking
|
||||
|
||||
def perform(issue_id)
|
||||
issue = Issue.find(issue_id)
|
||||
def perform(ignore = nil, project_id = nil)
|
||||
return if project_id.nil?
|
||||
|
||||
project = Project.find(project_id)
|
||||
issue = project.issues.first # All issues are equivalent as far as we are concerned
|
||||
|
||||
IssueRebalancingService.new(issue).execute
|
||||
rescue ActiveRecord::RecordNotFound, IssueRebalancingService::TooManyIssues => e
|
||||
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id)
|
||||
Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add index on merge_request_id to approval_merge_request_rules
|
||||
merge_request: 40556
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix file file input top position cutoff
|
||||
merge_request: 40634
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add toml and json front matter language support to Static Site Editor's WYSIWYG mode
|
||||
merge_request: 40718
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate DevOps Score empty state into Vue component
|
||||
merge_request: 40595
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow users with expired passwords to sign out
|
||||
merge_request: 40830
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Highlight un-focused/un-viewed file's in file tree
|
||||
merge_request: 27937
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix auto-deploy-image external chart dependencies
|
||||
merge_request: 40730
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix client usage of max line rendering
|
||||
merge_request: 40741
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Prevent form submission in search boxes on New Release and Edit Release pages
|
||||
merge_request: 40011
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnMergeRequestIdAndRuleTypeToApprovalMergeRequestRule < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = "approval_mr_rule_index_merge_request_id"
|
||||
|
||||
def up
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
:merge_request_id,
|
||||
name: INDEX_NAME
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :approval_merge_request_rules, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
360c42f4d34c3b03e7a0375a0ff2776f066888f0a40131180bf301b876ea58db
|
|
@ -18945,6 +18945,8 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public.
|
|||
|
||||
CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3);
|
||||
|
||||
CREATE INDEX approval_mr_rule_index_merge_request_id ON public.approval_merge_request_rules USING btree (merge_request_id);
|
||||
|
||||
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
|
||||
|
||||
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
|
||||
|
|
|
@ -106,7 +106,7 @@ end
|
|||
Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
|
||||
```
|
||||
|
||||
### Alternative: `expect_next_instance_of` or `allow_next_instance_of`
|
||||
### Alternative: `expect_next_instance_of`, `allow_next_instance_of`, `expect_next_found_instance_of` or `allow_next_found_instance_of`
|
||||
|
||||
Instead of writing:
|
||||
|
||||
|
@ -130,8 +130,21 @@ end
|
|||
allow_next_instance_of(Project) do |project|
|
||||
allow(project).to receive(:add_import_job)
|
||||
end
|
||||
|
||||
# Do this:
|
||||
expect_next_found_instance_of(Project) do |project|
|
||||
expect(project).to receive(:add_import_job)
|
||||
end
|
||||
|
||||
# Do this:
|
||||
allow_next_found_instance_of(Project) do |project|
|
||||
allow(project).to receive(:add_import_job)
|
||||
end
|
||||
```
|
||||
|
||||
_**Note:** Since Active Record is not calling the `.new` method on model classes to instantiate the objects,
|
||||
you should use `expect_next_found_instance_of` or `allow_next_found_instance_of` mock helpers to setup mock on objects returned by Active Record query & finder methods._
|
||||
|
||||
If we also want to initialize the instance with some particular arguments, we
|
||||
could also pass it like:
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.dast-auto-deploy:
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0"
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
|
||||
|
||||
dast_environment_deploy:
|
||||
extends: .dast-auto-deploy
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.auto-deploy:
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0"
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
|
||||
dependencies: []
|
||||
|
||||
include:
|
||||
|
|
|
@ -7021,6 +7021,9 @@ msgstr ""
|
|||
msgid "Could not create group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not create issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not create project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9281,9 +9284,6 @@ msgstr ""
|
|||
msgid "Enable usage ping"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable usage ping to get an overview of how you are using GitLab from a feature perspective."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -13034,9 +13034,6 @@ msgstr ""
|
|||
msgid "In %{time_to_now}"
|
||||
msgstr ""
|
||||
|
||||
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21873,6 +21870,9 @@ msgstr ""
|
|||
msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Ensure that %{trackingStart}issue tracking%{trackingEnd} is enabled for this project and you have %{permissionsStart}permission to create new issues%{permissionsEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21924,6 +21924,9 @@ msgstr ""
|
|||
msgid "SecurityReports|Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Project was not found or you do not have permission to add this project to Security Dashboards."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Projects added"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21999,7 +22002,7 @@ msgstr ""
|
|||
msgid "SecurityReports|To widen your search, change or remove filters above"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Unable to add %{invalidProjectsMessage}"
|
||||
msgid "SecurityReports|Unable to add %{invalidProjectsMessage}: %{errorMessage}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Unable to add %{invalidProjects}"
|
||||
|
@ -25974,6 +25977,9 @@ msgstr ""
|
|||
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
|
||||
msgstr ""
|
||||
|
||||
msgid "To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown."
|
||||
msgstr ""
|
||||
|
||||
|
@ -26283,6 +26289,9 @@ msgstr ""
|
|||
msgid "Turn on usage ping"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn on usage ping to review instance-level analytics."
|
||||
msgstr ""
|
||||
|
||||
msgid "Twitter"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26760,7 +26769,7 @@ msgstr ""
|
|||
msgid "Usage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Usage ping is not enabled"
|
||||
msgid "Usage ping is off"
|
||||
msgstr ""
|
||||
|
||||
msgid "Usage statistics"
|
||||
|
@ -27547,9 +27556,6 @@ msgstr ""
|
|||
msgid "VulnerabilityManagement|Something went wrong while trying to unlink the issue. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Something went wrong, could not get user."
|
||||
msgstr ""
|
||||
|
||||
|
@ -29534,9 +29540,15 @@ msgstr ""
|
|||
msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} to create one."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|A new merge train has started and this merge request is the first of the queue."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Added to the merge train by"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Allows commits from members who can merge to the target branch"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29624,9 +29636,6 @@ msgstr ""
|
|||
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|In the merge train at position %{mergeTrainPosition}"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Jump to first unresolved thread"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ RSpec.describe SessionsController do
|
|||
include DeviseHelpers
|
||||
include LdapHelpers
|
||||
|
||||
describe '#new' do
|
||||
before do
|
||||
set_devise_mapping(context: @request)
|
||||
end
|
||||
|
||||
describe '#new' do
|
||||
context 'when auto sign-in is enabled' do
|
||||
before do
|
||||
stub_omniauth_setting(auto_sign_in_with_provider: :saml)
|
||||
|
@ -59,13 +59,19 @@ RSpec.describe SessionsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "redirects correctly for referer on same host with params" do
|
||||
host = "test.host"
|
||||
search_path = "/search?search=seed_project"
|
||||
request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}"
|
||||
|
||||
get(:new, params: { redirect_to_referer: :yes })
|
||||
|
||||
expect(controller.stored_location_for(:redirect)).to eq(search_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create' do
|
||||
before do
|
||||
set_devise_mapping(context: @request)
|
||||
end
|
||||
|
||||
it_behaves_like 'known sign in' do
|
||||
let(:user) { create(:user) }
|
||||
let(:post_action) { post(:create, params: { user: { login: user.username, password: user.password } }) }
|
||||
|
@ -439,25 +445,8 @@ RSpec.describe SessionsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#new" do
|
||||
before do
|
||||
set_devise_mapping(context: @request)
|
||||
end
|
||||
|
||||
it "redirects correctly for referer on same host with params" do
|
||||
host = "test.host"
|
||||
search_path = "/search?search=seed_project"
|
||||
request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}"
|
||||
|
||||
get(:new, params: { redirect_to_referer: :yes })
|
||||
|
||||
expect(controller.stored_location_for(:redirect)).to eq(search_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when login fails' do
|
||||
before do
|
||||
set_devise_mapping(context: @request)
|
||||
@request.env["warden.options"] = { action: 'unauthenticated' }
|
||||
end
|
||||
|
||||
|
@ -471,10 +460,6 @@ RSpec.describe SessionsController do
|
|||
describe '#set_current_context' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
set_devise_mapping(context: @request)
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
|
@ -528,4 +513,21 @@ RSpec.describe SessionsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#destroy' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'for a user whose password has expired' do
|
||||
let(:user) { create(:user, password_expires_at: 2.days.ago) }
|
||||
|
||||
it 'allows to sign out successfully' do
|
||||
delete :destroy
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,10 +22,10 @@ RSpec.describe 'DevOps Report page' do
|
|||
stub_application_setting(usage_ping_enabled: false)
|
||||
end
|
||||
|
||||
it 'shows empty state' do
|
||||
it 'shows empty state', :js do
|
||||
visit admin_dev_ops_score_path
|
||||
|
||||
expect(page).to have_content('Usage ping is not enabled')
|
||||
expect(page).to have_selector(".js-empty-state")
|
||||
end
|
||||
|
||||
it 'hides the intro callout' do
|
||||
|
|
|
@ -8,4 +8,15 @@
|
|||
// [2]: https://gitlab.com/gitlab-org/gitlab/-/issues/213378
|
||||
// Further reference: https://github.com/facebook/jest/issues/3465
|
||||
|
||||
export default fn => fn;
|
||||
export default fn => {
|
||||
const debouncedFn = jest.fn().mockImplementation(fn);
|
||||
debouncedFn.cancel = jest.fn();
|
||||
debouncedFn.flush = jest.fn().mockImplementation(() => {
|
||||
const errorMessage =
|
||||
"The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'.";
|
||||
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
|
||||
return debouncedFn;
|
||||
};
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('DiffFileHeader component', () => {
|
|||
toggleFileDiscussions: jest.fn(),
|
||||
toggleFileDiscussionWrappers: jest.fn(),
|
||||
toggleFullDiff: jest.fn(),
|
||||
toggleActiveFileByHash: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,9 +7,12 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
|
|||
describe('Diff File Row component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
const createComponent = (props = {}, highlightCurrentDiffRow = false) => {
|
||||
wrapper = shallowMount(DiffFileRow, {
|
||||
propsData: { ...props },
|
||||
provide: {
|
||||
glFeatures: { highlightCurrentDiffRow },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -56,6 +59,31 @@ describe('Diff File Row component', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
features | fileType | isViewed | expected
|
||||
${{ highlightCurrentDiffRow: true }} | ${'blob'} | ${false} | ${'gl-font-weight-bold'}
|
||||
${{}} | ${'blob'} | ${true} | ${''}
|
||||
${{}} | ${'tree'} | ${false} | ${''}
|
||||
${{}} | ${'tree'} | ${true} | ${''}
|
||||
`(
|
||||
'with (features="$features", fileType="$fileType", isViewed=$isViewed), sets fileClasses="$expected"',
|
||||
({ features, fileType, isViewed, expected }) => {
|
||||
createComponent(
|
||||
{
|
||||
file: {
|
||||
type: fileType,
|
||||
fileHash: '#123456789',
|
||||
},
|
||||
level: 0,
|
||||
hideFileStats: false,
|
||||
viewedFiles: isViewed ? { '#123456789': true } : {},
|
||||
},
|
||||
features.highlightCurrentDiffRow,
|
||||
);
|
||||
expect(wrapper.find(FileRow).props('fileClasses')).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
describe('FileRowStats components', () => {
|
||||
it.each`
|
||||
type | hideFileStats | value | desc
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
import Vuex from 'vuex';
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
|
||||
import TreeList from '~/diffs/components/tree_list.vue';
|
||||
import createStore from '~/diffs/store/modules';
|
||||
import FileTree from '~/vue_shared/components/file_tree.vue';
|
||||
|
||||
describe('Diffs tree list component', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
const getFileRows = () => wrapper.findAll('.file-row');
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = state => {
|
||||
const store = new Vuex.Store({
|
||||
const createComponent = (mountFn = mount) => {
|
||||
wrapper = mountFn(TreeList, {
|
||||
store,
|
||||
localVue,
|
||||
propsData: { hideFileStats: false },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
diffs: createStore(),
|
||||
},
|
||||
|
@ -23,32 +33,10 @@ describe('Diffs tree list component', () => {
|
|||
addedLines: 10,
|
||||
removedLines: 20,
|
||||
...store.state.diffs,
|
||||
...state,
|
||||
};
|
||||
|
||||
wrapper = mount(TreeList, {
|
||||
store,
|
||||
localVue,
|
||||
propsData: { hideFileStats: false },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.removeItem('mr_diff_tree_list');
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders empty text', () => {
|
||||
expect(wrapper.text()).toContain('No files found');
|
||||
});
|
||||
|
||||
describe('with files', () => {
|
||||
beforeEach(() => {
|
||||
const setupFilesInState = () => {
|
||||
const treeEntries = {
|
||||
'index.js': {
|
||||
addedLines: 0,
|
||||
|
@ -72,12 +60,30 @@ describe('Diffs tree list component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
createComponent({
|
||||
Object.assign(store.state.diffs, {
|
||||
treeEntries,
|
||||
tree: [treeEntries['index.js'], treeEntries.app],
|
||||
});
|
||||
};
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders empty text', () => {
|
||||
expect(wrapper.text()).toContain('No files found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with files', () => {
|
||||
beforeEach(() => {
|
||||
setupFilesInState();
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders tree', () => {
|
||||
|
@ -136,4 +142,23 @@ describe('Diffs tree list component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with viewedDiffFileIds', () => {
|
||||
const viewedDiffFileIds = { fileId: '#12345' };
|
||||
|
||||
beforeEach(() => {
|
||||
setupFilesInState();
|
||||
store.state.diffs.viewedDiffFileIds = viewedDiffFileIds;
|
||||
});
|
||||
|
||||
it('passes the viewedDiffFileIds to the FileTree', () => {
|
||||
createComponent(shallowMount);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
// Have to use $attrs['viewed-files'] because we are passing down an object
|
||||
// and attributes('') stringifies values (e.g. [object])...
|
||||
expect(wrapper.find(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -191,10 +191,10 @@ describe('DiffsStoreActions', () => {
|
|||
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
|
||||
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
|
||||
{ type: types.SET_BATCH_LOADING, payload: false },
|
||||
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test' },
|
||||
{ type: types.VIEW_DIFF_FILE, payload: 'test' },
|
||||
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
|
||||
{ type: types.SET_BATCH_LOADING, payload: false },
|
||||
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test2' },
|
||||
{ type: types.VIEW_DIFF_FILE, payload: 'test2' },
|
||||
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
|
||||
],
|
||||
[],
|
||||
|
@ -300,7 +300,7 @@ describe('DiffsStoreActions', () => {
|
|||
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
|
||||
testAction(setHighlightedRow, 'ABC_123', {}, [
|
||||
{ type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
|
||||
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'ABC' },
|
||||
{ type: types.VIEW_DIFF_FILE, payload: 'ABC' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -904,7 +904,7 @@ describe('DiffsStoreActions', () => {
|
|||
expect(document.location.hash).toBe('#test');
|
||||
});
|
||||
|
||||
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
|
||||
it('commits VIEW_DIFF_FILE', () => {
|
||||
const state = {
|
||||
treeEntries: {
|
||||
path: {
|
||||
|
@ -915,7 +915,7 @@ describe('DiffsStoreActions', () => {
|
|||
|
||||
scrollToFile({ state, commit }, 'path');
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, 'test');
|
||||
expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, 'test');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1413,7 +1413,7 @@ describe('DiffsStoreActions', () => {
|
|||
});
|
||||
|
||||
describe('setCurrentDiffFileIdFromNote', () => {
|
||||
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
|
||||
it('commits VIEW_DIFF_FILE', () => {
|
||||
const commit = jest.fn();
|
||||
const state = { diffFiles: [{ file_hash: '123' }] };
|
||||
const rootGetters = {
|
||||
|
@ -1423,10 +1423,10 @@ describe('DiffsStoreActions', () => {
|
|||
|
||||
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, '123');
|
||||
expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, '123');
|
||||
});
|
||||
|
||||
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when discussion has no diff_file', () => {
|
||||
it('does not commit VIEW_DIFF_FILE when discussion has no diff_file', () => {
|
||||
const commit = jest.fn();
|
||||
const state = { diffFiles: [{ file_hash: '123' }] };
|
||||
const rootGetters = {
|
||||
|
@ -1439,7 +1439,7 @@ describe('DiffsStoreActions', () => {
|
|||
expect(commit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when diff file does not exist', () => {
|
||||
it('does not commit VIEW_DIFF_FILE when diff file does not exist', () => {
|
||||
const commit = jest.fn();
|
||||
const state = { diffFiles: [{ file_hash: '123' }] };
|
||||
const rootGetters = {
|
||||
|
@ -1454,12 +1454,12 @@ describe('DiffsStoreActions', () => {
|
|||
});
|
||||
|
||||
describe('navigateToDiffFileIndex', () => {
|
||||
it('commits UPDATE_CURRENT_DIFF_FILE_ID', done => {
|
||||
it('commits VIEW_DIFF_FILE', done => {
|
||||
testAction(
|
||||
navigateToDiffFileIndex,
|
||||
0,
|
||||
{ diffFiles: [{ file_hash: '123' }] },
|
||||
[{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: '123' }],
|
||||
[{ type: types.VIEW_DIFF_FILE, payload: '123' }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
|
|
|
@ -737,11 +737,11 @@ describe('DiffsStoreMutations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('UPDATE_CURRENT_DIFF_FILE_ID', () => {
|
||||
describe('VIEW_DIFF_FILE', () => {
|
||||
it('updates currentDiffFileId', () => {
|
||||
const state = createState();
|
||||
|
||||
mutations[types.UPDATE_CURRENT_DIFF_FILE_ID](state, 'somefileid');
|
||||
mutations[types.VIEW_DIFF_FILE](state, 'somefileid');
|
||||
|
||||
expect(state.currentDiffFileId).toBe('somefileid');
|
||||
});
|
||||
|
|
|
@ -22,11 +22,11 @@ describe('IDE pipelines list', () => {
|
|||
const defaultState = {
|
||||
links: { ciHelpPagePath: TEST_HOST },
|
||||
pipelinesEmptyStateSvgPath: TEST_HOST,
|
||||
pipelines: {
|
||||
};
|
||||
const defaultPipelinesState = {
|
||||
stages: [],
|
||||
failedStages: [],
|
||||
isLoadingJobs: false,
|
||||
},
|
||||
};
|
||||
|
||||
const fetchLatestPipelineMock = jest.fn();
|
||||
|
@ -34,23 +34,20 @@ describe('IDE pipelines list', () => {
|
|||
const failedStagesGetterMock = jest.fn().mockReturnValue([]);
|
||||
const fakeProjectPath = 'alpha/beta';
|
||||
|
||||
const createComponent = (state = {}) => {
|
||||
const { pipelines: pipelinesState, ...restOfState } = state;
|
||||
const { defaultPipelines, ...defaultRestOfState } = defaultState;
|
||||
|
||||
const fakeStore = new Vuex.Store({
|
||||
const createStore = (rootState, pipelinesState) => {
|
||||
return new Vuex.Store({
|
||||
getters: {
|
||||
currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }),
|
||||
},
|
||||
state: {
|
||||
...defaultRestOfState,
|
||||
...restOfState,
|
||||
...defaultState,
|
||||
...rootState,
|
||||
},
|
||||
modules: {
|
||||
pipelines: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
...defaultPipelines,
|
||||
...defaultPipelinesState,
|
||||
...pipelinesState,
|
||||
},
|
||||
actions: {
|
||||
|
@ -69,10 +66,12 @@ describe('IDE pipelines list', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = (state = {}, pipelinesState = {}) => {
|
||||
wrapper = shallowMount(List, {
|
||||
localVue,
|
||||
store: fakeStore,
|
||||
store: createStore(state, pipelinesState),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -94,31 +93,33 @@ describe('IDE pipelines list', () => {
|
|||
|
||||
describe('when loading', () => {
|
||||
let defaultPipelinesLoadingState;
|
||||
|
||||
beforeAll(() => {
|
||||
defaultPipelinesLoadingState = {
|
||||
...defaultState.pipelines,
|
||||
isLoadingPipeline: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('does not render when pipeline has loaded before', () => {
|
||||
createComponent({
|
||||
pipelines: {
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
...defaultPipelinesLoadingState,
|
||||
hasLoadedPipeline: true,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders loading state', () => {
|
||||
createComponent({
|
||||
pipelines: {
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
...defaultPipelinesLoadingState,
|
||||
hasLoadedPipeline: false,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
|
@ -126,21 +127,22 @@ describe('IDE pipelines list', () => {
|
|||
|
||||
describe('when loaded', () => {
|
||||
let defaultPipelinesLoadedState;
|
||||
|
||||
beforeAll(() => {
|
||||
defaultPipelinesLoadedState = {
|
||||
...defaultState.pipelines,
|
||||
isLoadingPipeline: false,
|
||||
hasLoadedPipeline: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('renders empty state when no latestPipeline', () => {
|
||||
createComponent({ pipelines: { ...defaultPipelinesLoadedState, latestPipeline: null } });
|
||||
createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null });
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('with latest pipeline loaded', () => {
|
||||
let withLatestPipelineState;
|
||||
|
||||
beforeAll(() => {
|
||||
withLatestPipelineState = {
|
||||
...defaultPipelinesLoadedState,
|
||||
|
@ -149,12 +151,12 @@ describe('IDE pipelines list', () => {
|
|||
});
|
||||
|
||||
it('renders ci icon', () => {
|
||||
createComponent({ pipelines: withLatestPipelineState });
|
||||
createComponent({}, withLatestPipelineState);
|
||||
expect(wrapper.find(CiIcon).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders pipeline data', () => {
|
||||
createComponent({ pipelines: withLatestPipelineState });
|
||||
createComponent({}, withLatestPipelineState);
|
||||
|
||||
expect(wrapper.text()).toContain('#1');
|
||||
});
|
||||
|
@ -162,7 +164,7 @@ describe('IDE pipelines list', () => {
|
|||
it('renders list of jobs', () => {
|
||||
const stages = [];
|
||||
const isLoadingJobs = true;
|
||||
createComponent({ pipelines: { ...withLatestPipelineState, stages, isLoadingJobs } });
|
||||
createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs });
|
||||
|
||||
const jobProps = wrapper
|
||||
.findAll(Tab)
|
||||
|
@ -177,7 +179,7 @@ describe('IDE pipelines list', () => {
|
|||
const failedStages = [];
|
||||
failedStagesGetterMock.mockReset().mockReturnValue(failedStages);
|
||||
const isLoadingJobs = true;
|
||||
createComponent({ pipelines: { ...withLatestPipelineState, isLoadingJobs } });
|
||||
createComponent({}, { ...withLatestPipelineState, isLoadingJobs });
|
||||
|
||||
const jobProps = wrapper
|
||||
.findAll(Tab)
|
||||
|
@ -191,12 +193,13 @@ describe('IDE pipelines list', () => {
|
|||
describe('with YAML error', () => {
|
||||
it('renders YAML error', () => {
|
||||
const yamlError = 'test yaml error';
|
||||
createComponent({
|
||||
pipelines: {
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
...defaultPipelinesLoadedState,
|
||||
latestPipeline: { ...pipelines[0], yamlError },
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
|
||||
expect(wrapper.text()).toContain(yamlError);
|
||||
|
|
|
@ -2,10 +2,12 @@ import axios from 'axios';
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlNewDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { ENTER_KEY } from '~/lib/utils/keys';
|
||||
import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
|
||||
import { milestones as projectMilestones } from './mock_data';
|
||||
|
||||
const TEST_SEARCH_ENDPOINT = '/api/v4/projects/8/search';
|
||||
const TEST_SEARCH = 'TEST_SEARCH';
|
||||
|
||||
const extraLinks = [
|
||||
{ text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
|
||||
|
@ -21,6 +23,8 @@ describe('Milestone selector', () => {
|
|||
|
||||
const findNoResultsMessage = () => wrapper.find({ ref: 'noResults' });
|
||||
|
||||
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
|
||||
|
||||
const factory = (options = {}) => {
|
||||
wrapper = shallowMount(MilestoneCombobox, {
|
||||
...options,
|
||||
|
@ -63,7 +67,7 @@ describe('Milestone selector', () => {
|
|||
describe('before results', () => {
|
||||
it('should show a loading icon', () => {
|
||||
const request = mock.onGet(TEST_SEARCH_ENDPOINT, {
|
||||
params: { search: 'TEST_SEARCH', scope: 'milestones' },
|
||||
params: { search: TEST_SEARCH, scope: 'milestones' },
|
||||
});
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
|
@ -85,9 +89,9 @@ describe('Milestone selector', () => {
|
|||
describe('with empty results', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
|
||||
.onGet(TEST_SEARCH_ENDPOINT, { params: { search: TEST_SEARCH, scope: 'milestones' } })
|
||||
.reply(200, []);
|
||||
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'TEST_SEARCH');
|
||||
findSearchBox().vm.$emit('input', TEST_SEARCH);
|
||||
return axios.waitForAll();
|
||||
});
|
||||
|
||||
|
@ -116,7 +120,7 @@ describe('Milestone selector', () => {
|
|||
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
|
||||
},
|
||||
]);
|
||||
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'v0.1');
|
||||
findSearchBox().vm.$emit('input', 'v0.1');
|
||||
return axios.waitForAll().then(() => {
|
||||
items = wrapper.findAll('[role="milestone option"]');
|
||||
});
|
||||
|
@ -147,4 +151,36 @@ describe('Milestone selector', () => {
|
|||
expect(findNoResultsMessage().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when Enter is pressed', () => {
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
propsData: {
|
||||
projectId,
|
||||
preselectedMilestones,
|
||||
extraLinks,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: 'TEST_SEARCH',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
mock
|
||||
.onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
|
||||
.reply(200, []);
|
||||
});
|
||||
|
||||
it('should trigger a search', async () => {
|
||||
mock.resetHistory();
|
||||
|
||||
findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
|
||||
|
||||
await axios.waitForAll();
|
||||
|
||||
expect(mock.history.get.length).toBe(1);
|
||||
expect(mock.history.get[0].url).toBe(TEST_SEARCH_ENDPOINT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import { GlLoadingIcon, GlSearchBoxByType, GlNewDropdownItem, GlIcon } from '@gitlab/ui';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import { sprintf } from '~/locale';
|
||||
import { ENTER_KEY } from '~/lib/utils/keys';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
|
||||
import createStore from '~/ref/stores/';
|
||||
|
@ -83,6 +84,8 @@ describe('Ref selector component', () => {
|
|||
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
|
||||
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
|
||||
|
||||
const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
|
||||
const findBranchDropdownItems = () => findBranchesSection().findAll(GlNewDropdownItem);
|
||||
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
|
||||
|
@ -120,7 +123,7 @@ describe('Ref selector component', () => {
|
|||
// Convenience methods
|
||||
//
|
||||
const updateQuery = newQuery => {
|
||||
wrapper.find(GlSearchBoxByType).vm.$emit('input', newQuery);
|
||||
findSearchBox().vm.$emit('input', newQuery);
|
||||
};
|
||||
|
||||
const selectFirstBranch = () => {
|
||||
|
@ -244,6 +247,23 @@ describe('Ref selector component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the Enter is pressed', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
||||
return waitForRequests({ andClearMocks: true });
|
||||
});
|
||||
|
||||
it('requeries the endpoints when Enter is pressed', () => {
|
||||
findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
|
||||
|
||||
return waitForRequests().then(() => {
|
||||
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
|
||||
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no results are found', () => {
|
||||
beforeEach(() => {
|
||||
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
|
||||
|
|
|
@ -10,7 +10,7 @@ import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved
|
|||
|
||||
import {
|
||||
sourceContentTitle as title,
|
||||
sourceContent as content,
|
||||
sourceContentYAML as content,
|
||||
sourceContentBody as body,
|
||||
returnUrl,
|
||||
} from '../mock_data';
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
projectId,
|
||||
sourcePath,
|
||||
sourceContentTitle as title,
|
||||
sourceContent as content,
|
||||
sourceContentYAML as content,
|
||||
} from '../../mock_data';
|
||||
|
||||
jest.mock('~/static_site_editor/services/load_source_content', () => jest.fn());
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
projectId as project,
|
||||
sourcePath,
|
||||
username,
|
||||
sourceContent as content,
|
||||
sourceContentYAML as content,
|
||||
savedContentMeta,
|
||||
} from '../../mock_data';
|
||||
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
export const sourceContentHeader = `---
|
||||
export const sourceContentHeaderYAML = `---
|
||||
layout: handbook-page-toc
|
||||
title: Handbook
|
||||
twitter_image: '/images/tweets/handbook-gitlab.png'
|
||||
---`;
|
||||
export const sourceContentHeaderTOML = `+++
|
||||
layout: "handbook-page-toc"
|
||||
title: "Handbook"
|
||||
twitter_image: "/images/tweets/handbook-gitlab.png"
|
||||
+++`;
|
||||
export const sourceContentHeaderJSON = `{
|
||||
"layout": "handbook-page-toc",
|
||||
"title": "Handbook",
|
||||
"twitter_image": "/images/tweets/handbook-gitlab.png",
|
||||
}`;
|
||||
export const sourceContentSpacing = `
|
||||
`;
|
||||
export const sourceContentBody = `## On this page
|
||||
|
@ -13,7 +23,9 @@ export const sourceContentBody = `## On this page
|
|||
|
||||
![image](path/to/image1.png)
|
||||
`;
|
||||
export const sourceContent = `${sourceContentHeader}${sourceContentSpacing}${sourceContentBody}`;
|
||||
export const sourceContentYAML = `${sourceContentHeaderYAML}${sourceContentSpacing}${sourceContentBody}`;
|
||||
export const sourceContentTOML = `${sourceContentHeaderTOML}${sourceContentSpacing}${sourceContentBody}`;
|
||||
export const sourceContentJSON = `${sourceContentHeaderJSON}${sourceContentSpacing}${sourceContentBody}`;
|
||||
export const sourceContentTitle = 'Handbook';
|
||||
|
||||
export const username = 'gitlabuser';
|
||||
|
|
|
@ -13,7 +13,7 @@ import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constant
|
|||
import {
|
||||
projectId as project,
|
||||
returnUrl,
|
||||
sourceContent as content,
|
||||
sourceContentYAML as content,
|
||||
sourceContentTitle as title,
|
||||
sourcePath,
|
||||
username,
|
||||
|
|
|
@ -2,7 +2,12 @@ import Api from '~/api';
|
|||
|
||||
import loadSourceContent from '~/static_site_editor/services/load_source_content';
|
||||
|
||||
import { sourceContent, sourceContentTitle, projectId, sourcePath } from '../mock_data';
|
||||
import {
|
||||
sourceContentYAML as sourceContent,
|
||||
sourceContentTitle,
|
||||
projectId,
|
||||
sourcePath,
|
||||
} from '../mock_data';
|
||||
|
||||
describe('loadSourceContent', () => {
|
||||
describe('requesting source content succeeds', () => {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import getFrontMatterLanguageDefinition from '~/static_site_editor/services/parse_source_file_language_support';
|
||||
|
||||
describe('static_site_editor/services/parse_source_file_language_support', () => {
|
||||
describe('getFrontMatterLanguageDefinition', () => {
|
||||
it.each`
|
||||
languageName
|
||||
${'yaml'}
|
||||
${'toml'}
|
||||
${'json'}
|
||||
${'abcd'}
|
||||
`('returns $hasMatch when provided $languageName', ({ languageName }) => {
|
||||
try {
|
||||
const definition = getFrontMatterLanguageDefinition(languageName);
|
||||
expect(definition.name).toBe(languageName);
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(`Unsupported front matter language: ${languageName}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,12 +1,16 @@
|
|||
import {
|
||||
sourceContent as content,
|
||||
sourceContentHeader as frontMatter,
|
||||
sourceContentYAML as content,
|
||||
sourceContentTOML as tomlContent,
|
||||
sourceContentJSON as jsonContent,
|
||||
sourceContentHeaderYAML as yamlFrontMatter,
|
||||
sourceContentHeaderTOML as tomlFrontMatter,
|
||||
sourceContentHeaderJSON as jsonFrontMatter,
|
||||
sourceContentBody as body,
|
||||
} from '../mock_data';
|
||||
|
||||
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
|
||||
|
||||
describe('parseSourceFile', () => {
|
||||
describe('static_site_editor/services/parse_source_file', () => {
|
||||
const contentComplex = [content, content, content].join('');
|
||||
const complexBody = [body, content, content].join('');
|
||||
const edit = 'and more';
|
||||
|
@ -14,13 +18,22 @@ describe('parseSourceFile', () => {
|
|||
const newContentComplex = `${contentComplex} ${edit}`;
|
||||
|
||||
describe('unmodified front matter', () => {
|
||||
const yamlOptions = { frontMatterLanguage: 'yaml' };
|
||||
|
||||
it.each`
|
||||
parsedSource
|
||||
${parseSourceFile(content)}
|
||||
${parseSourceFile(contentComplex)}
|
||||
`('returns the correct front matter when queried', ({ parsedSource }) => {
|
||||
expect(parsedSource.frontMatter()).toBe(frontMatter);
|
||||
});
|
||||
parsedSource | targetFrontMatter
|
||||
${parseSourceFile(content)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(content, yamlOptions)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(contentComplex, yamlOptions)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(tomlContent, { frontMatterLanguage: 'toml' })} | ${tomlFrontMatter}
|
||||
${parseSourceFile(jsonContent, { frontMatterLanguage: 'json' })} | ${jsonFrontMatter}
|
||||
`(
|
||||
'returns $targetFrontMatter when frontMatter queried',
|
||||
({ parsedSource, targetFrontMatter }) => {
|
||||
expect(parsedSource.frontMatter()).toBe(targetFrontMatter);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('unmodified content', () => {
|
||||
|
@ -49,9 +62,12 @@ describe('parseSourceFile', () => {
|
|||
});
|
||||
|
||||
describe('modified front matter', () => {
|
||||
const newFrontMatter = '---\nnewKey: newVal\n---';
|
||||
const contentWithNewFrontMatter = content.replace(frontMatter, newFrontMatter);
|
||||
const contentComplexWithNewFrontMatter = contentComplex.replace(frontMatter, newFrontMatter);
|
||||
const newYamlFrontMatter = '---\nnewKey: newVal\n---';
|
||||
const contentWithNewFrontMatter = content.replace(yamlFrontMatter, newYamlFrontMatter);
|
||||
const contentComplexWithNewFrontMatter = contentComplex.replace(
|
||||
yamlFrontMatter,
|
||||
newYamlFrontMatter,
|
||||
);
|
||||
|
||||
it.each`
|
||||
parsedSource | targetContent
|
||||
|
@ -60,11 +76,11 @@ describe('parseSourceFile', () => {
|
|||
`(
|
||||
'returns the correct front matter and modified content',
|
||||
({ parsedSource, targetContent }) => {
|
||||
expect(parsedSource.frontMatter()).toBe(frontMatter);
|
||||
expect(parsedSource.frontMatter()).toBe(yamlFrontMatter);
|
||||
|
||||
parsedSource.setFrontMatter(newFrontMatter);
|
||||
parsedSource.setFrontMatter(newYamlFrontMatter);
|
||||
|
||||
expect(parsedSource.frontMatter()).toBe(newFrontMatter);
|
||||
expect(parsedSource.frontMatter()).toBe(newYamlFrontMatter);
|
||||
expect(parsedSource.content()).toBe(targetContent);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
commitMultipleResponse,
|
||||
createMergeRequestResponse,
|
||||
sourcePath,
|
||||
sourceContent as content,
|
||||
sourceContentYAML as content,
|
||||
trackingCategory,
|
||||
images,
|
||||
} from '../mock_data';
|
||||
|
|
|
@ -139,4 +139,16 @@ describe('File row component', () => {
|
|||
|
||||
expect(wrapper.vm.hasUrlAtCurrentRoute()).toBe(true);
|
||||
});
|
||||
|
||||
it('render with the correct file classes prop', () => {
|
||||
createComponent({
|
||||
file: {
|
||||
...file(),
|
||||
},
|
||||
level: 0,
|
||||
fileClasses: 'font-weight-bold',
|
||||
});
|
||||
|
||||
expect(wrapper.find('.file-row-name').classes()).toContain('font-weight-bold');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1196,7 +1196,7 @@ RSpec.describe Issue do
|
|||
it 'schedules rebalancing if we time-out when finding a gap' do
|
||||
lhs = build_stubbed(:issue, relative_position: 99, project: project)
|
||||
to_move = build(:issue, project: project)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
|
||||
|
||||
expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled)
|
||||
end
|
||||
|
@ -1205,7 +1205,7 @@ RSpec.describe Issue do
|
|||
describe '#find_next_gap_after' do
|
||||
it 'schedules rebalancing if we time-out when finding a gap' do
|
||||
allow(issue).to receive(:find_next_gap) { raise ActiveRecord::QueryCanceled }
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
|
||||
|
||||
expect { issue.move_sequence_after }.to raise_error(ActiveRecord::QueryCanceled)
|
||||
end
|
||||
|
|
|
@ -77,7 +77,7 @@ RSpec.describe Issues::CreateService do
|
|||
|
||||
it 'rebalances if needed' do
|
||||
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
|
||||
|
||||
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
|
||||
end
|
||||
|
@ -86,7 +86,7 @@ RSpec.describe Issues::CreateService do
|
|||
stub_feature_flags(rebalance_issues: false)
|
||||
|
||||
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
|
||||
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(Integer)
|
||||
expect(IssueRebalancingWorker).not_to receive(:perform_async)
|
||||
|
||||
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
|
||||
end
|
||||
|
@ -95,7 +95,7 @@ RSpec.describe Issues::CreateService do
|
|||
stub_feature_flags(rebalance_issues: project)
|
||||
|
||||
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
|
||||
|
||||
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
|
||||
end
|
||||
|
|
|
@ -126,7 +126,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
|
||||
opts[:move_between_ids] = [issue1.id, issue2.id]
|
||||
|
||||
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(issue.id)
|
||||
expect(IssueRebalancingWorker).not_to receive(:perform_async)
|
||||
|
||||
update_issue(opts)
|
||||
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
|
||||
|
@ -142,7 +142,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
|
||||
opts[:move_between_ids] = [issue1.id, issue2.id]
|
||||
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
|
||||
|
||||
update_issue(opts)
|
||||
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
|
||||
|
@ -156,7 +156,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
|
||||
opts[:move_between_ids] = [issue1.id, issue2.id]
|
||||
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
|
||||
|
||||
update_issue(opts)
|
||||
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
|
||||
|
@ -170,7 +170,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
|
||||
opts[:move_between_ids] = [issue1.id, issue2.id]
|
||||
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
|
||||
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
|
||||
|
||||
update_issue(opts)
|
||||
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
|
||||
|
|
|
@ -115,6 +115,7 @@ RSpec.configure do |config|
|
|||
config.include StubExperiments
|
||||
config.include StubGitlabCalls
|
||||
config.include StubGitlabData
|
||||
config.include NextFoundInstanceOf
|
||||
config.include NextInstanceOf
|
||||
config.include TestEnv
|
||||
config.include Devise::Test::ControllerHelpers, type: :controller
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module NextFoundInstanceOf
|
||||
ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets'
|
||||
|
||||
def expect_next_found_instance_of(klass)
|
||||
check_if_active_record!(klass)
|
||||
|
||||
stub_allocate(expect(klass)) do |expectation|
|
||||
yield(expectation)
|
||||
end
|
||||
end
|
||||
|
||||
def allow_next_found_instance_of(klass)
|
||||
check_if_active_record!(klass)
|
||||
|
||||
stub_allocate(allow(klass)) do |allowance|
|
||||
yield(allowance)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_if_active_record!(klass)
|
||||
raise ArgumentError.new(ERROR_MESSAGE) unless klass < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def stub_allocate(target)
|
||||
target.to receive(:allocate).and_wrap_original do |method|
|
||||
method.call.tap { |allocation| yield(allocation) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,23 +10,30 @@ RSpec.describe IssueRebalancingWorker do
|
|||
service = double(execute: nil)
|
||||
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
|
||||
|
||||
described_class.new.perform(issue.id)
|
||||
described_class.new.perform(nil, issue.project_id)
|
||||
end
|
||||
|
||||
it 'anticipates the inability to find the issue' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(issue_id: -1))
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(project_id: -1))
|
||||
expect(IssueRebalancingService).not_to receive(:new)
|
||||
|
||||
described_class.new.perform(-1)
|
||||
described_class.new.perform(nil, -1)
|
||||
end
|
||||
|
||||
it 'anticipates there being too many issues' do
|
||||
service = double
|
||||
allow(service).to receive(:execute) { raise IssueRebalancingService::TooManyIssues }
|
||||
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(issue_id: issue.id))
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(project_id: issue.project_id))
|
||||
|
||||
described_class.new.perform(issue.id)
|
||||
described_class.new.perform(nil, issue.project_id)
|
||||
end
|
||||
|
||||
it 'takes no action if the value is nil' do
|
||||
expect(IssueRebalancingService).not_to receive(:new)
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
|
||||
|
||||
described_class.new.perform(nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,14 +54,10 @@ RSpec.describe NewNoteWorker do
|
|||
let(:note) { create(:note) }
|
||||
|
||||
before do
|
||||
# TODO: `allow_next_instance_of` helper method is not working
|
||||
# because ActiveRecord is directly calling `.allocate` on model
|
||||
# classes and bypasses the `.new` method call.
|
||||
# Fix the `allow_next_instance_of` helper and change these to mock
|
||||
# the next instance of `Note` model class.
|
||||
allow(Note).to receive(:find_by).with(id: note.id).and_return(note)
|
||||
allow_next_found_instance_of(Note) do |note|
|
||||
allow(note).to receive(:skip_notification?).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not create a new note notification' do
|
||||
expect_any_instance_of(NotificationService).not_to receive(:new_note)
|
||||
|
|
Loading…
Reference in New Issue