Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
524e972622
commit
78cfc7cf4a
|
|
@ -14,8 +14,6 @@ import { mapActions, mapGetters, mapState } from 'vuex';
|
|||
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
|
||||
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
import eventHub from '../eventhub';
|
||||
|
|
@ -23,6 +21,8 @@ import groupBoardsQuery from '../graphql/group_boards.query.graphql';
|
|||
import projectBoardsQuery from '../graphql/project_boards.query.graphql';
|
||||
import groupBoardQuery from '../graphql/group_board.query.graphql';
|
||||
import projectBoardQuery from '../graphql/project_board.query.graphql';
|
||||
import groupRecentBoardsQuery from '../graphql/group_recent_boards.query.graphql';
|
||||
import projectRecentBoardsQuery from '../graphql/project_recent_boards.query.graphql';
|
||||
|
||||
const MIN_BOARDS_TO_VIEW_RECENT = 10;
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ export default {
|
|||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
inject: ['fullPath', 'recentBoardsEndpoint'],
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
throttleDuration: {
|
||||
type: Number,
|
||||
|
|
@ -158,6 +158,10 @@ export default {
|
|||
this.scrollFadeInitialized = false;
|
||||
this.$nextTick(this.setScrollFade);
|
||||
},
|
||||
recentBoards() {
|
||||
this.scrollFadeInitialized = false;
|
||||
this.$nextTick(this.setScrollFade);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('showBoardModal', this.showPage);
|
||||
|
|
@ -173,11 +177,11 @@ export default {
|
|||
cancel() {
|
||||
this.showPage('');
|
||||
},
|
||||
boardUpdate(data) {
|
||||
boardUpdate(data, boardType) {
|
||||
if (!data?.[this.parentType]) {
|
||||
return [];
|
||||
}
|
||||
return data[this.parentType].boards.edges.map(({ node }) => ({
|
||||
return data[this.parentType][boardType].edges.map(({ node }) => ({
|
||||
id: getIdFromGraphQLId(node.id),
|
||||
name: node.name,
|
||||
}));
|
||||
|
|
@ -185,6 +189,9 @@ export default {
|
|||
boardQuery() {
|
||||
return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
|
||||
},
|
||||
recentBoardsQuery() {
|
||||
return this.isGroupBoard ? groupRecentBoardsQuery : projectRecentBoardsQuery;
|
||||
},
|
||||
loadBoards(toggleDropdown = true) {
|
||||
if (toggleDropdown && this.boards.length > 0) {
|
||||
return;
|
||||
|
|
@ -196,39 +203,20 @@ export default {
|
|||
},
|
||||
query: this.boardQuery,
|
||||
loadingKey: 'loadingBoards',
|
||||
update: this.boardUpdate,
|
||||
update: (data) => this.boardUpdate(data, 'boards'),
|
||||
});
|
||||
|
||||
this.loadRecentBoards();
|
||||
},
|
||||
loadRecentBoards() {
|
||||
this.loadingRecentBoards = true;
|
||||
// Follow up to fetch recent boards using GraphQL
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/300985
|
||||
axios
|
||||
.get(this.recentBoardsEndpoint)
|
||||
.then((res) => {
|
||||
this.recentBoards = res.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
/**
|
||||
* If user is unauthorized we'd still want to resolve the
|
||||
* request to display all boards.
|
||||
*/
|
||||
if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
|
||||
this.recentBoards = []; // recent boards are empty
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
})
|
||||
.then(() => this.$nextTick()) // Wait for boards list in DOM
|
||||
.then(() => {
|
||||
this.setScrollFade();
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
this.loadingRecentBoards = false;
|
||||
});
|
||||
this.$apollo.addSmartQuery('recentBoards', {
|
||||
variables() {
|
||||
return { fullPath: this.fullPath };
|
||||
},
|
||||
query: this.recentBoardsQuery,
|
||||
loadingKey: 'loadingRecentBoards',
|
||||
update: (data) => this.boardUpdate(data, 'recentIssueBoards'),
|
||||
});
|
||||
},
|
||||
isScrolledUp() {
|
||||
const { content } = this.$refs;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
#import "ee_else_ce/boards/graphql/board.fragment.graphql"
|
||||
|
||||
query group_recent_boards($fullPath: ID!) {
|
||||
group(fullPath: $fullPath) {
|
||||
id
|
||||
recentIssueBoards {
|
||||
edges {
|
||||
node {
|
||||
...BoardFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#import "ee_else_ce/boards/graphql/board.fragment.graphql"
|
||||
|
||||
query project_recent_boards($fullPath: ID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
recentIssueBoards {
|
||||
edges {
|
||||
node {
|
||||
...BoardFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +144,6 @@ export default () => {
|
|||
mountMultipleBoardsSwitcher({
|
||||
fullPath: $boardApp.dataset.fullPath,
|
||||
rootPath: $boardApp.dataset.boardsEndpoint,
|
||||
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
|
||||
allowScopedLabels: $boardApp.dataset.scopedLabels,
|
||||
labelsManagePath: $boardApp.dataset.labelsManagePath,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export default (params = {}) => {
|
|||
provide: {
|
||||
fullPath: params.fullPath,
|
||||
rootPath: params.rootPath,
|
||||
recentBoardsEndpoint: params.recentBoardsEndpoint,
|
||||
allowScopedLabels: params.allowScopedLabels,
|
||||
labelsManagePath: params.labelsManagePath,
|
||||
allowLabelCreate: parseBoolean(dataset.canAdminBoard),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import DiffFile from './diff_file.vue';
|
|||
import HiddenFilesWarning from './hidden_files_warning.vue';
|
||||
import NoChanges from './no_changes.vue';
|
||||
import TreeList from './tree_list.vue';
|
||||
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
|
||||
|
||||
export default {
|
||||
name: 'DiffsApp',
|
||||
|
|
@ -62,8 +63,7 @@ export default {
|
|||
DynamicScrollerItem: () =>
|
||||
import('vendor/vue-virtual-scroller').then(({ DynamicScrollerItem }) => DynamicScrollerItem),
|
||||
PreRenderer: () => import('./pre_renderer.vue').then((PreRenderer) => PreRenderer),
|
||||
VirtualScrollerScrollSync: () =>
|
||||
import('./virtual_scroller_scroll_sync').then((VSSSync) => VSSSync),
|
||||
VirtualScrollerScrollSync,
|
||||
CompareVersions,
|
||||
DiffFile,
|
||||
NoChanges,
|
||||
|
|
@ -406,10 +406,8 @@ export default {
|
|||
this.unsubscribeFromEvents();
|
||||
this.removeEventListeners();
|
||||
|
||||
if (window.gon?.features?.diffsVirtualScrolling) {
|
||||
diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
|
||||
diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
|
||||
}
|
||||
diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
|
||||
diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['startTaskList']),
|
||||
|
|
@ -522,32 +520,27 @@ export default {
|
|||
);
|
||||
}
|
||||
|
||||
if (
|
||||
window.gon?.features?.diffsVirtualScrolling ||
|
||||
window.gon?.features?.usageDataDiffSearches
|
||||
) {
|
||||
let keydownTime;
|
||||
Mousetrap.bind(['mod+f', 'mod+g'], () => {
|
||||
keydownTime = new Date().getTime();
|
||||
});
|
||||
let keydownTime;
|
||||
Mousetrap.bind(['mod+f', 'mod+g'], () => {
|
||||
keydownTime = new Date().getTime();
|
||||
});
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
if (keydownTime) {
|
||||
const delta = new Date().getTime() - keydownTime;
|
||||
window.addEventListener('blur', () => {
|
||||
if (keydownTime) {
|
||||
const delta = new Date().getTime() - keydownTime;
|
||||
|
||||
// To make sure the user is using the find function we need to wait for blur
|
||||
// and max 1000ms to be sure it the search box is filtered
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
this.disableVirtualScroller();
|
||||
// To make sure the user is using the find function we need to wait for blur
|
||||
// and max 1000ms to be sure it the search box is filtered
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
this.disableVirtualScroller();
|
||||
|
||||
if (window.gon?.features?.usageDataDiffSearches) {
|
||||
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
|
||||
api.trackRedisCounterEvent('diff_searches');
|
||||
}
|
||||
if (window.gon?.features?.usageDataDiffSearches) {
|
||||
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
|
||||
api.trackRedisCounterEvent('diff_searches');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
removeEventListeners() {
|
||||
Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF));
|
||||
|
|
@ -589,8 +582,6 @@ export default {
|
|||
this.virtualScrollCurrentIndex = -1;
|
||||
},
|
||||
scrollVirtualScrollerToDiffNote() {
|
||||
if (!window.gon?.features?.diffsVirtualScrolling) return;
|
||||
|
||||
const id = window?.location?.hash;
|
||||
|
||||
if (id.startsWith('#note_')) {
|
||||
|
|
@ -605,11 +596,7 @@ export default {
|
|||
}
|
||||
},
|
||||
subscribeToVirtualScrollingEvents() {
|
||||
if (
|
||||
window.gon?.features?.diffsVirtualScrolling &&
|
||||
this.shouldShow &&
|
||||
!this.subscribedToVirtualScrollingEvents
|
||||
) {
|
||||
if (this.shouldShow && !this.subscribedToVirtualScrollingEvents) {
|
||||
diffsEventHub.$on('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
|
||||
diffsEventHub.$on('scrollToIndex', this.scrollVirtualScrollerToIndex);
|
||||
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export default {
|
|||
handler(val) {
|
||||
const el = this.$el.closest('.vue-recycle-scroller__item-view');
|
||||
|
||||
if (this.glFeatures.diffsVirtualScrolling && el) {
|
||||
if (el) {
|
||||
// We can't add a style with Vue because of the way the virtual
|
||||
// scroller library renders the diff files
|
||||
el.style.zIndex = val ? '1' : null;
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ export const UNFOLD_COUNT = 20;
|
|||
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 DIFF_FILE_SYMLINK_MODE = '120000';
|
||||
export const DIFF_FILE_DELETED_MODE = '0';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { getCookie, setCookie, parseBoolean, removeCookie } from '~/lib/utils/common_utils';
|
||||
import { getCookie, parseBoolean, removeCookie } from '~/lib/utils/common_utils';
|
||||
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
import eventHub from '../notes/event_hub';
|
||||
import diffsApp from './components/app.vue';
|
||||
|
||||
|
|
@ -74,11 +73,6 @@ export default function initDiffsApp(store) {
|
|||
trackClick: false,
|
||||
});
|
||||
}
|
||||
|
||||
const vScrollingParam = getParameterValues('virtual_scrolling')[0];
|
||||
if (vScrollingParam === 'false' || vScrollingParam === 'true') {
|
||||
setCookie('diffs_virtual_scrolling', vScrollingParam);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']),
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
|
|||
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
|
||||
commit(types.SET_BATCH_LOADING_STATE, 'loaded');
|
||||
|
||||
if (window.gon?.features?.diffsVirtualScrolling && !scrolledVirtualScroller) {
|
||||
if (!scrolledVirtualScroller) {
|
||||
const index = state.diffFiles.findIndex(
|
||||
(f) =>
|
||||
f.file_hash === hash || f[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === hash),
|
||||
|
|
@ -195,9 +195,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
|
|||
commit(types.SET_BATCH_LOADING_STATE, 'error');
|
||||
});
|
||||
|
||||
return getBatch().then(
|
||||
() => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash(),
|
||||
);
|
||||
return getBatch();
|
||||
};
|
||||
|
||||
export const fetchDiffFilesMeta = ({ commit, state }) => {
|
||||
|
|
@ -529,7 +527,7 @@ export const setCurrentFileHash = ({ commit }, hash) => {
|
|||
commit(types.SET_CURRENT_DIFF_FILE, hash);
|
||||
};
|
||||
|
||||
export const scrollToFile = ({ state, commit, getters }, { path, setHash = true }) => {
|
||||
export const scrollToFile = ({ state, commit, getters }, { path }) => {
|
||||
if (!state.treeEntries[path]) return;
|
||||
|
||||
const { fileHash } = state.treeEntries[path];
|
||||
|
|
@ -539,11 +537,9 @@ export const scrollToFile = ({ state, commit, getters }, { path, setHash = true
|
|||
if (getters.isVirtualScrollingEnabled) {
|
||||
eventHub.$emit('scrollToFileHash', fileHash);
|
||||
|
||||
if (setHash) {
|
||||
setTimeout(() => {
|
||||
window.history.replaceState(null, null, `#${fileHash}`);
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.history.replaceState(null, null, `#${fileHash}`);
|
||||
});
|
||||
} else {
|
||||
document.location.hash = fileHash;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { getCookie } from '~/lib/utils/common_utils';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
import { __, n__ } from '~/locale';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
PARALLEL_DIFF_VIEW_TYPE,
|
||||
INLINE_DIFF_VIEW_TYPE,
|
||||
|
|
@ -175,21 +174,11 @@ export function suggestionCommitMessage(state, _, rootState) {
|
|||
}
|
||||
|
||||
export const isVirtualScrollingEnabled = (state) => {
|
||||
const vSrollerCookie = getCookie('diffs_virtual_scrolling');
|
||||
|
||||
if (state.disableVirtualScroller) {
|
||||
if (state.disableVirtualScroller || getParameterValues('virtual_scrolling')[0] === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vSrollerCookie) {
|
||||
return vSrollerCookie === 'true';
|
||||
}
|
||||
|
||||
return (
|
||||
!state.viewDiffsFileByFile &&
|
||||
(window.gon?.features?.diffsVirtualScrolling ||
|
||||
getParameterValues('virtual_scrolling')[0] === 'true')
|
||||
);
|
||||
return !state.viewDiffsFileByFile;
|
||||
};
|
||||
|
||||
export const isBatchLoading = (state) => state.batchLoadingState === 'loading';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
NEW_LINE_TYPE,
|
||||
OLD_LINE_TYPE,
|
||||
MATCH_LINE_TYPE,
|
||||
LINES_TO_BE_RENDERED_DIRECTLY,
|
||||
INLINE_DIFF_LINES_KEY,
|
||||
CONFLICT_OUR,
|
||||
CONFLICT_THEIR,
|
||||
|
|
@ -380,16 +379,9 @@ function prepareDiffFileLines(file) {
|
|||
return file;
|
||||
}
|
||||
|
||||
function finalizeDiffFile(file, index) {
|
||||
let renderIt = Boolean(window.gon?.features?.diffsVirtualScrolling);
|
||||
|
||||
if (!window.gon?.features?.diffsVirtualScrolling) {
|
||||
renderIt =
|
||||
index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false;
|
||||
}
|
||||
|
||||
function finalizeDiffFile(file) {
|
||||
Object.assign(file, {
|
||||
renderIt,
|
||||
renderIt: true,
|
||||
isShowingFullFile: false,
|
||||
isLoadingFullFile: false,
|
||||
discussions: [],
|
||||
|
|
@ -417,15 +409,13 @@ export function prepareDiffData({ diff, priorFiles = [], meta = false }) {
|
|||
.map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles, meta }))
|
||||
.map(ensureBasicDiffFileLines)
|
||||
.map(prepareDiffFileLines)
|
||||
.map((file, index) => finalizeDiffFile(file, priorFiles.length + index));
|
||||
.map((file) => finalizeDiffFile(file));
|
||||
|
||||
return deduplicateFilesList([...priorFiles, ...cleanedFiles]);
|
||||
}
|
||||
|
||||
export function getDiffPositionByLineCode(diffFiles) {
|
||||
let lines = [];
|
||||
|
||||
lines = diffFiles.reduce((acc, diffFile) => {
|
||||
const lines = diffFiles.reduce((acc, diffFile) => {
|
||||
diffFile[INLINE_DIFF_LINES_KEY].forEach((line) => {
|
||||
acc.push({ file: diffFile, line });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,21 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
|
|||
IssuableTokenKeys.tokenKeys.splice(2, 0, reviewerToken);
|
||||
IssuableTokenKeys.tokenKeysWithAlternative.splice(2, 0, reviewerToken);
|
||||
|
||||
if (window.gon?.features?.mrAttentionRequests) {
|
||||
const attentionRequestedToken = {
|
||||
formattedKey: __('Attention'),
|
||||
key: 'attention',
|
||||
type: 'string',
|
||||
param: '',
|
||||
symbol: '@',
|
||||
icon: 'user',
|
||||
tag: '@attention',
|
||||
hideNotEqual: true,
|
||||
};
|
||||
IssuableTokenKeys.tokenKeys.splice(2, 0, attentionRequestedToken);
|
||||
IssuableTokenKeys.tokenKeysWithAlternative.splice(2, 0, attentionRequestedToken);
|
||||
}
|
||||
|
||||
const draftToken = {
|
||||
token: {
|
||||
formattedKey: __('Draft'),
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ export default class AvailableDropdownMappings {
|
|||
gl: DropdownUser,
|
||||
element: this.container.querySelector('#js-dropdown-reviewer'),
|
||||
},
|
||||
attention: {
|
||||
reference: null,
|
||||
gl: DropdownUser,
|
||||
element: this.container.getElementById('js-dropdown-attention-requested'),
|
||||
},
|
||||
'approved-by': {
|
||||
reference: null,
|
||||
gl: DropdownUser,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by', 'reviewer'];
|
||||
export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by', 'reviewer', 'attention'];
|
||||
|
||||
export const DROPDOWN_TYPE = {
|
||||
hint: 'hint',
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { scrollToElementWithContext, scrollToElement } from '~/lib/utils/common_
|
|||
import { updateHistory } from '../../lib/utils/url_utility';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
const isDiffsVirtualScrollingEnabled = () => window.gon?.features?.diffsVirtualScrolling;
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @returns {boolean}
|
||||
|
|
@ -15,7 +13,7 @@ function scrollTo(selector, { withoutContext = false } = {}) {
|
|||
|
||||
if (el) {
|
||||
scrollFunction(el, {
|
||||
behavior: isDiffsVirtualScrollingEnabled() ? 'auto' : 'smooth',
|
||||
behavior: 'auto',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
@ -31,7 +29,7 @@ function updateUrlWithNoteId(noteId) {
|
|||
replace: true,
|
||||
};
|
||||
|
||||
if (noteId && isDiffsVirtualScrollingEnabled()) {
|
||||
if (noteId) {
|
||||
// Temporarily mask the ID to avoid the browser default
|
||||
// scrolling taking over which is broken with virtual
|
||||
// scrolling enabled.
|
||||
|
|
@ -115,17 +113,13 @@ function handleDiscussionJump(self, fn, discussionId = self.currentDiscussionId)
|
|||
const isDiffView = window.mrTabs.currentAction === 'diffs';
|
||||
const targetId = fn(discussionId, isDiffView);
|
||||
const discussion = self.getDiscussion(targetId);
|
||||
const setHash = !isDiffView && !isDiffsVirtualScrollingEnabled();
|
||||
const discussionFilePath = discussion?.diff_file?.file_path;
|
||||
|
||||
if (isDiffsVirtualScrollingEnabled()) {
|
||||
window.location.hash = '';
|
||||
}
|
||||
window.location.hash = '';
|
||||
|
||||
if (discussionFilePath) {
|
||||
self.scrollToFile({
|
||||
path: discussionFilePath,
|
||||
setHash,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import createFlash from '~/flash';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { __, s__ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
|
@ -79,6 +80,7 @@ export default {
|
|||
[STOPPING]: {
|
||||
actionName: STOPPING,
|
||||
buttonText: s__('MrDeploymentActions|Stop environment'),
|
||||
buttonVariant: 'danger',
|
||||
busyText: __('This environment is being deployed'),
|
||||
confirmMessage: __('Are you sure you want to stop this environment?'),
|
||||
errorMessage: __('Something went wrong while stopping this environment. Please try again.'),
|
||||
|
|
@ -86,6 +88,7 @@ export default {
|
|||
[DEPLOYING]: {
|
||||
actionName: DEPLOYING,
|
||||
buttonText: s__('MrDeploymentActions|Deploy'),
|
||||
buttonVariant: 'confirm',
|
||||
busyText: __('This environment is being deployed'),
|
||||
confirmMessage: __('Are you sure you want to deploy this environment?'),
|
||||
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
|
||||
|
|
@ -93,14 +96,27 @@ export default {
|
|||
[REDEPLOYING]: {
|
||||
actionName: REDEPLOYING,
|
||||
buttonText: s__('MrDeploymentActions|Re-deploy'),
|
||||
buttonVariant: 'confirm',
|
||||
busyText: __('This environment is being re-deployed'),
|
||||
confirmMessage: __('Are you sure you want to re-deploy this environment?'),
|
||||
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
executeAction(endpoint, { actionName, confirmMessage, errorMessage }) {
|
||||
const isConfirmed = confirm(confirmMessage); //eslint-disable-line
|
||||
async executeAction(
|
||||
endpoint,
|
||||
{
|
||||
actionName,
|
||||
buttonText: primaryBtnText,
|
||||
buttonVariant: primaryBtnVariant,
|
||||
confirmMessage,
|
||||
errorMessage,
|
||||
},
|
||||
) {
|
||||
const isConfirmed = await confirmAction(confirmMessage, {
|
||||
primaryBtnVariant,
|
||||
primaryBtnText,
|
||||
});
|
||||
|
||||
if (isConfirmed) {
|
||||
this.actionInProgress = actionName;
|
||||
|
|
|
|||
|
|
@ -278,3 +278,33 @@ $gl-line-height-42: px-to-rem(42px);
|
|||
.gl-pr-10 {
|
||||
padding-right: $gl-spacing-scale-10;
|
||||
}
|
||||
|
||||
/* Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709 */
|
||||
.gl-md-grid-template-columns-2 {
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.gl-gap-6 {
|
||||
gap: $gl-spacing-scale-6;
|
||||
}
|
||||
|
||||
$gl-spacing-scale-48: 48 * $grid-size;
|
||||
|
||||
.gl-max-w-48 {
|
||||
max-width: $gl-spacing-scale-48;
|
||||
}
|
||||
|
||||
$gl-spacing-scale-75: 75 * $grid-size;
|
||||
|
||||
.gl-max-w-75 {
|
||||
max-width: $gl-spacing-scale-75;
|
||||
}
|
||||
|
||||
.gl-md-pt-11 {
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $gl-spacing-scale-11 !important; // only need !important for now so that it overrides styles from @gitlab/ui which currently take precedence
|
||||
}
|
||||
}
|
||||
/* End gitlab-ui#1709 */
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ class DashboardController < Dashboard::ApplicationController
|
|||
|
||||
urgency :low, [:merge_requests]
|
||||
|
||||
before_action only: [:merge_requests] do
|
||||
push_frontend_feature_flag(:mr_attention_requests, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
def activity
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
before_action :set_issuables_index, only: [:index]
|
||||
before_action :authenticate_user!, only: [:assign_related_issues]
|
||||
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
|
||||
before_action only: [:index, :show] do
|
||||
push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:file_identifier_hash)
|
||||
push_frontend_feature_flag(:merge_request_widget_graphql, @project, default_enabled: :yaml)
|
||||
|
|
@ -38,9 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:diffs_virtual_scrolling, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:refactor_mr_widgets_extensions, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:rebase_without_ci_ui, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml)
|
||||
|
|
@ -50,6 +52,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:usage_data_diff_searches, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:permit_all_shared_groups_for_approval, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
|
||||
|
||||
after_action :log_merge_request_show, only: [:show]
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ class MergeRequestsFinder < IssuableFinder
|
|||
:reviewer_id,
|
||||
:reviewer_username,
|
||||
:target_branch,
|
||||
:wip
|
||||
:wip,
|
||||
:attention
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ class MergeRequestsFinder < IssuableFinder
|
|||
items = by_approvals(items)
|
||||
items = by_deployments(items)
|
||||
items = by_reviewer(items)
|
||||
items = by_attention(items)
|
||||
|
||||
by_source_project_id(items)
|
||||
end
|
||||
|
|
@ -218,6 +220,12 @@ class MergeRequestsFinder < IssuableFinder
|
|||
end
|
||||
end
|
||||
|
||||
def by_attention(items)
|
||||
return items unless params.attention?
|
||||
|
||||
items.attention(params.attention)
|
||||
end
|
||||
|
||||
def parse_datetime(input)
|
||||
# To work around http://www.ruby-lang.org/en/news/2021/11/15/date-parsing-method-regexp-dos-cve-2021-41817/
|
||||
DateTime.parse(input.byteslice(0, 128)) if input
|
||||
|
|
|
|||
|
|
@ -21,5 +21,11 @@ class MergeRequestsFinder
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def attention
|
||||
strong_memoize(:attention) do
|
||||
User.find_by_username(params[:attention])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ module Projects
|
|||
avenues = [authorizable_project_members]
|
||||
|
||||
avenues << if project.personal?
|
||||
project_owner
|
||||
project_owner_acting_as_maintainer
|
||||
else
|
||||
authorizable_group_members
|
||||
end
|
||||
|
|
@ -85,15 +85,9 @@ module Projects
|
|||
Member.from_union(members)
|
||||
end
|
||||
|
||||
# workaround until we migrate Project#owners to have membership with
|
||||
# OWNER access level
|
||||
def project_owner
|
||||
def project_owner_acting_as_maintainer
|
||||
user_id = project.namespace.owner.id
|
||||
access_level = if ::Feature.enabled?(:personal_project_owner_with_owner_access, default_enabled: :yaml)
|
||||
Gitlab::Access::OWNER
|
||||
else
|
||||
Gitlab::Access::MAINTAINER
|
||||
end
|
||||
access_level = Gitlab::Access::MAINTAINER
|
||||
|
||||
Member
|
||||
.from(generate_from_statement([[user_id, access_level]])) # rubocop: disable CodeReuse/ActiveRecord
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ module BoardsHelper
|
|||
can_update: can_update?.to_s,
|
||||
can_admin_list: can_admin_list?.to_s,
|
||||
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
|
||||
recent_boards_endpoint: recent_boards_path,
|
||||
parent: current_board_parent.model_name.param_key,
|
||||
group_id: group_id,
|
||||
labels_filter_base_path: build_issue_link_base,
|
||||
|
|
@ -128,10 +127,6 @@ module BoardsHelper
|
|||
}
|
||||
end
|
||||
|
||||
def recent_boards_path
|
||||
recent_project_boards_path(@project) if current_board_parent.is_a?(Project)
|
||||
end
|
||||
|
||||
def serializer
|
||||
CurrentBoardSerializer.new
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,14 +8,8 @@ module SelectForProjectAuthorization
|
|||
select("projects.id AS project_id", "members.access_level")
|
||||
end
|
||||
|
||||
# workaround until we migrate Project#owners to have membership with
|
||||
# OWNER access level
|
||||
def select_project_owner_for_project_authorization
|
||||
if ::Feature.enabled?(:personal_project_owner_with_owner_access, default_enabled: :yaml)
|
||||
select(["projects.id AS project_id", "#{Gitlab::Access::OWNER} AS access_level"])
|
||||
else
|
||||
select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"])
|
||||
end
|
||||
def select_as_maintainer_for_project_authorization
|
||||
select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -274,9 +274,7 @@ class ContainerRepository < ApplicationRecord
|
|||
def retry_aborted_migration
|
||||
return unless migration_state == 'import_aborted'
|
||||
|
||||
import_status = gitlab_api_client.import_status(self.path)
|
||||
|
||||
case import_status
|
||||
case external_import_status
|
||||
when 'native'
|
||||
raise NativeImportError
|
||||
when 'import_in_progress'
|
||||
|
|
@ -322,6 +320,12 @@ class ContainerRepository < ApplicationRecord
|
|||
[migration_pre_import_done_at, migration_import_done_at, migration_aborted_at].compact.max
|
||||
end
|
||||
|
||||
def external_import_status
|
||||
strong_memoize(:import_status) do
|
||||
gitlab_api_client.import_status(self.path)
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
def registry
|
||||
@registry ||= begin
|
||||
|
|
|
|||
|
|
@ -406,6 +406,17 @@ class MergeRequest < ApplicationRecord
|
|||
)
|
||||
end
|
||||
|
||||
scope :attention, ->(user) do
|
||||
# rubocop: disable Gitlab/Union
|
||||
union = Gitlab::SQL::Union.new([
|
||||
MergeRequestReviewer.select(:merge_request_id).where(user_id: user.id, state: MergeRequestReviewer.states[:attention_requested]),
|
||||
MergeRequestAssignee.select(:merge_request_id).where(user_id: user.id, state: MergeRequestAssignee.states[:attention_requested])
|
||||
])
|
||||
# rubocop: enable Gitlab/Union
|
||||
|
||||
with(Gitlab::SQL::CTE.new(:reviewers_and_assignees, union).to_arel).where('merge_requests.id in (select merge_request_id from reviewers_and_assignees)')
|
||||
end
|
||||
|
||||
def self.total_time_to_merge
|
||||
join_metrics
|
||||
.merge(MergeRequest::Metrics.with_valid_time_to_merge)
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ module Namespaces
|
|||
end
|
||||
end
|
||||
|
||||
def self_and_hierarchy
|
||||
return super unless use_traversal_ids_for_self_and_hierarchy_scopes?
|
||||
|
||||
unscoped.from_union([all.self_and_ancestors, all.self_and_descendants(include_self: false)])
|
||||
end
|
||||
|
||||
def order_by_depth(hierarchy_order)
|
||||
return all unless hierarchy_order
|
||||
|
||||
|
|
@ -114,6 +120,11 @@ module Namespaces
|
|||
use_traversal_ids?
|
||||
end
|
||||
|
||||
def use_traversal_ids_for_self_and_hierarchy_scopes?
|
||||
Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy_scopes, default_enabled: :yaml) &&
|
||||
use_traversal_ids?
|
||||
end
|
||||
|
||||
def self_and_descendants_with_comparison_operators(include_self: true)
|
||||
base = all.select(
|
||||
:traversal_ids,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ module Namespaces
|
|||
self_and_descendants(include_self: include_self).as_ids
|
||||
end
|
||||
alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
|
||||
|
||||
def self_and_hierarchy
|
||||
Gitlab::ObjectHierarchy.new(all).all_objects
|
||||
end
|
||||
alias_method :recursive_self_and_hierarchy, :self_and_hierarchy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ class Project < ApplicationRecord
|
|||
delegate :name, to: :owner, allow_nil: true, prefix: true
|
||||
delegate :members, to: :team, prefix: true
|
||||
delegate :add_user, :add_users, to: :team
|
||||
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role, to: :team
|
||||
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
|
||||
delegate :group_runners_enabled, :group_runners_enabled=, to: :ci_cd_settings, allow_nil: true
|
||||
delegate :root_ancestor, to: :namespace, allow_nil: true
|
||||
delegate :last_pipeline, to: :commit, allow_nil: true
|
||||
|
|
|
|||
|
|
@ -23,10 +23,6 @@ class ProjectTeam
|
|||
add_user(user, :maintainer, current_user: current_user)
|
||||
end
|
||||
|
||||
def add_owner(user, current_user: nil)
|
||||
add_user(user, :owner, current_user: current_user)
|
||||
end
|
||||
|
||||
def add_role(user, role, current_user: nil)
|
||||
public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
|
@ -107,9 +103,7 @@ class ProjectTeam
|
|||
if group
|
||||
group.owners
|
||||
else
|
||||
# workaround until we migrate Project#owners to have membership with
|
||||
# OWNER access level
|
||||
Array.wrap(fetch_members(Gitlab::Access::OWNER)) | Array.wrap(project.owner)
|
||||
[project.owner]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module Members
|
|||
module Projects
|
||||
class CreatorService < Members::CreatorService
|
||||
def self.access_levels
|
||||
Gitlab::Access.sym_options_with_owner
|
||||
Gitlab::Access.sym_options
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ module NotificationRecipients
|
|||
return [] unless project
|
||||
|
||||
add_recipients(project.team.maintainers, :mention, nil)
|
||||
add_recipients(project.team.owners, :mention, nil)
|
||||
end
|
||||
|
||||
def acting_user
|
||||
|
|
|
|||
|
|
@ -147,11 +147,7 @@ module Projects
|
|||
priority: UserProjectAccessChangedService::LOW_PRIORITY
|
||||
)
|
||||
else
|
||||
if ::Feature.enabled?(:personal_project_owner_with_owner_access, default_enabled: :yaml)
|
||||
@project.add_owner(@project.namespace.owner, current_user: current_user)
|
||||
else
|
||||
@project.add_maintainer(@project.namespace.owner, current_user: current_user)
|
||||
end
|
||||
@project.add_maintainer(@project.namespace.owner, current_user: current_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
-# We'll eventually migrate to .gl-display-none: https://gitlab.com/gitlab-org/gitlab/-/issues/351792.
|
||||
= gl_badge_tag({ size: :sm, variant: :info }, { class: "js-todos-count gl-ml-n2#{(' hidden' if todos_pending_count == 0)}", "aria-label": _("Todos count") }) do
|
||||
= todos_count_format(todos_pending_count)
|
||||
%li.nav-item.header-help.dropdown.d-none.d-md-block{ **tracking_attrs('main_navigation', 'click_question_mark_link', 'navigation') }
|
||||
%li.nav-item.header-help.dropdown.d-none.d-md-block{ data: { track_action: 'click_question_mark_link', track_label: 'main_navigation', track_property: 'navigation', track_experiment: 'cross_stage_fdm' } }
|
||||
= link_to help_path, class: 'header-help-dropdown-toggle gl-relative', data: { toggle: "dropdown" } do
|
||||
%span.gl-sr-only
|
||||
= s_('Nav|Help')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
%ul
|
||||
- if current_user_menu?(:help)
|
||||
= render 'layouts/header/gitlab_version'
|
||||
= render_if_exists 'layouts/header/help_dropdown/cross_stage_fdm'
|
||||
= render 'layouts/header/whats_new_dropdown_item'
|
||||
%li
|
||||
= link_to _("Help"), help_path
|
||||
|
|
|
|||
|
|
@ -107,6 +107,16 @@
|
|||
= render 'shared/issuable/user_dropdown_item',
|
||||
user: User.new(username: '{{username}}', name: '{{name}}'),
|
||||
avatar: { lazy: true, url: '{{avatar_url}}' }
|
||||
- if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml)
|
||||
#js-dropdown-attention-requested.filtered-search-input-dropdown-menu.dropdown-menu
|
||||
- if current_user
|
||||
%ul{ data: { dropdown: true } }
|
||||
= render 'shared/issuable/user_dropdown_item',
|
||||
user: current_user
|
||||
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
|
||||
= render 'shared/issuable/user_dropdown_item',
|
||||
user: User.new(username: '{{username}}', name: '{{name}}'),
|
||||
avatar: { lazy: true, url: '{{avatar_url}}' }
|
||||
= render_if_exists 'shared/issuable/approver_dropdown'
|
||||
= render_if_exists 'shared/issuable/approved_by_dropdown'
|
||||
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
|
||||
|
|
|
|||
|
|
@ -21,18 +21,68 @@ module ContainerRegistry
|
|||
|
||||
repositories = ::ContainerRepository.with_stale_migration(step_before_timestamp)
|
||||
.limit(max_capacity)
|
||||
aborts_count = 0
|
||||
long_running_migration_ids = []
|
||||
|
||||
# the #to_a is safe as the amount of entries is limited.
|
||||
# In addition, we're calling #each in the next line and we don't want two different SQL queries for these two lines
|
||||
log_extra_metadata_on_done(:stale_migrations_count, repositories.to_a.size)
|
||||
|
||||
repositories.each do |repository|
|
||||
repository.abort_import
|
||||
if abortable?(repository)
|
||||
repository.abort_import
|
||||
aborts_count += 1
|
||||
else
|
||||
long_running_migration_ids << repository.id if long_running_migration?(repository)
|
||||
end
|
||||
end
|
||||
|
||||
log_extra_metadata_on_done(:aborted_stale_migrations_count, aborts_count)
|
||||
|
||||
if long_running_migration_ids.any?
|
||||
log_extra_metadata_on_done(:long_running_stale_migration_container_repository_ids, long_running_migration_ids)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This can ping the Container Registry API.
|
||||
# We loop on a set of repositories to calls this function (see #perform)
|
||||
# In the worst case scenario, we have a n+1 API calls situation here.
|
||||
#
|
||||
# This is reasonable because the maximum amount of repositories looped
|
||||
# on is `25`. See ::ContainerRegistry::Migration.capacity.
|
||||
#
|
||||
# TODO We can remove this n+1 situation by having a Container Registry API
|
||||
# endpoint that accepts multiple repository paths at once. This is issue
|
||||
# https://gitlab.com/gitlab-org/container-registry/-/issues/582
|
||||
def abortable?(repository)
|
||||
# early return to save one Container Registry API request
|
||||
return true unless repository.importing? || repository.pre_importing?
|
||||
return true unless external_migration_in_progress?(repository)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def long_running_migration?(repository)
|
||||
migration_start_timestamp(repository).before?(long_running_migration_threshold)
|
||||
end
|
||||
|
||||
def external_migration_in_progress?(repository)
|
||||
status = repository.external_import_status
|
||||
|
||||
(status == 'pre_import_in_progress' && repository.pre_importing?) ||
|
||||
(status == 'import_in_progress' && repository.importing?)
|
||||
end
|
||||
|
||||
def migration_start_timestamp(repository)
|
||||
if repository.pre_importing?
|
||||
repository.migration_pre_import_started_at
|
||||
else
|
||||
repository.migration_import_started_at
|
||||
end
|
||||
end
|
||||
|
||||
def step_before_timestamp
|
||||
::ContainerRegistry::Migration.max_step_duration.seconds.ago
|
||||
end
|
||||
|
|
@ -42,6 +92,10 @@ module ContainerRegistry
|
|||
# is not properly applied
|
||||
::ContainerRegistry::Migration.capacity * 2
|
||||
end
|
||||
|
||||
def long_running_migration_threshold
|
||||
@threshold ||= 30.minutes.ago
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: diffs_virtual_scrolling
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60312
|
||||
rollout_issue_url:
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: permit_all_shared_groups_for_approval
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80655
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352766
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: personal_project_owner_with_owner_access
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78193
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351919
|
||||
name: use_traversal_ids_for_self_and_hierarchy_scopes
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80045
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352120
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::workspace
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToMergeRequestAssigneesState < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_on_merge_request_assignees_state'
|
||||
|
||||
def up
|
||||
add_concurrent_index :merge_request_assignees, :state, where: 'state = 2', name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :merge_request_assignees, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToMergeRequestReviewersState < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_on_merge_request_reviewers_state'
|
||||
|
||||
def up
|
||||
add_concurrent_index :merge_request_reviewers, :state, where: 'state = 2', name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :merge_request_reviewers, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
7707b9bcdcd7ec28af31b90355905eb8971c12a90c4334b0ae214e45fac9645a
|
||||
|
|
@ -0,0 +1 @@
|
|||
350409be3f491b61a1d757dbd839b48d088339883e8e019d00ad90e6adc350e6
|
||||
|
|
@ -27129,6 +27129,10 @@ CREATE UNIQUE INDEX index_on_instance_statistics_recorded_at_and_identifier ON a
|
|||
|
||||
CREATE INDEX index_on_label_links_all_columns ON label_links USING btree (target_id, label_id, target_type);
|
||||
|
||||
CREATE INDEX index_on_merge_request_assignees_state ON merge_request_assignees USING btree (state) WHERE (state = 2);
|
||||
|
||||
CREATE INDEX index_on_merge_request_reviewers_state ON merge_request_reviewers USING btree (state) WHERE (state = 2);
|
||||
|
||||
CREATE INDEX index_on_merge_requests_for_latest_diffs ON merge_requests USING btree (target_project_id) INCLUDE (id, latest_merge_request_diff_id);
|
||||
|
||||
COMMENT ON INDEX index_on_merge_requests_for_latest_diffs IS 'Index used to efficiently obtain the oldest merge request for a commit SHA';
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ For example, many projects do releases but don't need to do hotfixes.
|
|||
## GitHub flow as a simpler alternative
|
||||
|
||||
In reaction to Git flow, GitHub created a simpler alternative.
|
||||
[GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `main` branch:
|
||||
[GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) has only feature branches and a `main` branch:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
|
|
@ -341,7 +341,7 @@ However, as discussed in [the section about rebasing](#squashing-commits-with-re
|
|||
|
||||
Rebasing could create more work, as every time you rebase, you may need to resolve the same conflicts.
|
||||
Sometimes you can reuse recorded resolutions (`rerere`), but merging is better, because you only have to resolve conflicts once.
|
||||
Atlassian has a more thorough explanation of the tradeoffs between merging and rebasing [on their blog](https://www.atlassian.com/blog/git/git-team-workflows-merge-or-rebase).
|
||||
Atlassian has [a more thorough explanation of the tradeoffs between merging and rebasing](https://www.atlassian.com/blog/git/git-team-workflows-merge-or-rebase) on their blog.
|
||||
|
||||
A good way to prevent creating many merge commits is to not frequently merge `main` into the feature branch.
|
||||
There are three reasons to merge in `main`: utilizing new code, resolving merge conflicts, and updating long-running branches.
|
||||
|
|
@ -403,7 +403,7 @@ git commit -m 'Properly escape special characters in XML generation'
|
|||
|
||||
An example of a good commit message is: "Combine templates to reduce duplicate code in the user views."
|
||||
The words "change," "improve," "fix," and "refactor" don't add much information to a commit message.
|
||||
For more information about formatting commit messages, please see this excellent [blog post by Tim Pope](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
For more information, please see Tim Pope's excellent [note about formatting commit messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
To add more context to a commit message, consider adding information regarding the
|
||||
origin of the change. For example, the URL of a GitLab issue, or a Jira issue number,
|
||||
|
|
|
|||
|
|
@ -351,7 +351,8 @@ or [init scripts](upgrading_from_source.md#configure-sysv-init-script) by [follo
|
|||
In 14.5 we introduce a set of migrations that wrap up this process by making sure
|
||||
that all remaining jobs over the `merge_request_diff_commits` table are completed.
|
||||
These jobs will have already been processed in most cases so that no extra time is necessary during an upgrade to 14.5.
|
||||
But if there are remaining jobs, the deployment may take a few extra minutes to complete.
|
||||
However, if there are remaining jobs or you haven't already upgraded to 14.1,
|
||||
the deployment may take multiple hours to complete.
|
||||
|
||||
All merge request diff commits will automatically incorporate these changes, and there are no
|
||||
additional requirements to perform the upgrade.
|
||||
|
|
@ -439,6 +440,11 @@ for how to proceed.
|
|||
with self-managed installations, and ensure 14.0.0-14.0.4 installations do not
|
||||
encounter issues with [batched background migrations](#batched-background-migrations).
|
||||
|
||||
- Upgrading to GitLab [14.5](#1450) (or later) may take a lot longer if you do not upgrade to at least 14.1
|
||||
first. The 14.1 merge request diff commits database migration can take hours to run, but runs in the
|
||||
background while GitLab is in use. GitLab instances upgraded directly from 14.0 to 14.5 or later must
|
||||
run the migration in the foreground and therefore take a lot longer to complete.
|
||||
|
||||
- See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144).
|
||||
|
||||
### 14.0.0
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ When you are using the GitLab Agent for Kubernetes, you might experience issues
|
|||
You can start by viewing the service logs:
|
||||
|
||||
```shell
|
||||
kubectl logs -f -l=app=gitlab-kubernetes-agent -n gitlab-kubernetes-agent
|
||||
kubectl logs -f -l=app=gitlab-agent -n gitlab-kubernetes-agent
|
||||
```
|
||||
|
||||
If you are a GitLab administrator, you can also view the [GitLab Agent Server logs](../../../administration/clusters/kas.md#troubleshooting).
|
||||
|
|
|
|||
|
|
@ -33,27 +33,14 @@ usernames. A GitLab administrator can configure the GitLab instance to
|
|||
|
||||
## Project members permissions
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219299) in GitLab 14.8, personal namespace owners appear with Owner role in new projects in their namespace. Introduced [with a flag](../administration/feature_flags.md) named `personal_project_owner_with_owner_access`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, personal namespace owners appearing with the Owner role in new projects in their namespace is disabled. To make it available,
|
||||
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `personal_project_owner_with_owner_access`.
|
||||
The feature is not ready for production use.
|
||||
On GitLab.com, this feature is not available.
|
||||
|
||||
A user's role determines what permissions they have on a project. The Owner role provides all permissions but is
|
||||
available only:
|
||||
|
||||
- For group owners. The role is inherited for a group's projects.
|
||||
- For Administrators.
|
||||
|
||||
Personal [namespace](group/index.md#namespaces) owners:
|
||||
|
||||
- Are displayed as having the Maintainer role on projects in the namespace, but have the same permissions as a user with the Owner role.
|
||||
- (Disabled by default) In GitLab 14.8 and later, for new projects in the namespace, are displayed as having the Owner role.
|
||||
|
||||
For more information about how to manage project members, see
|
||||
[members of a project](project/members/index.md).
|
||||
Personal namespace owners have the same permissions as an Owner, but are displayed with the Maintainer role on projects created in their personal namespace.
|
||||
For more information, see [projects members documentation](project/members/index.md).
|
||||
|
||||
The following table lists project permissions available for each role:
|
||||
|
||||
|
|
|
|||
|
|
@ -32,24 +32,33 @@ in the search field in the upper right corner:
|
|||
|
||||
### Filter issue and merge request lists
|
||||
|
||||
> - Filter by Epics was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/195704) in GitLab Ultimate 12.9.
|
||||
> - Filter by child Epics was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9029) in GitLab Ultimate 13.0.
|
||||
> - Filter by Iterations was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6. Moved to GitLab Premium in 13.9.
|
||||
> - Filtering by epics was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/195704) in GitLab 12.9.
|
||||
> - Filtering by child epics was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9029) in GitLab 13.0.
|
||||
> - Filtering by iterations was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6. Moved from GitLab Ultimate to Premium in 13.9.
|
||||
> - Filtering by type was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322755) in GitLab 13.10 [with a flag](../../administration/feature_flags.md) named `vue_issues_list`. Disabled by default.
|
||||
|
||||
Follow these steps to filter the **Issues** and **Merge requests** list pages in projects and
|
||||
groups:
|
||||
|
||||
1. Click in the field **Search or filter results...**.
|
||||
1. In the dropdown menu that appears, select the attribute you wish to filter by:
|
||||
- Author
|
||||
- Assignee
|
||||
- [Milestone](../project/milestones/index.md)
|
||||
- [Iteration](../group/iterations/index.md)
|
||||
- Release
|
||||
- [Label](../project/labels.md)
|
||||
- My-reaction
|
||||
- Author
|
||||
- Confidential
|
||||
- [Epic and child Epic](../group/epics/index.md) (available only for the group the Epic was created, not for [higher group levels](https://gitlab.com/gitlab-org/gitlab/-/issues/233729)).
|
||||
- [Iteration](../group/iterations/index.md)
|
||||
- [Label](../project/labels.md)
|
||||
- [Milestone](../project/milestones/index.md)
|
||||
- My-reaction
|
||||
- Release
|
||||
- Type
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default filtering by type is not available.
|
||||
To make it available per group, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `vue_issues_list`.
|
||||
On GitLab.com, this feature is not available.
|
||||
|
||||
- Weight
|
||||
- Search for this text
|
||||
1. Select or type the operator to use for filtering the attribute. The following operators are
|
||||
available:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ module ContainerRegistry
|
|||
end
|
||||
end
|
||||
|
||||
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#compliance-check
|
||||
def supports_gitlab_api?
|
||||
strong_memoize(:supports_gitlab_api) do
|
||||
registry_features = Gitlab::CurrentSettings.container_registry_features || []
|
||||
|
|
@ -35,16 +36,19 @@ module ContainerRegistry
|
|||
end
|
||||
end
|
||||
|
||||
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#import-repository
|
||||
def pre_import_repository(path)
|
||||
response = start_import_for(path, pre: true)
|
||||
IMPORT_RESPONSES.fetch(response.status, :error)
|
||||
end
|
||||
|
||||
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#import-repository
|
||||
def import_repository(path)
|
||||
response = start_import_for(path, pre: false)
|
||||
IMPORT_RESPONSES.fetch(response.status, :error)
|
||||
end
|
||||
|
||||
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#get-repository-import-status
|
||||
def import_status(path)
|
||||
body_hash = response_body(faraday.get(import_url_for(path)))
|
||||
body_hash['status'] || 'error'
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ module ContainerRegistry
|
|||
end
|
||||
|
||||
def self.capacity
|
||||
# Increasing capacity numbers will increase the n+1 API calls we can have
|
||||
# in ContainerRegistry::Migration::GuardWorker#external_migration_in_progress?
|
||||
#
|
||||
# TODO: See https://gitlab.com/gitlab-org/container-registry/-/issues/582
|
||||
#
|
||||
return 25 if Feature.enabled?(:container_registry_migration_phase2_capacity_25)
|
||||
return 10 if Feature.enabled?(:container_registry_migration_phase2_capacity_10)
|
||||
return 1 if Feature.enabled?(:container_registry_migration_phase2_capacity_1)
|
||||
|
|
|
|||
|
|
@ -33,13 +33,7 @@ module Gitlab
|
|||
MAINTAINER_SUBGROUP_ACCESS = 1
|
||||
|
||||
class << self
|
||||
def values
|
||||
if ::Feature.enabled?(:personal_project_owner_with_owner_access, default_enabled: :yaml)
|
||||
options_with_owner.values
|
||||
else
|
||||
options.values
|
||||
end
|
||||
end
|
||||
delegate :values, to: :options
|
||||
|
||||
def all_values
|
||||
options_with_owner.values
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module Gitlab
|
|||
user.projects_with_active_memberships.select_for_project_authorization,
|
||||
|
||||
# The personal projects of the user.
|
||||
user.personal_projects.select_project_owner_for_project_authorization,
|
||||
user.personal_projects.select_as_maintainer_for_project_authorization,
|
||||
|
||||
# Projects that belong directly to any of the groups the user has
|
||||
# access to.
|
||||
|
|
|
|||
|
|
@ -4999,6 +4999,9 @@ msgstr[1] ""
|
|||
msgid "Attaching the file failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Attention"
|
||||
msgstr ""
|
||||
|
||||
msgid "Audit Events"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18605,6 +18608,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|3 ways to dive into GitLab CI/CD"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Access advanced features, build more efficiently, strengthen security and compliance."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Actually, GitLab makes the team work (better)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18638,6 +18644,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Ultimate and enable these features in less than 5 minutes with no credit card required."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Collaboration across stages in GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Create a custom CI runner with just a few clicks"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18662,6 +18671,12 @@ msgstr ""
|
|||
msgid "InProductMarketing|Dig in and create a project and a repo"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Discover Premium & Ultimate"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Discover Premium & Ultimate."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Do you have a minute?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18869,6 +18884,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Start a Self-Managed trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Start a free trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Start a free trial of GitLab Ultimate – no credit card required"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40305,6 +40323,15 @@ msgstr ""
|
|||
msgid "VulnerabilityManagement|Create Jira issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Enter a name"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Enter the CVE or CWE code"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Enter the CVE or CWE identifier URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Fetching linked Jira issues"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40329,6 +40356,12 @@ msgstr ""
|
|||
msgid "VulnerabilityManagement|Select a method"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Select a severity level"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Select a status"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Severity is a required field"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -665,7 +665,7 @@ RSpec.describe Projects::ProjectMembersController do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'creates a member' do
|
||||
it 'does not create a member' do
|
||||
expect do
|
||||
post :create, params: {
|
||||
user_ids: stranger.id,
|
||||
|
|
@ -673,9 +673,7 @@ RSpec.describe Projects::ProjectMembersController do
|
|||
access_level: Member::OWNER,
|
||||
project_id: project
|
||||
}
|
||||
end.to change { project.members.count }.by(1)
|
||||
|
||||
expect(project.team_members).to include(user)
|
||||
end.to change { project.members.count }.by(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ RSpec.describe 'Merge request > Batch comments', :js do
|
|||
|
||||
expect(page).to have_selector('[data-testid="review_bar_component"]')
|
||||
|
||||
expect(find('.review-bar-content .btn-confirm')).to have_content('1')
|
||||
expect(find('[data-testid="review_bar_component"] .btn-confirm')).to have_content('1')
|
||||
end
|
||||
|
||||
it 'publishes review' do
|
||||
|
|
@ -150,10 +150,6 @@ RSpec.describe 'Merge request > Batch comments', :js do
|
|||
|
||||
it 'adds draft comments to both sides' do
|
||||
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9')
|
||||
|
||||
# make sure line 9 is in the view
|
||||
execute_script("window.scrollBy(0, -200)")
|
||||
|
||||
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9', button_text: 'Add to review', text: 'Another wrong line')
|
||||
|
||||
expect(find('.new .draft-note-component')).to have_content('Line is wrong')
|
||||
|
|
@ -255,13 +251,15 @@ RSpec.describe 'Merge request > Batch comments', :js do
|
|||
end
|
||||
|
||||
def write_diff_comment(**params)
|
||||
click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']"))
|
||||
|
||||
write_comment(**params)
|
||||
end
|
||||
|
||||
def write_parallel_comment(line, **params)
|
||||
find("div[id='#{line}']").hover
|
||||
line_element = find_by_scrolling("[id='#{line}']")
|
||||
scroll_to_elements_bottom(line_element)
|
||||
line_element.hover
|
||||
find(".js-add-diff-note-button").click
|
||||
|
||||
write_comment(selector: "form[data-line-code='#{line}']", **params)
|
||||
|
|
|
|||
|
|
@ -25,14 +25,15 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
context 'when toggling inline comments' do
|
||||
context 'in a single file' do
|
||||
it 'hides a comment' do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
line_element = find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']").find(:xpath, "..")
|
||||
click_diff_line(line_element)
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: 'Line is wrong')
|
||||
click_button('Add comment now')
|
||||
end
|
||||
|
||||
page.within('.diff-files-holder > div:nth-child(6)') do
|
||||
page.within(line_element.ancestor('[data-path]')) do
|
||||
expect(page).to have_content('Line is wrong')
|
||||
|
||||
find('.js-diff-more-actions').click
|
||||
|
|
@ -45,7 +46,9 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
context 'in multiple files' do
|
||||
it 'toggles comments' do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
|
||||
first_line_element = find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']").find(:xpath, "..")
|
||||
first_root_element = first_line_element.ancestor('[data-path]')
|
||||
click_diff_line(first_line_element)
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: 'Line is correct')
|
||||
|
|
@ -54,11 +57,14 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do
|
||||
page.within(first_root_element) do
|
||||
expect(page).to have_content('Line is correct')
|
||||
end
|
||||
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
second_line_element = find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']")
|
||||
second_root_element = second_line_element.ancestor('[data-path]')
|
||||
|
||||
click_diff_line(second_line_element)
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: 'Line is wrong')
|
||||
|
|
@ -68,7 +74,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
wait_for_requests
|
||||
|
||||
# Hide the comment.
|
||||
page.within('.diff-files-holder > div:nth-child(6)') do
|
||||
page.within(second_root_element) do
|
||||
find('.js-diff-more-actions').click
|
||||
click_button 'Hide comments on this file'
|
||||
|
||||
|
|
@ -77,37 +83,36 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
# At this moment a user should see only one comment.
|
||||
# The other one should be hidden.
|
||||
page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do
|
||||
page.within(first_root_element) do
|
||||
expect(page).to have_content('Line is correct')
|
||||
end
|
||||
|
||||
# Show the comment.
|
||||
page.within('.diff-files-holder > div:nth-child(6)') do
|
||||
page.within(second_root_element) do
|
||||
find('.js-diff-more-actions').click
|
||||
click_button 'Show comments on this file'
|
||||
end
|
||||
|
||||
# Now both the comments should be shown.
|
||||
page.within('.diff-files-holder > div:nth-child(6) .note-body > .note-text') do
|
||||
page.within(second_root_element) do
|
||||
expect(page).to have_content('Line is wrong')
|
||||
end
|
||||
|
||||
page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do
|
||||
page.within(first_root_element) do
|
||||
expect(page).to have_content('Line is correct')
|
||||
end
|
||||
|
||||
# Check the same comments in the side-by-side view.
|
||||
execute_script("window.scrollTo(0,0);")
|
||||
find('.js-show-diff-settings').click
|
||||
click_button 'Side-by-side'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.diff-files-holder > div:nth-child(6) .parallel .note-body > .note-text') do
|
||||
page.within(second_root_element) do
|
||||
expect(page).to have_content('Line is wrong')
|
||||
end
|
||||
|
||||
page.within('.diff-files-holder > div:nth-child(5) .parallel .note-body > .note-text') do
|
||||
page.within(first_root_element) do
|
||||
expect(page).to have_content('Line is correct')
|
||||
end
|
||||
end
|
||||
|
|
@ -121,7 +126,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
context 'when adding multiline comments' do
|
||||
it 'saves a multiline comment' do
|
||||
click_diff_line(find("[id='#{sample_commit.line_code}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']").find(:xpath, '..'))
|
||||
add_comment('-13', '+14')
|
||||
end
|
||||
|
||||
|
|
@ -133,13 +138,13 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
# In `files/ruby/popen.rb`
|
||||
it 'allows comments for changes involving both sides' do
|
||||
# click +15, select -13 add and verify comment
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .right-side a[data-linenumber="15"]').find(:xpath, '../../..'), 'right')
|
||||
click_diff_line(find_by_scrolling('div[data-path="files/ruby/popen.rb"] .right-side a[data-linenumber="15"]').find(:xpath, '../../..'), 'right')
|
||||
add_comment('-13', '+15')
|
||||
end
|
||||
|
||||
it 'allows comments on previously hidden lines at the top of a file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/285294' do
|
||||
# Click -9, expand up, select 1 add and verify comment
|
||||
page.within('[data-path="files/ruby/popen.rb"]') do
|
||||
page.within find_by_scrolling('[data-path="files/ruby/popen.rb"]') do
|
||||
all('.js-unfold-all')[0].click
|
||||
end
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="9"]').find(:xpath, '../..'), 'left')
|
||||
|
|
@ -148,7 +153,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
it 'allows comments on previously hidden lines the middle of a file' do
|
||||
# Click 27, expand up, select 18, add and verify comment
|
||||
page.within('[data-path="files/ruby/popen.rb"]') do
|
||||
page.within find_by_scrolling('[data-path="files/ruby/popen.rb"]') do
|
||||
all('.js-unfold-all')[1].click
|
||||
end
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="21"]').find(:xpath, '../..'), 'left')
|
||||
|
|
@ -157,7 +162,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
it 'allows comments on previously hidden lines at the bottom of a file' do
|
||||
# Click +28, expand down, select 37 add and verify comment
|
||||
page.within('[data-path="files/ruby/popen.rb"]') do
|
||||
page.within find_by_scrolling('[data-path="files/ruby/popen.rb"]') do
|
||||
all('.js-unfold-down:not([disabled])')[1].click
|
||||
end
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="30"]').find(:xpath, '../..'), 'left')
|
||||
|
|
@ -198,7 +203,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
context 'when editing comments' do
|
||||
it 'edits a comment' do
|
||||
click_diff_line(find("[id='#{sample_commit.line_code}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in(:note_note, with: 'Line is wrong')
|
||||
|
|
@ -224,7 +229,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
context 'when deleting comments' do
|
||||
it 'deletes a comment' do
|
||||
click_diff_line(find("[id='#{sample_commit.line_code}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in(:note_note, with: 'Line is wrong')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ RSpec.describe 'User expands diff', :js do
|
|||
let(:merge_request) { create(:merge_request, source_branch: 'expand-collapse-files', source_project: project, target_project: project) }
|
||||
|
||||
before do
|
||||
allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(100.kilobytes)
|
||||
allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(100.bytes)
|
||||
|
||||
visit(diffs_project_merge_request_path(project, merge_request))
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ RSpec.describe 'User expands diff', :js do
|
|||
end
|
||||
|
||||
it 'allows user to expand diff' do
|
||||
page.within find('[id="19763941ab80e8c09871c0a425f0560d9053bcb3"]') do
|
||||
page.within find("[id='4c76a1271e41072d7da9fe40bf0f79f7384d472a']") do
|
||||
find('[data-testid="expand-button"]').click
|
||||
|
||||
wait_for_requests
|
||||
|
|
|
|||
|
|
@ -15,16 +15,14 @@ RSpec.describe 'Batch diffs', :js do
|
|||
visit diffs_project_merge_request_path(merge_request.project, merge_request)
|
||||
wait_for_requests
|
||||
|
||||
# Add discussion to first line of first file
|
||||
click_diff_line(find('.diff-file.file-holder:first-of-type .line_holder .left-side:first-of-type'))
|
||||
page.within('.js-discussion-note-form') do
|
||||
click_diff_line(get_first_diff.find('[data-testid="left-side"]', match: :first))
|
||||
page.within get_first_diff.find('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: 'First Line Comment')
|
||||
click_button('Add comment now')
|
||||
end
|
||||
|
||||
# Add discussion to first line of last file
|
||||
click_diff_line(find('.diff-file.file-holder:last-of-type .line_holder .left-side:first-of-type'))
|
||||
page.within('.js-discussion-note-form') do
|
||||
click_diff_line(get_second_diff.find('[data-testid="left-side"]', match: :first))
|
||||
page.within get_second_diff.find('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: 'Last Line Comment')
|
||||
click_button('Add comment now')
|
||||
end
|
||||
|
|
@ -36,17 +34,14 @@ RSpec.describe 'Batch diffs', :js do
|
|||
# Reload so we know the discussions are persisting across batch loads
|
||||
visit page.current_url
|
||||
|
||||
# Wait for JS to settle
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.diff-files-holder .file-holder', count: 39)
|
||||
|
||||
# Confirm discussions are applied to appropriate files (should be contained in multiple diff pages)
|
||||
page.within('.diff-file.file-holder:first-of-type .notes .timeline-entry .note .note-text') do
|
||||
page.within get_first_diff.find('.notes .timeline-entry .note .note-text') do
|
||||
expect(page).to have_content('First Line Comment')
|
||||
end
|
||||
|
||||
page.within('.diff-file.file-holder:last-of-type .notes .timeline-entry .note .note-text') do
|
||||
page.within get_second_diff.find('.notes .timeline-entry .note .note-text') do
|
||||
expect(page).to have_content('Last Line Comment')
|
||||
end
|
||||
end
|
||||
|
|
@ -54,7 +49,7 @@ RSpec.describe 'Batch diffs', :js do
|
|||
context 'when user visits a URL with a link directly to to a discussion' do
|
||||
context 'which is in the first batched page of diffs' do
|
||||
it 'scrolls to the correct discussion' do
|
||||
page.within('.diff-file.file-holder:first-of-type') do
|
||||
page.within get_first_diff do
|
||||
click_link('just now')
|
||||
end
|
||||
|
||||
|
|
@ -63,15 +58,15 @@ RSpec.describe 'Batch diffs', :js do
|
|||
wait_for_requests
|
||||
|
||||
# Confirm scrolled to correct UI element
|
||||
expect(page.find('.diff-file.file-holder:first-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
|
||||
expect(page.find('.diff-file.file-holder:last-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
|
||||
expect(get_first_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
|
||||
expect(get_second_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'which is in at least page 2 of the batched pages of diffs' do
|
||||
it 'scrolls to the correct discussion',
|
||||
quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/293814' } do
|
||||
page.within('.diff-file.file-holder:last-of-type') do
|
||||
page.within get_first_diff do
|
||||
click_link('just now')
|
||||
end
|
||||
|
||||
|
|
@ -80,8 +75,8 @@ RSpec.describe 'Batch diffs', :js do
|
|||
wait_for_requests
|
||||
|
||||
# Confirm scrolled to correct UI element
|
||||
expect(page.find('.diff-file.file-holder:first-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
|
||||
expect(page.find('.diff-file.file-holder:last-of-type .discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
|
||||
expect(get_first_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_truthy
|
||||
expect(get_second_diff.find('.discussion-notes .timeline-entry li.note[id]').obscured?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -95,15 +90,21 @@ RSpec.describe 'Batch diffs', :js do
|
|||
end
|
||||
|
||||
it 'has the correct discussions applied to files across batched pages' do
|
||||
expect(page).to have_selector('.diff-files-holder .file-holder', count: 39)
|
||||
|
||||
page.within('.diff-file.file-holder:first-of-type .notes .timeline-entry .note .note-text') do
|
||||
page.within get_first_diff.find('.notes .timeline-entry .note .note-text') do
|
||||
expect(page).to have_content('First Line Comment')
|
||||
end
|
||||
|
||||
page.within('.diff-file.file-holder:last-of-type .notes .timeline-entry .note .note-text') do
|
||||
page.within get_second_diff.find('.notes .timeline-entry .note .note-text') do
|
||||
expect(page).to have_content('Last Line Comment')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_first_diff
|
||||
find('#a9b6f940524f646951cc28d954aa41f814f95d4f')
|
||||
end
|
||||
|
||||
def get_second_diff
|
||||
find('#b285a86891571c7fdbf1f82e840816079de1cc8b')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,54 +29,54 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
|
||||
context 'with an old line on the left and no line on the right' do
|
||||
it 'allows commenting on the left side' do
|
||||
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]'), 'left')
|
||||
should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]'), 'left')
|
||||
end
|
||||
|
||||
it 'does not allow commenting on the right side' do
|
||||
should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
|
||||
should_not_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no line on the left and a new line on the right' do
|
||||
it 'does not allow commenting on the left side' do
|
||||
should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
|
||||
should_not_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
|
||||
end
|
||||
|
||||
it 'allows commenting on the right side' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an old line on the left and a new line on the right' do
|
||||
it 'allows commenting on the left side', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/199050' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
|
||||
end
|
||||
|
||||
it 'allows commenting on the right side' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unchanged line on the left and an unchanged line on the right' do
|
||||
it 'allows commenting on the left side', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196826' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
|
||||
end
|
||||
|
||||
it 'allows commenting on the right side' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a match line' do
|
||||
it 'does not allow commenting' do
|
||||
line_holder = find('.match', match: :first)
|
||||
line_holder = find_by_scrolling('.match', match: :first)
|
||||
match_should_not_allow_commenting(line_holder)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unfolded line' do
|
||||
before do
|
||||
page.within('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
|
||||
page.within find_by_scrolling('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
|
||||
find('.js-unfold', match: :first).click
|
||||
end
|
||||
|
||||
|
|
@ -84,12 +84,12 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
end
|
||||
|
||||
it 'allows commenting on the left side' do
|
||||
should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="left-side"]'))
|
||||
should_allow_commenting(find_by_scrolling('#a5cc2925ca8258af241be7e5b0381edf30266302').first('.line_holder [data-testid="left-side"]'))
|
||||
end
|
||||
|
||||
it 'allows commenting on the right side' do
|
||||
# Automatically shifts comment box to left side.
|
||||
should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="right-side"]'))
|
||||
should_allow_commenting(find_by_scrolling('#a5cc2925ca8258af241be7e5b0381edf30266302').first('.line_holder [data-testid="right-side"]'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -101,44 +101,44 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
|
||||
context 'after deleteing a note' do
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
|
||||
accept_gl_confirm(button_text: 'Delete Comment') do
|
||||
first('button.more-actions-toggle').click
|
||||
first('.js-note-delete').click
|
||||
end
|
||||
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a new line' do
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an old line' do
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unchanged line' do
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a match line' do
|
||||
it 'does not allow commenting' do
|
||||
match_should_not_allow_commenting(find('.match', match: :first))
|
||||
match_should_not_allow_commenting(find_by_scrolling('.match', match: :first))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unfolded line' do
|
||||
before do
|
||||
page.within('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
|
||||
page.within find_by_scrolling('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do
|
||||
find('.js-unfold', match: :first).click
|
||||
end
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
|
||||
# The first `.js-unfold` unfolds upwards, therefore the first
|
||||
# `.line_holder` will be an unfolded line.
|
||||
let(:line_holder) { first('[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
|
||||
let(:line_holder) { find_by_scrolling('[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
|
||||
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting line_holder
|
||||
|
|
@ -157,7 +157,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
context 'when hovering over a diff discussion' do
|
||||
before do
|
||||
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
|
||||
visit project_merge_request_path(project, merge_request)
|
||||
end
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
|
||||
context 'with a new line' do
|
||||
it 'allows dismissing a comment' do
|
||||
should_allow_dismissing_a_comment(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
should_allow_dismissing_a_comment(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -182,13 +182,13 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
describe 'with multiple note forms' do
|
||||
before do
|
||||
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
|
||||
click_diff_line(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
click_diff_line(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
|
||||
click_diff_line(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
click_diff_line(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
|
||||
end
|
||||
|
||||
describe 'posting a note' do
|
||||
it 'adds as discussion' do
|
||||
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
|
||||
should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
|
||||
expect(page).to have_css('.notes_holder .note.note-discussion', count: 1)
|
||||
expect(page).to have_field('Reply…')
|
||||
end
|
||||
|
|
@ -203,25 +203,25 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
|
||||
context 'with a new line' do
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an old line' do
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unchanged line' do
|
||||
it 'allows commenting' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
|
||||
should_allow_commenting(find_by_scrolling('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a match line' do
|
||||
it 'does not allow commenting' do
|
||||
match_should_not_allow_commenting(find('.match', match: :first))
|
||||
match_should_not_allow_commenting(find_by_scrolling('.match', match: :first))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ include Spec::Support::Helpers::ModalHelpers # rubocop:disable Style/MixinUsage
|
|||
RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
||||
include NoteInteractionHelpers
|
||||
include Spec::Support::Helpers::ModalHelpers
|
||||
include MergeRequestDiffHelpers
|
||||
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:user) { project.creator }
|
||||
|
|
@ -135,6 +136,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
end
|
||||
|
||||
it 'adds avatar when commenting' do
|
||||
find_by_scrolling('[data-discussion-id]', match: :first)
|
||||
find_field('Reply…', match: :first).click
|
||||
|
||||
page.within '.js-discussion-note-form' do
|
||||
|
|
@ -154,6 +156,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
|
||||
it 'adds multiple comments' do
|
||||
3.times do
|
||||
find_by_scrolling('[data-discussion-id]', match: :first)
|
||||
find_field('Reply…', match: :first).click
|
||||
|
||||
page.within '.js-discussion-note-form' do
|
||||
|
|
@ -192,7 +195,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
end
|
||||
|
||||
def find_line(line_code)
|
||||
line = find("[id='#{line_code}']")
|
||||
line = find_by_scrolling("[id='#{line_code}']")
|
||||
line = line.find(:xpath, 'preceding-sibling::*[1][self::td]/preceding-sibling::*[1][self::td]') if line.tag_name == 'td'
|
||||
line
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Merge request > User sees deployment widget', :js do
|
||||
include Spec::Support::Helpers::ModalHelpers
|
||||
|
||||
describe 'when merge request has associated environments' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
|
@ -118,7 +120,9 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
|
|||
end
|
||||
|
||||
it 'does start build when stop button clicked' do
|
||||
accept_confirm { find('.js-stop-env').click }
|
||||
accept_gl_confirm(button_text: 'Stop environment') do
|
||||
find('.js-stop-env').click
|
||||
end
|
||||
|
||||
expect(page).to have_content('close_app')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe 'Merge request > User sees diff', :js do
|
||||
include ProjectForksHelper
|
||||
include RepoHelpers
|
||||
include MergeRequestDiffHelpers
|
||||
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||
|
|
@ -58,12 +59,12 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
|
||||
|
||||
context 'as author' do
|
||||
it 'shows direct edit link', :sidekiq_might_not_need_inline do
|
||||
it 'contains direct edit link', :sidekiq_might_not_need_inline do
|
||||
sign_in(author_user)
|
||||
visit diffs_project_merge_request_path(project, merge_request)
|
||||
|
||||
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
|
||||
expect(page).to have_selector("[id=\"#{changelog_id}\"] .js-edit-blob", visible: false)
|
||||
expect(page).to have_selector(".js-edit-blob", visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -72,6 +73,8 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
sign_in(user)
|
||||
visit diffs_project_merge_request_path(project, merge_request)
|
||||
|
||||
find_by_scrolling("[id=\"#{changelog_id}\"]")
|
||||
|
||||
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
|
||||
find("[id=\"#{changelog_id}\"] .js-diff-more-actions").click
|
||||
find("[id=\"#{changelog_id}\"] .js-edit-blob").click
|
||||
|
|
@ -107,6 +110,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
CONTENT
|
||||
|
||||
file_name = 'xss_file.rs'
|
||||
file_hash = Digest::SHA1.hexdigest(file_name)
|
||||
|
||||
create_file('master', file_name, file_content)
|
||||
merge_request = create(:merge_request, source_project: project)
|
||||
|
|
@ -116,6 +120,8 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
|
||||
visit diffs_project_merge_request_path(project, merge_request)
|
||||
|
||||
find_by_scrolling("[id='#{file_hash}']")
|
||||
|
||||
expect(page).to have_text("function foo<input> {")
|
||||
expect(page).to have_css(".line[lang='rust'] .k")
|
||||
end
|
||||
|
|
@ -136,7 +142,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
end
|
||||
|
||||
context 'when file is an image', :js do
|
||||
let(:file_name) { 'files/lfs/image.png' }
|
||||
let(:file_name) { 'a/image.png' }
|
||||
|
||||
it 'shows an error message' do
|
||||
expect(page).not_to have_content('could not be displayed because it is stored in LFS')
|
||||
|
|
@ -144,7 +150,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
end
|
||||
|
||||
context 'when file is not an image' do
|
||||
let(:file_name) { 'files/lfs/ruby.rb' }
|
||||
let(:file_name) { 'a/ruby.rb' }
|
||||
|
||||
it 'shows an error message' do
|
||||
expect(page).to have_content('This source diff could not be displayed because it is stored in LFS')
|
||||
|
|
@ -153,7 +159,14 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
end
|
||||
|
||||
context 'when LFS is not enabled' do
|
||||
let(:file_name) { 'a/lfs_object.iso' }
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.lfs).to receive(:disabled).and_return(true)
|
||||
project.update_attribute(:lfs_enabled, false)
|
||||
|
||||
create_file('master', file_name, project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data)
|
||||
|
||||
visit diffs_project_merge_request_path(project, merge_request)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Merge request > User sees versions', :js do
|
||||
include MergeRequestDiffHelpers
|
||||
|
||||
let(:merge_request) do
|
||||
create(:merge_request).tap do |mr|
|
||||
mr.merge_request_diff.destroy!
|
||||
|
|
@ -27,8 +29,12 @@ RSpec.describe 'Merge request > User sees versions', :js do
|
|||
diff_file_selector = ".diff-file[id='#{file_id}']"
|
||||
line_code = "#{file_id}_#{line_code}"
|
||||
|
||||
page.within(diff_file_selector) do
|
||||
first("[id='#{line_code}']").hover
|
||||
page.within find_by_scrolling(diff_file_selector) do
|
||||
line_code_element = first("[id='#{line_code}']")
|
||||
# scrolling to element's bottom is required in order for .hover action to work
|
||||
# otherwise, the element could be hidden underneath a sticky header
|
||||
scroll_to_elements_bottom(line_code_element)
|
||||
line_code_element.hover
|
||||
first("[id='#{line_code}'] [role='button']").click
|
||||
|
||||
page.within("form[data-line-code='#{line_code}']") do
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
context 'single suggestion note' do
|
||||
it 'hides suggestion popover' do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
|
||||
expect(page).to have_selector('.diff-suggest-popover')
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
end
|
||||
|
||||
it 'suggestion is presented' do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
|
||||
|
|
@ -74,7 +74,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
end
|
||||
|
||||
it 'allows suggestions in replies' do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
|
||||
|
|
@ -91,7 +91,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
end
|
||||
|
||||
it 'suggestion is appliable' do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
|
||||
|
|
@ -273,7 +273,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
context 'multiple suggestions in a single note' do
|
||||
it 'suggestions are presented', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/258989' do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion:-2\n# or that\n# heh\n```")
|
||||
|
|
@ -316,7 +316,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
context 'multi-line suggestions' do
|
||||
before do
|
||||
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe MergeRequestsFinder::Params do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { described_class.new(params, user, MergeRequest) }
|
||||
|
||||
describe 'attention' do
|
||||
context 'attention param exists' do
|
||||
let(:params) { { attention: user.username } }
|
||||
|
||||
it { expect(subject.attention).to eq(user) }
|
||||
end
|
||||
|
||||
context 'attention param does not exist' do
|
||||
let(:params) { { attention: nil } }
|
||||
|
||||
it { expect(subject.attention).to eq(nil) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -408,6 +408,22 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
end
|
||||
|
||||
context 'attention' do
|
||||
subject { described_class.new(user, params).execute }
|
||||
|
||||
before do
|
||||
reviewer = merge_request1.find_reviewer(user2)
|
||||
reviewer.update!(state: :reviewed)
|
||||
end
|
||||
|
||||
context 'by username' do
|
||||
let(:params) { { attention: user2.username } }
|
||||
let(:expected_mr) { [merge_request2, merge_request3] }
|
||||
|
||||
it { is_expected.to contain_exactly(*expected_mr) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'reviewer filtering' do
|
||||
subject { described_class.new(user, params).execute }
|
||||
|
||||
|
|
|
|||
|
|
@ -11,40 +11,21 @@ RSpec.describe Projects::Members::EffectiveAccessLevelFinder, '#execute' do
|
|||
context 'for a personal project' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
shared_examples_for 'includes access level of the owner of the project' do
|
||||
context 'when personal_project_owner_with_owner_access feature flag is enabled' do
|
||||
it 'includes access level of the owner of the project as Owner' do
|
||||
expect(subject).to(
|
||||
contain_exactly(
|
||||
hash_including(
|
||||
'user_id' => project.namespace.owner.id,
|
||||
'access_level' => Gitlab::Access::OWNER
|
||||
)
|
||||
shared_examples_for 'includes access level of the owner of the project as Maintainer' do
|
||||
it 'includes access level of the owner of the project as Maintainer' do
|
||||
expect(subject).to(
|
||||
contain_exactly(
|
||||
hash_including(
|
||||
'user_id' => project.namespace.owner.id,
|
||||
'access_level' => Gitlab::Access::MAINTAINER
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(personal_project_owner_with_owner_access: false)
|
||||
end
|
||||
|
||||
it 'includes access level of the owner of the project as Maintainer' do
|
||||
expect(subject).to(
|
||||
contain_exactly(
|
||||
hash_including(
|
||||
'user_id' => project.namespace.owner.id,
|
||||
'access_level' => Gitlab::Access::MAINTAINER
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project owner is a member of the project' do
|
||||
it_behaves_like 'includes access level of the owner of the project'
|
||||
it_behaves_like 'includes access level of the owner of the project as Maintainer'
|
||||
end
|
||||
|
||||
context 'when the project owner is not explicitly a member of the project' do
|
||||
|
|
@ -52,7 +33,7 @@ RSpec.describe Projects::Members::EffectiveAccessLevelFinder, '#execute' do
|
|||
project.members.find_by(user_id: project.namespace.owner.id).destroy!
|
||||
end
|
||||
|
||||
it_behaves_like 'includes access level of the owner of the project'
|
||||
it_behaves_like 'includes access level of the owner of the project as Maintainer'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -103,32 +84,17 @@ RSpec.describe Projects::Members::EffectiveAccessLevelFinder, '#execute' do
|
|||
|
||||
context 'for a project within a group' do
|
||||
context 'project in a root group' do
|
||||
context 'includes access levels of users who are direct members of the parent group' do
|
||||
it 'when access level is developer' do
|
||||
group_member = create(:group_member, :developer, source: group)
|
||||
it 'includes access levels of users who are direct members of the parent group' do
|
||||
group_member = create(:group_member, :developer, source: group)
|
||||
|
||||
expect(subject).to(
|
||||
include(
|
||||
hash_including(
|
||||
'user_id' => group_member.user.id,
|
||||
'access_level' => Gitlab::Access::DEVELOPER
|
||||
)
|
||||
expect(subject).to(
|
||||
include(
|
||||
hash_including(
|
||||
'user_id' => group_member.user.id,
|
||||
'access_level' => Gitlab::Access::DEVELOPER
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
it 'when access level is owner' do
|
||||
group_member = create(:group_member, :owner, source: group)
|
||||
|
||||
expect(subject).to(
|
||||
include(
|
||||
hash_including(
|
||||
'user_id' => group_member.user.id,
|
||||
'access_level' => Gitlab::Access::OWNER
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +1,40 @@
|
|||
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vuex from 'vuex';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import BoardsSelector from '~/boards/components/boards_selector.vue';
|
||||
import { BoardType } from '~/boards/constants';
|
||||
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
|
||||
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
|
||||
import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
|
||||
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
|
||||
import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
|
||||
import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql';
|
||||
import defaultStore from '~/boards/stores';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mockGroupBoardResponse, mockProjectBoardResponse } from '../mock_data';
|
||||
import {
|
||||
mockGroupBoardResponse,
|
||||
mockProjectBoardResponse,
|
||||
mockGroupAllBoardsResponse,
|
||||
mockProjectAllBoardsResponse,
|
||||
mockGroupRecentBoardsResponse,
|
||||
mockProjectRecentBoardsResponse,
|
||||
mockSmallProjectAllBoardsResponse,
|
||||
mockEmptyProjectRecentBoardsResponse,
|
||||
boards,
|
||||
recentIssueBoards,
|
||||
} from '../mock_data';
|
||||
|
||||
const throttleDuration = 1;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
function boardGenerator(n) {
|
||||
return new Array(n).fill().map((board, index) => {
|
||||
const id = `${index}`;
|
||||
const name = `board${id}`;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
describe('BoardsSelector', () => {
|
||||
let wrapper;
|
||||
let allBoardsResponse;
|
||||
let recentBoardsResponse;
|
||||
let mock;
|
||||
let fakeApollo;
|
||||
let store;
|
||||
const boards = boardGenerator(20);
|
||||
const recentBoards = boardGenerator(5);
|
||||
|
||||
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
|
||||
store = new Vuex.Store({
|
||||
|
|
@ -63,17 +60,43 @@ describe('BoardsSelector', () => {
|
|||
};
|
||||
|
||||
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
|
||||
const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader);
|
||||
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
|
||||
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
|
||||
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
|
||||
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
|
||||
|
||||
const createComponent = () => {
|
||||
const projectBoardsQueryHandlerSuccess = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockProjectAllBoardsResponse);
|
||||
const groupBoardsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupAllBoardsResponse);
|
||||
|
||||
const projectRecentBoardsQueryHandlerSuccess = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockProjectRecentBoardsResponse);
|
||||
const groupRecentBoardsQueryHandlerSuccess = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockGroupRecentBoardsResponse);
|
||||
|
||||
const smallBoardsQueryHandlerSuccess = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockSmallProjectAllBoardsResponse);
|
||||
const emptyRecentBoardsQueryHandlerSuccess = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockEmptyProjectRecentBoardsResponse);
|
||||
|
||||
const createComponent = ({
|
||||
projectBoardsQueryHandler = projectBoardsQueryHandlerSuccess,
|
||||
projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([
|
||||
[projectBoardQuery, projectBoardQueryHandlerSuccess],
|
||||
[groupBoardQuery, groupBoardQueryHandlerSuccess],
|
||||
[projectBoardsQuery, projectBoardsQueryHandler],
|
||||
[groupBoardsQuery, groupBoardsQueryHandlerSuccess],
|
||||
[projectRecentBoardsQuery, projectRecentBoardsQueryHandler],
|
||||
[groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess],
|
||||
]);
|
||||
|
||||
wrapper = mount(BoardsSelector, {
|
||||
|
|
@ -91,67 +114,34 @@ describe('BoardsSelector', () => {
|
|||
attachTo: document.body,
|
||||
provide: {
|
||||
fullPath: '',
|
||||
recentBoardsEndpoint: `${TEST_HOST}/recent`,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
[options.loadingKey]: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
mock.restore();
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
describe('fetching all boards', () => {
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
allBoardsResponse = Promise.resolve({
|
||||
data: {
|
||||
group: {
|
||||
boards: {
|
||||
edges: boards.map((board) => ({ node: board })),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
recentBoardsResponse = Promise.resolve({
|
||||
data: recentBoards,
|
||||
});
|
||||
|
||||
createStore();
|
||||
createStore({ isProjectBoard: true });
|
||||
createComponent();
|
||||
|
||||
mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
|
||||
});
|
||||
|
||||
describe('loading', () => {
|
||||
beforeEach(async () => {
|
||||
// Wait for current board to be loaded
|
||||
await nextTick();
|
||||
|
||||
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
|
||||
findDropdown().vm.$emit('show');
|
||||
});
|
||||
|
||||
// we are testing loading state, so don't resolve responses until after the tests
|
||||
afterEach(async () => {
|
||||
await Promise.all([allBoardsResponse, recentBoardsResponse]);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('shows loading spinner', () => {
|
||||
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
|
||||
findDropdown().vm.$emit('show');
|
||||
|
||||
expect(getLoadingIcon().exists()).toBe(true);
|
||||
expect(getDropdownHeaders()).toHaveLength(0);
|
||||
expect(getDropdownItems()).toHaveLength(0);
|
||||
expect(getLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -163,16 +153,13 @@ describe('BoardsSelector', () => {
|
|||
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
|
||||
findDropdown().vm.$emit('show');
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
await wrapper.setData({
|
||||
loadingBoards: false,
|
||||
loadingRecentBoards: false,
|
||||
});
|
||||
await Promise.all([allBoardsResponse, recentBoardsResponse]);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('fetches all issue boards', () => {
|
||||
expect(projectBoardsQueryHandlerSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides loading spinner', async () => {
|
||||
await nextTick();
|
||||
expect(getLoadingIcon().exists()).toBe(false);
|
||||
|
|
@ -180,22 +167,17 @@ describe('BoardsSelector', () => {
|
|||
|
||||
describe('filtering', () => {
|
||||
beforeEach(async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
boards,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('shows all boards without filtering', () => {
|
||||
expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
|
||||
expect(getDropdownItems()).toHaveLength(boards.length + recentIssueBoards.length);
|
||||
});
|
||||
|
||||
it('shows only matching boards when filtering', async () => {
|
||||
const filterTerm = 'board1';
|
||||
const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
|
||||
const expectedCount = boards.filter((board) => board.node.name.includes(filterTerm))
|
||||
.length;
|
||||
|
||||
fillSearchBox(filterTerm);
|
||||
|
||||
|
|
@ -214,32 +196,21 @@ describe('BoardsSelector', () => {
|
|||
|
||||
describe('recent boards section', () => {
|
||||
it('shows only when boards are greater than 10', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
boards,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
expect(projectRecentBoardsQueryHandlerSuccess).toHaveBeenCalled();
|
||||
expect(getDropdownHeaders()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does not show when boards are less than 10', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
boards: boards.slice(0, 5),
|
||||
});
|
||||
createComponent({ projectBoardsQueryHandler: smallBoardsQueryHandlerSuccess });
|
||||
|
||||
await nextTick();
|
||||
expect(getDropdownHeaders()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not show when recentBoards api returns empty array', async () => {
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
recentBoards: [],
|
||||
it('does not show when recentIssueBoards api returns empty array', async () => {
|
||||
createComponent({
|
||||
projectRecentBoardsQueryHandler: emptyRecentBoardsQueryHandlerSuccess,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
|
@ -256,15 +227,39 @@ describe('BoardsSelector', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetching all boards', () => {
|
||||
it.each`
|
||||
boardType | queryHandler | notCalledHandler
|
||||
${BoardType.group} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
|
||||
${BoardType.project} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
|
||||
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
|
||||
createStore({
|
||||
isProjectBoard: boardType === BoardType.project,
|
||||
isGroupBoard: boardType === BoardType.group,
|
||||
});
|
||||
createComponent();
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
|
||||
findDropdown().vm.$emit('show');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(queryHandler).toHaveBeenCalled();
|
||||
expect(notCalledHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching current board', () => {
|
||||
it.each`
|
||||
boardType | queryHandler | notCalledHandler
|
||||
${'group'} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
|
||||
${'project'} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
|
||||
boardType | queryHandler | notCalledHandler
|
||||
${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
|
||||
${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
|
||||
`('fetches $boardType board', async ({ boardType, queryHandler, notCalledHandler }) => {
|
||||
createStore({
|
||||
isProjectBoard: boardType === 'project',
|
||||
isGroupBoard: boardType === 'group',
|
||||
isProjectBoard: boardType === BoardType.project,
|
||||
isGroupBoard: boardType === BoardType.group,
|
||||
});
|
||||
createComponent();
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,85 @@ export const listObj = {
|
|||
},
|
||||
};
|
||||
|
||||
function boardGenerator(n) {
|
||||
return new Array(n).fill().map((board, index) => {
|
||||
const id = `${index}`;
|
||||
const name = `board${id}`;
|
||||
|
||||
return {
|
||||
node: {
|
||||
id,
|
||||
name,
|
||||
weight: 0,
|
||||
__typename: 'Board',
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const boards = boardGenerator(20);
|
||||
export const recentIssueBoards = boardGenerator(5);
|
||||
|
||||
export const mockSmallProjectAllBoardsResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/114',
|
||||
boards: { edges: boardGenerator(3) },
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockEmptyProjectRecentBoardsResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/114',
|
||||
recentIssueBoards: { edges: [] },
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGroupAllBoardsResponse = {
|
||||
data: {
|
||||
group: {
|
||||
id: 'gid://gitlab/Group/114',
|
||||
boards: { edges: boards },
|
||||
__typename: 'Group',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockProjectAllBoardsResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
boards: { edges: boards },
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGroupRecentBoardsResponse = {
|
||||
data: {
|
||||
group: {
|
||||
id: 'gid://gitlab/Group/114',
|
||||
recentIssueBoards: { edges: recentIssueBoards },
|
||||
__typename: 'Group',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockProjectRecentBoardsResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
recentIssueBoards: { edges: recentIssueBoards },
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGroupBoardResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ describe('diffs/components/app', () => {
|
|||
},
|
||||
provide,
|
||||
store,
|
||||
stubs: {
|
||||
DynamicScroller: {
|
||||
template: `<div><slot :item="$store.state.diffs.diffFiles[0]"></slot></div>`,
|
||||
},
|
||||
DynamicScrollerItem: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ describe('DiffsStoreActions', () => {
|
|||
testAction(
|
||||
fetchDiffFilesBatch,
|
||||
{},
|
||||
{ endpointBatch, diffViewType: 'inline' },
|
||||
{ endpointBatch, diffViewType: 'inline', diffFiles: [] },
|
||||
[
|
||||
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
|
||||
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ describe('Discussion navigation mixin', () => {
|
|||
it('scrolls to element', () => {
|
||||
expect(utils.scrollToElement).toHaveBeenCalledWith(
|
||||
findDiscussion('div.discussion', expected),
|
||||
{ behavior: 'smooth' },
|
||||
{ behavior: 'auto' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -171,7 +171,7 @@ describe('Discussion navigation mixin', () => {
|
|||
|
||||
expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
|
||||
findDiscussion('ul.notes', expected),
|
||||
{ behavior: 'smooth' },
|
||||
{ behavior: 'auto' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -212,21 +212,15 @@ describe('Discussion navigation mixin', () => {
|
|||
it('scrolls to discussion', () => {
|
||||
expect(utils.scrollToElement).toHaveBeenCalledWith(
|
||||
findDiscussion('div.discussion', expected),
|
||||
{ behavior: 'smooth' },
|
||||
{ behavior: 'auto' },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
diffsVirtualScrolling
|
||||
${false}
|
||||
${true}
|
||||
`('virtual scrolling feature is $diffsVirtualScrolling', ({ diffsVirtualScrolling }) => {
|
||||
describe('virtual scrolling feature', () => {
|
||||
beforeEach(() => {
|
||||
window.gon = { features: { diffsVirtualScrolling } };
|
||||
|
||||
jest.spyOn(store, 'dispatch');
|
||||
|
||||
store.state.notes.currentDiscussionId = 'a';
|
||||
|
|
@ -238,22 +232,22 @@ describe('Discussion navigation mixin', () => {
|
|||
window.location.hash = '';
|
||||
});
|
||||
|
||||
it('resets location hash if diffsVirtualScrolling flag is true', async () => {
|
||||
it('resets location hash', async () => {
|
||||
wrapper.vm.jumpToNextDiscussion();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(window.location.hash).toBe(diffsVirtualScrolling ? '' : '#test');
|
||||
expect(window.location.hash).toBe('');
|
||||
});
|
||||
|
||||
it.each`
|
||||
tabValue | hashValue
|
||||
${'diffs'} | ${false}
|
||||
${'show'} | ${!diffsVirtualScrolling}
|
||||
${'other'} | ${!diffsVirtualScrolling}
|
||||
tabValue
|
||||
${'diffs'}
|
||||
${'show'}
|
||||
${'other'}
|
||||
`(
|
||||
'calls scrollToFile with setHash as $hashValue when the tab is $tabValue',
|
||||
async ({ hashValue, tabValue }) => {
|
||||
async ({ tabValue }) => {
|
||||
window.mrTabs.currentAction = tabValue;
|
||||
|
||||
wrapper.vm.jumpToNextDiscussion();
|
||||
|
|
@ -262,7 +256,6 @@ describe('Discussion navigation mixin', () => {
|
|||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
|
||||
path: 'test.js',
|
||||
setHash: hashValue,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
CREATED,
|
||||
|
|
@ -20,6 +22,11 @@ import {
|
|||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
|
||||
return {
|
||||
confirmAction: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('DeploymentAction component', () => {
|
||||
let wrapper;
|
||||
|
|
@ -51,6 +58,7 @@ describe('DeploymentAction component', () => {
|
|||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
confirmAction.mockReset();
|
||||
});
|
||||
|
||||
describe('actions do not appear when conditions are unmet', () => {
|
||||
|
|
@ -95,16 +103,6 @@ describe('DeploymentAction component', () => {
|
|||
'$configConst action',
|
||||
({ configConst, computedDeploymentStatus, displayConditionChanges, finderFn, endpoint }) => {
|
||||
describe(`${configConst} action`, () => {
|
||||
const confirmAction = () => {
|
||||
jest.spyOn(window, 'confirm').mockReturnValueOnce(true);
|
||||
finderFn().trigger('click');
|
||||
};
|
||||
|
||||
const rejectAction = () => {
|
||||
jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
|
||||
finderFn().trigger('click');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
propsData: {
|
||||
|
|
@ -125,13 +123,18 @@ describe('DeploymentAction component', () => {
|
|||
describe('should show a confirm dialog but not call executeInlineAction when declined', () => {
|
||||
beforeEach(() => {
|
||||
executeActionSpy.mockResolvedValueOnce();
|
||||
rejectAction();
|
||||
confirmAction.mockResolvedValueOnce(false);
|
||||
finderFn().trigger('click');
|
||||
});
|
||||
|
||||
it('should show the confirm dialog', () => {
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
expect(window.confirm).toHaveBeenCalledWith(
|
||||
expect(confirmAction).toHaveBeenCalled();
|
||||
expect(confirmAction).toHaveBeenCalledWith(
|
||||
actionButtonMocks[configConst].confirmMessage,
|
||||
{
|
||||
primaryBtnVariant: actionButtonMocks[configConst].buttonVariant,
|
||||
primaryBtnText: actionButtonMocks[configConst].buttonText,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -143,13 +146,18 @@ describe('DeploymentAction component', () => {
|
|||
describe('should show a confirm dialog and call executeInlineAction when accepted', () => {
|
||||
beforeEach(() => {
|
||||
executeActionSpy.mockResolvedValueOnce();
|
||||
confirmAction();
|
||||
confirmAction.mockResolvedValueOnce(true);
|
||||
finderFn().trigger('click');
|
||||
});
|
||||
|
||||
it('should show the confirm dialog', () => {
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
expect(window.confirm).toHaveBeenCalledWith(
|
||||
expect(confirmAction).toHaveBeenCalled();
|
||||
expect(confirmAction).toHaveBeenCalledWith(
|
||||
actionButtonMocks[configConst].confirmMessage,
|
||||
{
|
||||
primaryBtnVariant: actionButtonMocks[configConst].buttonVariant,
|
||||
primaryBtnText: actionButtonMocks[configConst].buttonText,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -164,11 +172,15 @@ describe('DeploymentAction component', () => {
|
|||
|
||||
describe('response includes redirect_url', () => {
|
||||
const url = '/root/example';
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
executeActionSpy.mockResolvedValueOnce({
|
||||
data: { redirect_url: url },
|
||||
});
|
||||
confirmAction();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
confirmAction.mockResolvedValueOnce(true);
|
||||
finderFn().trigger('click');
|
||||
});
|
||||
|
||||
it('calls visit url with the redirect_url', () => {
|
||||
|
|
@ -178,9 +190,13 @@ describe('DeploymentAction component', () => {
|
|||
});
|
||||
|
||||
describe('it should call the executeAction method ', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
|
||||
confirmAction();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
confirmAction.mockResolvedValueOnce(true);
|
||||
finderFn().trigger('click');
|
||||
});
|
||||
|
||||
it('calls with the expected arguments', () => {
|
||||
|
|
@ -193,9 +209,13 @@ describe('DeploymentAction component', () => {
|
|||
});
|
||||
|
||||
describe('when executeInlineAction errors', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
executeActionSpy.mockRejectedValueOnce();
|
||||
confirmAction();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
confirmAction.mockResolvedValueOnce(true);
|
||||
finderFn().trigger('click');
|
||||
});
|
||||
|
||||
it('should call createFlash with error message', () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const actionButtonMocks = {
|
|||
[STOPPING]: {
|
||||
actionName: STOPPING,
|
||||
buttonText: 'Stop environment',
|
||||
buttonVariant: 'danger',
|
||||
busyText: 'This environment is being deployed',
|
||||
confirmMessage: 'Are you sure you want to stop this environment?',
|
||||
errorMessage: 'Something went wrong while stopping this environment. Please try again.',
|
||||
|
|
@ -16,6 +17,7 @@ const actionButtonMocks = {
|
|||
[DEPLOYING]: {
|
||||
actionName: DEPLOYING,
|
||||
buttonText: 'Deploy',
|
||||
buttonVariant: 'confirm',
|
||||
busyText: 'This environment is being deployed',
|
||||
confirmMessage: 'Are you sure you want to deploy this environment?',
|
||||
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
|
||||
|
|
@ -23,6 +25,7 @@ const actionButtonMocks = {
|
|||
[REDEPLOYING]: {
|
||||
actionName: REDEPLOYING,
|
||||
buttonText: 'Re-deploy',
|
||||
buttonVariant: 'confirm',
|
||||
busyText: 'This environment is being re-deployed',
|
||||
confirmMessage: 'Are you sure you want to re-deploy this environment?',
|
||||
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
|
||||
|
|
|
|||
|
|
@ -34,28 +34,12 @@ RSpec.describe Gitlab::ProjectAuthorizations do
|
|||
.to include(owned_project.id, other_project.id, group_project.id)
|
||||
end
|
||||
|
||||
context 'when personal_project_owner_with_owner_access feature flag is enabled' do
|
||||
it 'includes the correct access levels' do
|
||||
mapping = map_access_levels(authorizations)
|
||||
it 'includes the correct access levels' do
|
||||
mapping = map_access_levels(authorizations)
|
||||
|
||||
expect(mapping[owned_project.id]).to eq(Gitlab::Access::OWNER)
|
||||
expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
|
||||
expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(personal_project_owner_with_owner_access: false)
|
||||
end
|
||||
|
||||
it 'includes the correct access levels' do
|
||||
mapping = map_access_levels(authorizations)
|
||||
|
||||
expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
|
||||
expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
|
||||
expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
|
||||
expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
|
||||
expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1179,6 +1179,16 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#external_import_status' do
|
||||
subject { repository.external_import_status }
|
||||
|
||||
it 'returns the response from the client' do
|
||||
expect(repository.gitlab_api_client).to receive(:import_status).with(repository.path).and_return('test')
|
||||
|
||||
expect(subject).to eq('test')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_stale_migration' do
|
||||
let_it_be(:repository) { create(:container_repository) }
|
||||
let_it_be(:stale_pre_importing_old_timestamp) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 10.minutes.ago) }
|
||||
|
|
|
|||
|
|
@ -144,6 +144,20 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.attention' do
|
||||
let_it_be(:merge_request5) { create(:merge_request, :unique_branches, assignees: [user2])}
|
||||
let_it_be(:merge_request6) { create(:merge_request, :unique_branches, assignees: [user2])}
|
||||
|
||||
before do
|
||||
assignee = merge_request6.find_assignee(user2)
|
||||
assignee.update!(state: :reviewed)
|
||||
end
|
||||
|
||||
it 'returns MRs that have any attention requests' do
|
||||
expect(described_class.attention(user2)).to eq([merge_request2, merge_request5])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.drafts' do
|
||||
it 'returns MRs where draft == true' do
|
||||
expect(described_class.drafts).to eq([merge_request4])
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ RSpec.describe ProjectTeam do
|
|||
let_it_be(:maintainer) { create(:user) }
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:project) { create(:project, group: create(:group)) }
|
||||
let_it_be(:project) { create(:project, namespace: maintainer.namespace) }
|
||||
let_it_be(:access_levels) { [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER] }
|
||||
|
||||
subject(:members_with_access_levels) { project.team.members_with_access_levels(access_levels) }
|
||||
|
|
|
|||
|
|
@ -3717,7 +3717,7 @@ RSpec.describe User do
|
|||
|
||||
context 'with min_access_level' do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:project) { create(:project, :private, group: create(:group)) }
|
||||
let!(:project) { create(:project, :private, namespace: user.namespace) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
|
|
|
|||
|
|
@ -675,30 +675,13 @@ RSpec.describe API::Members do
|
|||
end
|
||||
|
||||
context 'adding owner to project' do
|
||||
context 'when personal_project_owner_with_owner_access feature flag is enabled' do
|
||||
it 'returns created status' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/members", maintainer),
|
||||
params: { user_id: stranger.id, access_level: Member::OWNER }
|
||||
it 'returns 403' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/members", maintainer),
|
||||
params: { user_id: stranger.id, access_level: Member::OWNER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end.to change { project.members.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(personal_project_owner_with_owner_access: false)
|
||||
end
|
||||
|
||||
it 'returns created status' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/members", maintainer),
|
||||
params: { user_id: stranger.id, access_level: Member::OWNER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end.not_to change { project.members.count }
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end.not_to change { project.members.count }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
it 'is called' do
|
||||
ProjectAuthorization.delete_all
|
||||
|
||||
expect(callback).to receive(:call).with(project.id, Gitlab::Access::OWNER).once
|
||||
expect(callback).to receive(:call).with(project.id, Gitlab::Access::MAINTAINER).once
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
|
@ -60,20 +60,20 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
|
||||
to_be_removed = [project2.id]
|
||||
to_be_added = [
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER }
|
||||
]
|
||||
|
||||
expect(service.execute).to eq([to_be_removed, to_be_added])
|
||||
end
|
||||
|
||||
it 'finds duplicate entries that has to be removed' do
|
||||
[Gitlab::Access::OWNER, Gitlab::Access::REPORTER].each do |access_level|
|
||||
[Gitlab::Access::MAINTAINER, Gitlab::Access::REPORTER].each do |access_level|
|
||||
user.project_authorizations.create!(project: project, access_level: access_level)
|
||||
end
|
||||
|
||||
to_be_removed = [project.id]
|
||||
to_be_added = [
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER }
|
||||
]
|
||||
|
||||
expect(service.execute).to eq([to_be_removed, to_be_added])
|
||||
|
|
@ -85,7 +85,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
|
||||
to_be_removed = [project.id]
|
||||
to_be_added = [
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER }
|
||||
]
|
||||
|
||||
expect(service.execute).to eq([to_be_removed, to_be_added])
|
||||
|
|
@ -143,16 +143,16 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
end
|
||||
|
||||
it 'sets the keys to the project IDs' do
|
||||
expect(hash.keys).to match_array([project.id])
|
||||
expect(hash.keys).to eq([project.id])
|
||||
end
|
||||
|
||||
it 'sets the values to the access levels' do
|
||||
expect(hash.values).to match_array([Gitlab::Access::OWNER])
|
||||
expect(hash.values).to eq([Gitlab::Access::MAINTAINER])
|
||||
end
|
||||
|
||||
context 'personal projects' do
|
||||
it 'includes the project with the right access level' do
|
||||
expect(hash[project.id]).to eq(Gitlab::Access::OWNER)
|
||||
expect(hash[project.id]).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -242,7 +242,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
value = hash.values[0]
|
||||
|
||||
expect(value.project_id).to eq(project.id)
|
||||
expect(value.access_level).to eq(Gitlab::Access::OWNER)
|
||||
expect(value.access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
end
|
||||
|
||||
it 'includes the access level for every row' do
|
||||
expect(row.access_level).to eq(Gitlab::Access::OWNER)
|
||||
expect(row.access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -283,7 +283,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
rows = service.fresh_authorizations.to_a
|
||||
|
||||
expect(rows.length).to eq(1)
|
||||
expect(rows.first.access_level).to eq(Gitlab::Access::OWNER)
|
||||
expect(rows.first.access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
|
||||
context 'every returned row' do
|
||||
|
|
@ -294,7 +294,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
|
|||
end
|
||||
|
||||
it 'includes the access level' do
|
||||
expect(row.access_level).to eq(Gitlab::Access::OWNER)
|
||||
expect(row.access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ RSpec.describe Members::Projects::CreatorService do
|
|||
end
|
||||
|
||||
describe '.access_levels' do
|
||||
it 'returns Gitlab::Access.sym_options_with_owner' do
|
||||
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
|
||||
it 'returns Gitlab::Access.sym_options' do
|
||||
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3312,7 +3312,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
describe "##{sym}" do
|
||||
subject(:notify!) { notification.send(sym, domain) }
|
||||
|
||||
it 'emails current watching maintainers and owners' do
|
||||
it 'emails current watching maintainers' do
|
||||
expect(Notify).to receive(:"#{sym}_email").at_least(:once).and_call_original
|
||||
|
||||
notify!
|
||||
|
|
@ -3410,7 +3410,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
reset_delivered_emails!
|
||||
end
|
||||
|
||||
it 'emails current watching maintainers and owners' do
|
||||
it 'emails current watching maintainers' do
|
||||
notification.remote_mirror_update_failed(remote_mirror)
|
||||
|
||||
should_only_email(u_maintainer1, u_maintainer2, u_owner)
|
||||
|
|
|
|||
|
|
@ -116,34 +116,14 @@ RSpec.describe Projects::CreateService, '#execute' do
|
|||
end
|
||||
|
||||
context 'user namespace' do
|
||||
context 'when personal_project_owner_with_owner_access feature flag is enabled' do
|
||||
it 'creates a project in user namespace' do
|
||||
project = create_project(user, opts)
|
||||
it 'creates a project in user namespace' do
|
||||
project = create_project(user, opts)
|
||||
|
||||
expect(project).to be_valid
|
||||
expect(project.first_owner).to eq(user)
|
||||
expect(project.team.maintainers).not_to include(user)
|
||||
expect(project.team.owners).to contain_exactly(user)
|
||||
expect(project.namespace).to eq(user.namespace)
|
||||
expect(project.project_namespace).to be_in_sync_with_project(project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(personal_project_owner_with_owner_access: false)
|
||||
end
|
||||
|
||||
it 'creates a project in user namespace' do
|
||||
project = create_project(user, opts)
|
||||
|
||||
expect(project).to be_valid
|
||||
expect(project.first_owner).to eq(user)
|
||||
expect(project.team.maintainers).to contain_exactly(user)
|
||||
expect(project.team.owners).to contain_exactly(user)
|
||||
expect(project.namespace).to eq(user.namespace)
|
||||
expect(project.project_namespace).to be_in_sync_with_project(project)
|
||||
end
|
||||
expect(project).to be_valid
|
||||
expect(project.first_owner).to eq(user)
|
||||
expect(project.team.maintainers).to include(user)
|
||||
expect(project.namespace).to eq(user.namespace)
|
||||
expect(project.project_namespace).to be_in_sync_with_project(project)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -182,7 +162,7 @@ RSpec.describe Projects::CreateService, '#execute' do
|
|||
expect(project).to be_persisted
|
||||
expect(project.owner).to eq(user)
|
||||
expect(project.first_owner).to eq(user)
|
||||
expect(project.team.owners).to contain_exactly(user)
|
||||
expect(project.team.maintainers).to contain_exactly(user)
|
||||
expect(project.namespace).to eq(user.namespace)
|
||||
expect(project.project_namespace).to be_in_sync_with_project(project)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
|
|||
it 'is called' do
|
||||
ProjectAuthorization.delete_all
|
||||
|
||||
expect(callback).to receive(:call).with(project.id, Gitlab::Access::OWNER).once
|
||||
expect(callback).to receive(:call).with(project.id, Gitlab::Access::MAINTAINER).once
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
|
@ -73,7 +73,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
|
|||
to_be_removed = [project_authorization.project_id]
|
||||
|
||||
to_be_added = [
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER }
|
||||
]
|
||||
|
||||
expect(service).to receive(:update_authorizations)
|
||||
|
|
@ -83,14 +83,14 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
|
|||
end
|
||||
|
||||
it 'removes duplicate entries' do
|
||||
[Gitlab::Access::OWNER, Gitlab::Access::REPORTER].each do |access_level|
|
||||
[Gitlab::Access::MAINTAINER, Gitlab::Access::REPORTER].each do |access_level|
|
||||
user.project_authorizations.create!(project: project, access_level: access_level)
|
||||
end
|
||||
|
||||
to_be_removed = [project.id]
|
||||
|
||||
to_be_added = [
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER }
|
||||
]
|
||||
expect(service).to(
|
||||
receive(:update_authorizations)
|
||||
|
|
@ -103,7 +103,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
|
|||
project_authorization = ProjectAuthorization.where(
|
||||
project_id: project.id,
|
||||
user_id: user.id,
|
||||
access_level: Gitlab::Access::OWNER)
|
||||
access_level: Gitlab::Access::MAINTAINER)
|
||||
expect(project_authorization).to exist
|
||||
end
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
|
|||
to_be_removed = [project_authorization.project_id]
|
||||
|
||||
to_be_added = [
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
|
||||
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER }
|
||||
]
|
||||
|
||||
expect(service).to receive(:update_authorizations)
|
||||
|
|
|
|||
|
|
@ -294,8 +294,6 @@ RSpec.configure do |config|
|
|||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/33867
|
||||
stub_feature_flags(file_identifier_hash: false)
|
||||
|
||||
stub_feature_flags(diffs_virtual_scrolling: false)
|
||||
|
||||
# The following `vue_issues_list` stub can be removed
|
||||
# once the Vue issues page has feature parity with the current Haml page
|
||||
stub_feature_flags(vue_issues_list: false)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MergeRequestDiffHelpers
|
||||
PageEndReached = Class.new(StandardError)
|
||||
|
||||
def click_diff_line(line_holder, diff_side = nil)
|
||||
line = get_line_components(line_holder, diff_side)
|
||||
scroll_to_elements_bottom(line_holder)
|
||||
line_holder.hover
|
||||
line[:num].find('.js-add-diff-note-button').click
|
||||
end
|
||||
|
|
@ -27,4 +30,55 @@ module MergeRequestDiffHelpers
|
|||
line_holder.find('.diff-line-num', match: :first)
|
||||
{ content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
|
||||
end
|
||||
|
||||
def has_reached_page_end
|
||||
evaluate_script("(window.innerHeight + window.scrollY) >= document.body.offsetHeight")
|
||||
end
|
||||
|
||||
def scroll_to_elements_bottom(element)
|
||||
evaluate_script("(function(el) {
|
||||
window.scrollBy(0, el.getBoundingClientRect().bottom - window.innerHeight);
|
||||
})(arguments[0]);", element.native)
|
||||
end
|
||||
|
||||
# we're not using Capybara's .obscured here because it also checks if the element is clickable
|
||||
def within_viewport?(element)
|
||||
evaluate_script("(function(el) {
|
||||
var rect = el.getBoundingClientRect();
|
||||
return (
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.left <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
})(arguments[0]);", element.native)
|
||||
end
|
||||
|
||||
def find_within_viewport(selector, **options)
|
||||
begin
|
||||
element = find(selector, **options, wait: 2)
|
||||
rescue Capybara::ElementNotFound
|
||||
return
|
||||
end
|
||||
return element if within_viewport?(element)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def find_by_scrolling(selector, **options)
|
||||
element = find_within_viewport(selector, **options)
|
||||
return element if element
|
||||
|
||||
page.execute_script "window.scrollTo(0,0)"
|
||||
until element
|
||||
|
||||
if has_reached_page_end
|
||||
raise PageEndReached, "Failed to find any elements matching a selector '#{selector}' by scrolling. Page end reached."
|
||||
end
|
||||
|
||||
page.execute_script "window.scrollBy(0,window.innerHeight/1.5)"
|
||||
element = find_within_viewport(selector, **options)
|
||||
end
|
||||
element
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module NoteInteractionHelpers
|
||||
include MergeRequestDiffHelpers
|
||||
|
||||
def open_more_actions_dropdown(note)
|
||||
note_element = find("#note_#{note.id}")
|
||||
note_element = find_by_scrolling("#note_#{note.id}")
|
||||
|
||||
note_element.find('.more-actions-toggle').click
|
||||
note_element.find('.more-actions .dropdown-menu li', match: :first)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
RSpec.shared_examples 'comment on merge request file' do
|
||||
it 'adds a comment' do
|
||||
click_diff_line(find("[id='#{sample_commit.line_code}']"))
|
||||
click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
fill_in(:note_note, with: 'Line is wrong')
|
||||
|
|
|
|||
|
|
@ -299,4 +299,51 @@ RSpec.shared_examples 'namespace traversal scopes' do
|
|||
include_examples '.self_and_descendant_ids'
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples '.self_and_hierarchy' do
|
||||
let(:base_scope) { Group.where(id: base_groups) }
|
||||
|
||||
subject { base_scope.self_and_hierarchy }
|
||||
|
||||
context 'with ancestors only' do
|
||||
let(:base_groups) { [group_1, group_2] }
|
||||
|
||||
it { is_expected.to match_array(groups) }
|
||||
end
|
||||
|
||||
context 'with descendants only' do
|
||||
let(:base_groups) { [deep_nested_group_1, deep_nested_group_2] }
|
||||
|
||||
it { is_expected.to match_array(groups) }
|
||||
end
|
||||
|
||||
context 'nodes with both ancestors and descendants' do
|
||||
let(:base_groups) { [nested_group_1, nested_group_2] }
|
||||
|
||||
it { is_expected.to match_array(groups) }
|
||||
end
|
||||
|
||||
context 'with duplicate base groups' do
|
||||
let(:base_groups) { [nested_group_1, nested_group_1] }
|
||||
|
||||
it { is_expected.to contain_exactly(group_1, nested_group_1, deep_nested_group_1) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.self_and_hierarchy' do
|
||||
it_behaves_like '.self_and_hierarchy'
|
||||
|
||||
context "use_traversal_ids_for_self_and_hierarchy_scopes feature flag is false" do
|
||||
before do
|
||||
stub_feature_flags(use_traversal_ids_for_self_and_hierarchy_scopes: false)
|
||||
end
|
||||
|
||||
it_behaves_like '.self_and_hierarchy'
|
||||
|
||||
it 'make recursive queries' do
|
||||
base_groups = Group.where(id: nested_group_1)
|
||||
expect { base_groups.self_and_hierarchy.load }.to make_queries_matching(/WITH RECURSIVE/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,11 +26,30 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
|
|||
allow(::Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
shared_examples 'not aborting any migration' do
|
||||
it 'will not abort the migration' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 0)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:long_running_stale_migration_container_repository_ids, [stale_migration.id])
|
||||
|
||||
expect { subject }
|
||||
.to not_change(pre_importing_migrations, :count)
|
||||
.and not_change(pre_import_done_migrations, :count)
|
||||
.and not_change(importing_migrations, :count)
|
||||
.and not_change(import_done_migrations, :count)
|
||||
.and not_change(import_aborted_migrations, :count)
|
||||
.and not_change { stale_migration.reload.migration_state }
|
||||
.and not_change { ongoing_migration.migration_state }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no stale migrations' do
|
||||
it_behaves_like 'an idempotent worker'
|
||||
|
||||
it 'will not update any migration state' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 0)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 0)
|
||||
|
||||
expect { subject }
|
||||
.to not_change(pre_importing_migrations, :count)
|
||||
.and not_change(pre_import_done_migrations, :count)
|
||||
|
|
@ -41,10 +60,19 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
|
|||
|
||||
context 'with pre_importing stale migrations' do
|
||||
let(:ongoing_migration) { create(:container_repository, :pre_importing) }
|
||||
let(:stale_migration) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 10.minutes.ago) }
|
||||
let(:stale_migration) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 35.minutes.ago) }
|
||||
let(:import_status) { 'test' }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(ContainerRegistry::GitlabApiClient) do |client|
|
||||
allow(client).to receive(:import_status).and_return(import_status)
|
||||
end
|
||||
end
|
||||
|
||||
it 'will abort the migration' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 1)
|
||||
|
||||
expect { subject }
|
||||
.to change(pre_importing_migrations, :count).by(-1)
|
||||
.and not_change(pre_import_done_migrations, :count)
|
||||
|
|
@ -54,18 +82,26 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
|
|||
.and change { stale_migration.reload.migration_state }.from('pre_importing').to('import_aborted')
|
||||
.and not_change { ongoing_migration.migration_state }
|
||||
end
|
||||
|
||||
context 'the client returns pre_import_in_progress' do
|
||||
let(:import_status) { 'pre_import_in_progress' }
|
||||
|
||||
it_behaves_like 'not aborting any migration'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pre_import_done stale migrations' do
|
||||
let(:ongoing_migration) { create(:container_repository, :pre_import_done) }
|
||||
let(:stale_migration) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 10.minutes.ago) }
|
||||
let(:stale_migration) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 35.minutes.ago) }
|
||||
|
||||
before do
|
||||
allow(::ContainerRegistry::Migration).to receive(:max_step_duration).and_return(5.minutes)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
end
|
||||
|
||||
it 'will abort the migration' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 1)
|
||||
|
||||
expect { subject }
|
||||
.to not_change(pre_importing_migrations, :count)
|
||||
.and change(pre_import_done_migrations, :count).by(-1)
|
||||
|
|
@ -79,14 +115,19 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
|
|||
|
||||
context 'with importing stale migrations' do
|
||||
let(:ongoing_migration) { create(:container_repository, :importing) }
|
||||
let(:stale_migration) { create(:container_repository, :importing, migration_import_started_at: 10.minutes.ago) }
|
||||
let(:stale_migration) { create(:container_repository, :importing, migration_import_started_at: 35.minutes.ago) }
|
||||
let(:import_status) { 'test' }
|
||||
|
||||
before do
|
||||
allow(::ContainerRegistry::Migration).to receive(:max_step_duration).and_return(5.minutes)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
allow_next_instance_of(ContainerRegistry::GitlabApiClient) do |client|
|
||||
allow(client).to receive(:import_status).and_return(import_status)
|
||||
end
|
||||
end
|
||||
|
||||
it 'will abort the migration' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 1)
|
||||
|
||||
expect { subject }
|
||||
.to not_change(pre_importing_migrations, :count)
|
||||
.and not_change(pre_import_done_migrations, :count)
|
||||
|
|
@ -96,6 +137,12 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
|
|||
.and change { stale_migration.reload.migration_state }.from('importing').to('import_aborted')
|
||||
.and not_change { ongoing_migration.migration_state }
|
||||
end
|
||||
|
||||
context 'the client returns import_in_progress' do
|
||||
let(:import_status) { 'import_in_progress' }
|
||||
|
||||
it_behaves_like 'not aborting any migration'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue