diff options
51 files changed, 619 insertions, 482 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 2cd25f58770..a8b870f9b8e 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,4 +1,4 @@ -import { sortBy } from 'lodash'; +import { sortBy, cloneDeep } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { ListType, NOT_FILTER } from './constants'; @@ -113,6 +113,37 @@ export function formatIssueInput(issueInput, boardConfig) { }; } +export function shouldCloneCard(fromListType, toListType) { + const involvesClosed = fromListType === ListType.closed || toListType === ListType.closed; + const involvesBacklog = fromListType === ListType.backlog || toListType === ListType.backlog; + + if (involvesClosed || involvesBacklog) { + return false; + } + + if (fromListType !== toListType) { + return true; + } + + return false; +} + +export function getMoveData(state, params) { + const { boardItems, boardItemsByListId, boardLists } = state; + const { itemId, fromListId, toListId } = params; + const fromListType = boardLists[fromListId].listType; + const toListType = boardLists[toListId].listType; + + return { + reordering: fromListId === toListId, + shouldClone: shouldCloneCard(fromListType, toListType), + itemNotInToList: !boardItemsByListId[toListId].includes(itemId), + originalIssue: cloneDeep(boardItems[itemId]), + originalIndex: boardItemsByListId[fromListId].indexOf(itemId), + ...params, + }; +} + export function moveItemListHelper(item, fromList, toList) { const updatedItem = item; if ( diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index ae8434be312..94e29f3ad86 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -190,7 +190,7 @@ export default { } this.moveItem({ - itemId, + itemId: Number(itemId), itemIid, itemPath, fromListId: from.dataset.listId, diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index edae4c75de3..db4ecd8c614 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -2,6 +2,7 @@ import * as Sentry from '@sentry/browser'; import { pick } from 'lodash'; import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql'; import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; +import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; import { BoardType, ListType, @@ -23,13 +24,14 @@ import { formatIssueInput, updateListPosition, transformNotFilters, + moveItemListHelper, + getMoveData, } from '../boards_util'; import boardLabelsQuery from '../graphql/board_labels.query.graphql'; import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql'; import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; -import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql'; import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql'; import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql'; import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql'; @@ -333,42 +335,134 @@ export default { dispatch('moveIssue', payload); }, - moveIssue: ( - { state, commit }, - { itemId, itemIid, itemPath, fromListId, toListId, moveBeforeId, moveAfterId }, + moveIssue: ({ dispatch, state }, params) => { + const moveData = getMoveData(state, params); + + dispatch('moveIssueCard', moveData); + dispatch('updateMovedIssue', moveData); + dispatch('updateIssueOrder', { moveData }); + }, + + moveIssueCard: ({ commit }, moveData) => { + const { + reordering, + shouldClone, + itemNotInToList, + originalIndex, + itemId, + fromListId, + toListId, + moveBeforeId, + moveAfterId, + } = moveData; + + commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId }); + + if (reordering) { + commit(types.ADD_BOARD_ITEM_TO_LIST, { + itemId, + listId: toListId, + moveBeforeId, + moveAfterId, + }); + + return; + } + + if (itemNotInToList) { + commit(types.ADD_BOARD_ITEM_TO_LIST, { + itemId, + listId: toListId, + moveBeforeId, + moveAfterId, + }); + } + + if (shouldClone) { + commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex }); + } + }, + + updateMovedIssue: ( + { commit, state: { boardItems, boardLists } }, + { itemId, fromListId, toListId }, ) => { - const originalIssue = state.boardItems[itemId]; - const fromList = state.boardItemsByListId[fromListId]; - const originalIndex = fromList.indexOf(Number(itemId)); - commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }); + const updatedIssue = moveItemListHelper( + boardItems[itemId], + boardLists[fromListId], + boardLists[toListId], + ); - const { boardId } = state; - const [fullProjectPath] = itemPath.split(/[#]/); + commit(types.UPDATE_BOARD_ITEM, updatedIssue); + }, - gqlClient - .mutate({ + undoMoveIssueCard: ({ commit }, moveData) => { + const { + reordering, + shouldClone, + itemNotInToList, + itemId, + fromListId, + toListId, + originalIssue, + originalIndex, + } = moveData; + + commit(types.UPDATE_BOARD_ITEM, originalIssue); + + if (reordering) { + commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId }); + commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex }); + return; + } + + if (shouldClone) { + commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId }); + } + if (itemNotInToList) { + commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: toListId }); + } + + commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex }); + }, + + updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => { + try { + const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData; + const { + boardId, + boardItems: { + [itemId]: { iid, referencePath }, + }, + } = state; + + const { data } = await gqlClient.mutate({ mutation: issueMoveListMutation, variables: { - projectPath: fullProjectPath, + iid, + projectPath: referencePath.split(/[#]/)[0], boardId: fullBoardId(boardId), - iid: itemIid, fromListId: getIdFromGraphQLId(fromListId), toListId: getIdFromGraphQLId(toListId), moveBeforeId, moveAfterId, + // 'mutationVariables' allows EE code to pass in extra parameters. + ...mutationVariables, }, - }) - .then(({ data }) => { - if (data?.issueMoveList?.errors.length) { - throw new Error(); - } else { - const issue = data.issueMoveList?.issue; - commit(types.MOVE_ISSUE_SUCCESS, { issue }); - } - }) - .catch(() => - commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }), + }); + + if (data?.issueMoveList?.errors.length || !data.issueMoveList) { + throw new Error('issueMoveList empty'); + } + + commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issueMoveList.issue }); + } catch { + commit( + types.SET_ERROR, + s__('Boards|An error occurred while moving the issue. Please try again.'), ); + dispatch('undoMoveIssueCard', moveData); + } }, setAssignees: ({ commit, getters }, assigneeUsernames) => { diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 7ff1f8296ca..22b9905ee62 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -23,12 +23,10 @@ export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS'; export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE'; export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS'; export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR'; -export const MOVE_ISSUE = 'MOVE_ISSUE'; -export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS'; -export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE'; export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM'; export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM'; export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; +export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS'; export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 495b3b31df5..561c21b78c1 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -2,7 +2,7 @@ import { pull, union } from 'lodash'; import Vue from 'vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { s__ } from '~/locale'; -import { formatIssue, moveItemListHelper } from '../boards_util'; +import { formatIssue } from '../boards_util'; import { issuableTypes } from '../constants'; import * as mutationTypes from './mutation_types'; @@ -183,40 +183,11 @@ export default { notImplemented(); }, - [mutationTypes.MOVE_ISSUE]: ( - state, - { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }, - ) => { - const fromList = state.boardLists[fromListId]; - const toList = state.boardLists[toListId]; - - const issue = moveItemListHelper(originalIssue, fromList, toList); - Vue.set(state.boardItems, issue.id, issue); - - removeItemFromList({ state, listId: fromListId, itemId: issue.id }); - addItemToList({ state, listId: toListId, itemId: issue.id, moveBeforeId, moveAfterId }); - }, - - [mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => { + [mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => { const issueId = getIdFromGraphQLId(issue.id); Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId })); }, - [mutationTypes.MOVE_ISSUE_FAILURE]: ( - state, - { originalIssue, fromListId, toListId, originalIndex }, - ) => { - state.error = s__('Boards|An error occurred while moving the issue. Please try again.'); - Vue.set(state.boardItems, originalIssue.id, originalIssue); - removeItemFromList({ state, listId: toListId, itemId: originalIssue.id }); - addItemToList({ - state, - listId: fromListId, - itemId: originalIssue.id, - atIndex: originalIndex, - }); - }, - [mutationTypes.REQUEST_UPDATE_ISSUE]: () => { notImplemented(); }, diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 02fb5df07e8..8f41b848b0b 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -184,12 +184,7 @@ export default { 'viewDiffsFileByFile', 'mrReviews', ]), - ...mapGetters('diffs', [ - 'whichCollapsedTypes', - 'isParallelView', - 'currentDiffIndex', - 'fileCodequalityDiff', - ]), + ...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']), ...mapGetters(['isNotesFetched', 'getNoteableData']), diffs() { if (!this.viewDiffsFileByFile) { @@ -287,7 +282,6 @@ export default { endpointMetadata: this.endpointMetadata, endpointBatch: this.endpointBatch, endpointCoverage: this.endpointCoverage, - endpointCodequality: this.endpointCodequality, endpointUpdateUser: this.endpointUpdateUser, projectPath: this.projectPath, dismissEndpoint: this.dismissEndpoint, @@ -297,6 +291,10 @@ export default { mrReviews: this.rehydratedMrReviews, }); + if (this.endpointCodequality) { + this.setCodequalityEndpoint(this.endpointCodequality); + } + if (this.shouldShow) { this.fetchData(); } @@ -341,6 +339,7 @@ export default { ...mapActions('diffs', [ 'moveToNeighboringCommit', 'setBaseConfig', + 'setCodequalityEndpoint', 'fetchDiffFilesMeta', 'fetchDiffFilesBatch', 'fetchCoverageFiles', @@ -532,7 +531,6 @@ export default { :help-page-path="helpPagePath" :can-current-user-fork="canCurrentUserFork" :view-diffs-file-by-file="viewDiffsFileByFile" - :codequality-diff="fileCodequalityDiff(file.file_path)" /> <div v-if="showFileByFileNavigation" diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 93855db52b6..bdbc13a38c4 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -67,11 +67,6 @@ export default { type: Boolean, required: true, }, - codequalityDiff: { - type: Array, - required: false, - default: () => [], - }, }, data() { return { @@ -85,7 +80,7 @@ export default { genericError: GENERIC_ERROR, }, computed: { - ...mapState('diffs', ['currentDiffFileId']), + ...mapState('diffs', ['currentDiffFileId', 'codequalityDiff']), ...mapGetters(['isNotesFetched']), ...mapGetters('diffs', ['getDiffFileDiscussions']), viewBlobHref() { @@ -154,7 +149,9 @@ export default { return loggedIn && featureOn; }, hasCodequalityChanges() { - return this.codequalityDiff.length > 0; + return ( + this.codequalityDiff?.files && this.codequalityDiff?.files[this.file.file_path]?.length > 0 + ); }, }, watch: { diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 81416984dbf..1c66ad1a18c 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -1,5 +1,4 @@ import Cookies from 'js-cookie'; -import Visibility from 'visibilityjs'; import Vue from 'vue'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { diffViewerModes } from '~/ide/constants'; @@ -53,15 +52,12 @@ import { prepareLineForRenamedFile, } from './utils'; -let eTagPoll; - export const setBaseConfig = ({ commit }, options) => { const { endpoint, endpointMetadata, endpointBatch, endpointCoverage, - endpointCodequality, endpointUpdateUser, projectPath, dismissEndpoint, @@ -75,7 +71,6 @@ export const setBaseConfig = ({ commit }, options) => { endpointMetadata, endpointBatch, endpointCoverage, - endpointCodequality, endpointUpdateUser, projectPath, dismissEndpoint, @@ -238,48 +233,6 @@ export const fetchCoverageFiles = ({ commit, state }) => { coveragePoll.makeRequest(); }; -export const clearEtagPoll = () => { - eTagPoll = null; -}; - -export const stopCodequalityPolling = () => { - if (eTagPoll) eTagPoll.stop(); -}; - -export const restartCodequalityPolling = () => { - if (eTagPoll) eTagPoll.restart(); -}; - -export const fetchCodequality = ({ commit, state, dispatch }) => { - eTagPoll = new Poll({ - resource: { - getCodequalityDiffReports: (endpoint) => axios.get(endpoint), - }, - data: state.endpointCodequality, - method: 'getCodequalityDiffReports', - successCallback: ({ status, data }) => { - if (status === httpStatusCodes.OK) { - commit(types.SET_CODEQUALITY_DATA, data); - - eTagPoll.stop(); - } - }, - errorCallback: () => createFlash(__('Something went wrong on our end. Please try again!')), - }); - - if (!Visibility.hidden()) { - eTagPoll.makeRequest(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - dispatch('restartCodequalityPolling'); - } else { - dispatch('stopCodequalityPolling'); - } - }); -}; - export const setHighlightedRow = ({ commit }, lineCode) => { const fileHash = lineCode.split('_')[0]; commit(types.SET_HIGHLIGHTED_ROW, lineCode); diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index b06faa2284b..dec3f87b03e 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -136,16 +136,6 @@ export const fileLineCoverage = (state) => (file, line) => { }; /** - * Returns the codequality diff data for a given file - * @param {string} filePath - * @returns {Array} - */ -export const fileCodequalityDiff = (state) => (filePath) => { - if (!state.codequalityDiff.files || !state.codequalityDiff.files[filePath]) return []; - return state.codequalityDiff.files[filePath]; -}; - -/** * Returns index of a currently selected diff in diffFiles * @returns {number} */ diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index a99ef00d61e..1674d3d3b5a 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -30,7 +30,6 @@ export default () => ({ startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff diffFiles: [], coverageFiles: {}, - codequalityDiff: {}, mergeRequestDiffs: [], mergeRequestDiff: null, diffViewType: getViewTypeFromQueryString() || viewTypeFromCookie || defaultViewType, diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js index 6860e24db6b..03d11e60745 100644 --- a/app/assets/javascripts/diffs/store/modules/index.js +++ b/app/assets/javascripts/diffs/store/modules/index.js @@ -1,7 +1,7 @@ -import * as actions from '../actions'; +import * as actions from 'ee_else_ce/diffs/store/actions'; +import createState from 'ee_else_ce/diffs/store/modules/diff_state'; +import mutations from 'ee_else_ce/diffs/store/mutations'; import * as getters from '../getters'; -import mutations from '../mutations'; -import createState from './diff_state'; export default () => ({ namespaced: true, diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index b0f396f905a..4641731c4b6 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -11,7 +11,6 @@ export const SET_MR_FILE_REVIEWS = 'SET_MR_FILE_REVIEWS'; export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE'; export const SET_COVERAGE_DATA = 'SET_COVERAGE_DATA'; -export const SET_CODEQUALITY_DATA = 'SET_CODEQUALITY_DATA'; export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS'; export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM'; export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index eacf76234fc..9ff9a02d444 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -33,7 +33,6 @@ export default { endpointMetadata, endpointBatch, endpointCoverage, - endpointCodequality, endpointUpdateUser, projectPath, dismissEndpoint, @@ -47,7 +46,6 @@ export default { endpointMetadata, endpointBatch, endpointCoverage, - endpointCodequality, endpointUpdateUser, projectPath, dismissEndpoint, @@ -91,10 +89,6 @@ export default { Object.assign(state, { coverageFiles }); }, - [types.SET_CODEQUALITY_DATA](state, codequalityDiffData) { - Object.assign(state, { codequalityDiff: codequalityDiffData }); - }, - [types.RENDER_FILE](state, file) { renderFile(file); }, diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue index d15c3e0bb6d..bf992b84387 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -103,7 +103,7 @@ export default { <template> <gl-dropdown ref="dropdown" - v-gl-tooltip.hover + v-gl-tooltip.hover.ds0 data-testid="mini-pipeline-graph-dropdown" :title="stage.title" variant="link" diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d0761083c8b..8a090c8ef10 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -13,7 +13,7 @@ class Admin::UsersController < Admin::ApplicationController def index @users = User.filter_items(params[:filter]).order_name_asc @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present? - @users = @users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord + @users = users_with_included_associations(@users) @users = @users.sort_by_attribute(@sort = params[:sort]) @users = @users.page(params[:page]) @@ -228,6 +228,10 @@ class Admin::UsersController < Admin::ApplicationController protected + def users_with_included_associations(users) + users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord + end + def admin_making_changes_for_another_user? user != current_user end diff --git a/app/controllers/projects/logs_controller.rb b/app/controllers/projects/logs_controller.rb index b9aa9bfc947..f9b8091a419 100644 --- a/app/controllers/projects/logs_controller.rb +++ b/app/controllers/projects/logs_controller.rb @@ -58,7 +58,7 @@ module Projects def environment strong_memoize(:environment) do if cluster_params.key?(:environment_name) - EnvironmentsFinder.new(project, current_user, name: cluster_params[:environment_name]).find.first + EnvironmentsFinder.new(project, current_user, name: cluster_params[:environment_name]).execute.first else project.default_environment end diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb index 62de2c6ecc8..3ce5e258cf2 100644 --- a/app/finders/environments_finder.rb +++ b/app/finders/environments_finder.rb @@ -9,12 +9,7 @@ class EnvironmentsFinder @project, @current_user, @params = project, current_user, params end - # This method will eventually take the place of `#execute` as an - # efficient way to get relevant environment entries. - # Currently, `#execute` method has a serious technical debt and - # we will likely rework on it in the future. - # See more https://gitlab.com/gitlab-org/gitlab-foss/issues/63381 - def find + def execute environments = project.environments environments = by_name(environments) environments = by_search(environments) diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb index ed3395d05aa..df04e70e250 100644 --- a/app/graphql/resolvers/environments_resolver.rb +++ b/app/graphql/resolvers/environments_resolver.rb @@ -21,7 +21,7 @@ module Resolvers def resolve(**args) return unless project.present? - EnvironmentsFinder.new(project, context[:current_user], args).find + EnvironmentsFinder.new(project, context[:current_user], args).execute rescue EnvironmentsFinder::InvalidStatesError => exception raise Gitlab::Graphql::Errors::ArgumentError, exception.message end diff --git a/app/serializers/admin/user_entity.rb b/app/serializers/admin/user_entity.rb index 8908d610046..a5cf40a50b9 100644 --- a/app/serializers/admin/user_entity.rb +++ b/app/serializers/admin/user_entity.rb @@ -30,3 +30,5 @@ module Admin end end end + +Admin::UserEntity.prepend_if_ee('EE::Admin::UserEntity') diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index cee24098049..68660b35bee 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -38,12 +38,6 @@ module Issues user_agent_detail_service.create resolve_discussions_with_issue(issue) - if Feature.disabled?(:issue_perform_after_creation_tasks_async, issue.project, default_enabled: :yaml) - Issues::AfterCreateService - .new(issue.project, current_user) - .execute(issue) - end - super end diff --git a/app/services/prometheus/create_default_alerts_service.rb b/app/services/prometheus/create_default_alerts_service.rb index 53baf6a650e..4ae2743cc28 100644 --- a/app/services/prometheus/create_default_alerts_service.rb +++ b/app/services/prometheus/create_default_alerts_service.rb @@ -84,7 +84,7 @@ module Prometheus def environment strong_memoize(:environment) do - EnvironmentsFinder.new(project, nil, name: 'production').find.first || + EnvironmentsFinder.new(project, nil, name: 'production').execute.first || project.environments.first end end diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 50f3c94bcb3..79d77790b02 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -18,20 +18,20 @@ .search-holder = render 'shared/projects/search_form', autofocus: true, admin_view: true .dropdown - - toggle_text = 'Namespace' + - toggle_text = _('Namespace') - if params[:namespace_id].present? = hidden_field_tag :namespace_id, params[:namespace_id] - namespace = Namespace.find(params[:namespace_id]) - toggle_text = "#{namespace.kind}: #{namespace.full_path}" = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' }) .dropdown-menu.dropdown-select.dropdown-menu-right - = dropdown_title('Namespaces') - = dropdown_filter("Search for Namespace") + = dropdown_title(_('Namespaces')) + = dropdown_filter(_("Search for Namespace")) = dropdown_content = dropdown_loading = render 'shared/projects/dropdown' = link_to new_project_path, class: 'gl-button btn btn-confirm' do - New Project - = button_tag "Search", class: "gl-button btn btn-confirm btn-search hide" + = _('New Project') + = button_tag _("Search"), class: "gl-button btn btn-confirm btn-search hide" = render 'projects' diff --git a/app/views/projects/jobs/_table.html.haml b/app/views/projects/jobs/_table.html.haml index fc4b3260ab3..819837a9eff 100644 --- a/app/views/projects/jobs/_table.html.haml +++ b/app/views/projects/jobs/_table.html.haml @@ -28,7 +28,7 @@ %th Runner %th Stage %th Name - %th Timing + %th Duration %th Coverage %th diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml index 390a070de02..94f16369e96 100644 --- a/app/views/registrations/welcome/show.html.haml +++ b/app/views/registrations/welcome/show.html.haml @@ -1,12 +1,16 @@ - page_title _('Your profile') - add_page_specific_style 'page_bundles/signup' +- gitlab_experience_text = _('To personalize your GitLab experience, we\'d like to know a bit more about you') .row.gl-flex-grow-1 .d-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.gl-p-5 .edit-profile.login-page.d-flex.flex-column.gl-align-items-center.pt-lg-3 = render_if_exists "registrations/welcome/progress_bar" %h2.gl-text-center= html_escape(_('Welcome to GitLab,%{br_tag}%{name}!')) % { name: html_escape(current_user.first_name), br_tag: '<br/>'.html_safe } - %p.gl-text-center= html_escape(_('To personalize your GitLab experience, we\'d like to know a bit more about you. We won\'t share this information with anyone.')) % { br_tag: '<br/>'.html_safe } + - if Gitlab.com? + %p.gl-text-center= html_escape(_('%{gitlab_experience_text}. We won\'t share this information with anyone.')) % { gitlab_experience_text: gitlab_experience_text } + - else + %p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text } = form_for(current_user, url: users_sign_up_welcome_path, html: { class: 'card gl-w-full! gl-p-5', 'aria-live' => 'assertive' }) do |f| .devise-errors = render 'devise/shared/error_messages', resource: current_user diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb index 03a8af30c6f..c08f4b4cd75 100644 --- a/app/workers/new_issue_worker.rb +++ b/app/workers/new_issue_worker.rb @@ -17,11 +17,9 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker issuable.create_cross_references!(user) - if Feature.enabled?(:issue_perform_after_creation_tasks_async, issuable.project, default_enabled: :yaml) - Issues::AfterCreateService - .new(issuable.project, user) - .execute(issuable) - end + Issues::AfterCreateService + .new(issuable.project, user) + .execute(issuable) end def issuable_class diff --git a/changelogs/unreleased/21140-remove-issue-perform-after-creation-tasks-async-feature-flag.yml b/changelogs/unreleased/21140-remove-issue-perform-after-creation-tasks-async-feature-flag.yml new file mode 100644 index 00000000000..04c9d1b8d0d --- /dev/null +++ b/changelogs/unreleased/21140-remove-issue-perform-after-creation-tasks-async-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove issue_perform_after_creation_tasks_async feature flag +merge_request: 59042 +author: +type: other diff --git a/changelogs/unreleased/Externalize-strings-in-projects-index-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-projects-index-html-haml.yml new file mode 100644 index 00000000000..5ed6e7fb88e --- /dev/null +++ b/changelogs/unreleased/Externalize-strings-in-projects-index-html-haml.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings in projects/index.html.haml +merge_request: 58160 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/nicolasdular-welcome-page-text-change.yml b/changelogs/unreleased/nicolasdular-welcome-page-text-change.yml new file mode 100644 index 00000000000..ed3fbd335cc --- /dev/null +++ b/changelogs/unreleased/nicolasdular-welcome-page-text-change.yml @@ -0,0 +1,5 @@ +--- +title: Clarify on welcome page that we do not share any data +merge_request: 59183 +author: +type: changed diff --git a/changelogs/unreleased/reduce-pipline-tooltip-delay-to-zero.yml b/changelogs/unreleased/reduce-pipline-tooltip-delay-to-zero.yml new file mode 100644 index 00000000000..aedff0a7080 --- /dev/null +++ b/changelogs/unreleased/reduce-pipline-tooltip-delay-to-zero.yml @@ -0,0 +1,5 @@ +--- +title: Reduce pipeline tooltip delay to 0 +merge_request: 59155 +author: +type: changed diff --git a/changelogs/unreleased/ui-text-job-pipeline-runtime.yml b/changelogs/unreleased/ui-text-job-pipeline-runtime.yml new file mode 100644 index 00000000000..eeb9bfc502a --- /dev/null +++ b/changelogs/unreleased/ui-text-job-pipeline-runtime.yml @@ -0,0 +1,5 @@ +--- +title: Update UI text from timing to Run time +merge_request: 58838 +author: +type: other diff --git a/config/feature_flags/development/issue_perform_after_creation_tasks_async.yml b/config/feature_flags/development/issue_perform_after_creation_tasks_async.yml deleted file mode 100644 index 0efef586022..00000000000 --- a/config/feature_flags/development/issue_perform_after_creation_tasks_async.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: issue_perform_after_creation_tasks_async -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58588 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/21140 -milestone: '13.11' -type: development -group: group::geo -default_enabled: true diff --git a/doc/user/admin_area/img/export_permissions_v13_11.png b/doc/user/admin_area/img/export_permissions_v13_11.png Binary files differnew file mode 100644 index 00000000000..d9bbe8c3daf --- /dev/null +++ b/doc/user/admin_area/img/export_permissions_v13_11.png diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md index 6877148bd6d..08fcd4674dc 100644 --- a/doc/user/admin_area/index.md +++ b/doc/user/admin_area/index.md @@ -173,6 +173,8 @@ The following data is included in the export: - Path - Access level ([Project](../permissions.md#project-members-permissions) and [Group](../permissions.md#group-members-permissions)) +![user permission export button](img/export_permissions_v13_11.png) + #### Users statistics The **Users statistics** page provides an overview of user accounts by role. These statistics are diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 3e1e430c2f9..b606b2e814d 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -26,7 +26,7 @@ module API get ':id/environments' do authorize! :read_environment, user_project - environments = ::EnvironmentsFinder.new(user_project, current_user, params).find + environments = ::EnvironmentsFinder.new(user_project, current_user, params).execute present paginate(environments), with: Entities::Environment, current_user: current_user end diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb index c8b8d6c259d..786c5bf675b 100644 --- a/lib/gitlab/alert_management/payload/base.rb +++ b/lib/gitlab/alert_management/payload/base.rb @@ -132,7 +132,7 @@ module Gitlab EnvironmentsFinder .new(project, nil, { name: environment_name }) - .find + .execute .first end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cbcd2b4ed36..0efc8809179 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -539,6 +539,12 @@ msgstr "" msgid "%{firstMilestoneName} + %{numberOfOtherMilestones} more" msgstr "" +msgid "%{gitlab_experience_text}. Don't worry, this information isn't shared outside of your self-managed GitLab instance." +msgstr "" + +msgid "%{gitlab_experience_text}. We won't share this information with anyone." +msgstr "" + msgid "%{global_id} is not a valid ID for %{expected_type}." msgstr "" @@ -8048,7 +8054,7 @@ msgstr "" msgid "ComplianceFrameworks|Invalid format: it should follow the format [PATH].y(a)ml@[GROUP]/[PROJECT]" msgstr "" -msgid "ComplianceFrameworks|Once you have created a compliance framework it will appear here." +msgid "ComplianceFrameworks|Once a compliance framework is added it will appear here." msgstr "" msgid "ComplianceFrameworks|Regulated" @@ -29056,6 +29062,9 @@ msgstr "" msgid "Something went wrong on our end" msgstr "" +msgid "Something went wrong on our end while loading the code quality diff." +msgstr "" + msgid "Something went wrong on our end." msgstr "" @@ -31027,7 +31036,7 @@ msgstr "" msgid "The merge request can now be merged." msgstr "" -msgid "The merge request has been updated, and the number of code quality violations in this file has changed." +msgid "The merge request has made changes to this file that affect the number of code quality violations in it." msgstr "" msgid "The metric must be one of %{metrics}." @@ -32576,7 +32585,7 @@ msgstr "" msgid "To pass variables to the triggered pipeline, add %{code_start}variables[VARIABLE]=VALUE%{code_end} to the API request." msgstr "" -msgid "To personalize your GitLab experience, we'd like to know a bit more about you. We won't share this information with anyone." +msgid "To personalize your GitLab experience, we'd like to know a bit more about you" msgstr "" msgid "To preserve performance only %{strong_open}%{display_size} of %{real_size}%{strong_close} files are displayed." diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb index 888a1ef9567..c2022331ad9 100644 --- a/spec/finders/environments_finder_spec.rb +++ b/spec/finders/environments_finder_spec.rb @@ -11,37 +11,37 @@ RSpec.describe EnvironmentsFinder do project.add_maintainer(user) end - describe '#find' do + describe '#execute' do context 'with states parameter' do let(:stopped_environment) { create(:environment, :stopped, project: project) } it 'returns environments with the requested state' do - result = described_class.new(project, user, states: 'available').find + result = described_class.new(project, user, states: 'available').execute expect(result).to contain_exactly(environment) end it 'returns environments with any of the requested states' do - result = described_class.new(project, user, states: %w(available stopped)).find + result = described_class.new(project, user, states: %w(available stopped)).execute expect(result).to contain_exactly(environment, stopped_environment) end it 'raises exception when requested state is invalid' do - expect { described_class.new(project, user, states: %w(invalid stopped)).find }.to( + expect { described_class.new(project, user, states: %w(invalid stopped)).execute }.to( raise_error(described_class::InvalidStatesError, 'Requested states are invalid') ) end context 'works with symbols' do it 'returns environments with the requested state' do - result = described_class.new(project, user, states: :available).find + result = described_class.new(project, user, states: :available).execute expect(result).to contain_exactly(environment) end it 'returns environments with any of the requested states' do - result = described_class.new(project, user, states: [:available, :stopped]).find + result = described_class.new(project, user, states: [:available, :stopped]).execute expect(result).to contain_exactly(environment, stopped_environment) end @@ -53,7 +53,7 @@ RSpec.describe EnvironmentsFinder do let(:environment3) { create(:environment, :available, name: 'test3', project: project) } it 'searches environments by name and state' do - result = described_class.new(project, user, search: 'test', states: :available).find + result = described_class.new(project, user, search: 'test', states: :available).execute expect(result).to contain_exactly(environment3) end diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index a8a6423b47c..1c5b7cf8248 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -3,6 +3,7 @@ import { keyBy } from 'lodash'; import Vue from 'vue'; import '~/boards/models/list'; +import { ListType } from '~/boards/constants'; import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { @@ -488,3 +489,38 @@ export const mockBlockedIssue2 = { blockedByCount: 4, webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0', }; + +export const mockMoveIssueParams = { + itemId: 1, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + moveBeforeId: undefined, + moveAfterId: undefined, +}; + +export const mockMoveState = { + boardLists: { + 'gid://gitlab/List/1': { + listType: ListType.backlog, + }, + 'gid://gitlab/List/2': { + listType: ListType.closed, + }, + }, + boardItems: { + [mockMoveIssueParams.itemId]: { foo: 'bar' }, + }, + boardItemsByListId: { + [mockMoveIssueParams.fromListId]: [mockMoveIssueParams.itemId], + [mockMoveIssueParams.toListId]: [], + }, +}; + +export const mockMoveData = { + reordering: false, + shouldClone: false, + itemNotInToList: true, + originalIndex: 0, + originalIssue: { foo: 'bar' }, + ...mockMoveIssueParams, +}; diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 9b034c49075..bb747779cac 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/browser'; +import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; import testAction from 'helpers/vuex_action_helper'; import { fullBoardId, @@ -6,14 +7,15 @@ import { formatBoardLists, formatIssueInput, formatIssue, + getMoveData, } from '~/boards/boards_util'; -import { inactiveId, ISSUABLE } from '~/boards/constants'; +import { inactiveId, ISSUABLE, ListType } from '~/boards/constants'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'; -import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + import { mockLists, mockListsById, @@ -25,6 +27,9 @@ import { labels, mockActiveIssue, mockGroupProjects, + mockMoveIssueParams, + mockMoveState, + mockMoveData, } from '../mock_data'; jest.mock('~/flash'); @@ -653,64 +658,302 @@ describe('moveItem', () => { }); describe('moveIssue', () => { - const listIssues = { - 'gid://gitlab/List/1': [436, 437], - 'gid://gitlab/List/2': [], - }; + it('should dispatch a correct set of actions', () => { + testAction({ + action: actions.moveIssue, + payload: mockMoveIssueParams, + state: mockMoveState, + expectedActions: [ + { type: 'moveIssueCard', payload: mockMoveData }, + { type: 'updateMovedIssue', payload: mockMoveData }, + { type: 'updateIssueOrder', payload: { moveData: mockMoveData } }, + ], + }); + }); +}); - const issues = { - 436: mockIssue, - 437: mockIssue2, - }; +describe('moveIssueCard and undoMoveIssueCard', () => { + describe('card should move without clonning', () => { + let state; + let params; + let moveMutations; + let undoMutations; + + describe('when re-ordering card', () => { + beforeEach( + ({ + itemId = 123, + fromListId = 'gid://gitlab/List/1', + toListId = 'gid://gitlab/List/1', + originalIssue = { foo: 'bar' }, + originalIndex = 0, + moveBeforeId = undefined, + moveAfterId = undefined, + } = {}) => { + state = { + boardLists: { + [toListId]: { listType: ListType.backlog }, + [fromListId]: { listType: ListType.backlog }, + }, + boardItems: { [itemId]: originalIssue }, + boardItemsByListId: { [fromListId]: [123] }, + }; + params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + moveMutations = [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + }, + ]; + undoMutations = [ + { type: types.UPDATE_BOARD_ITEM, payload: originalIssue }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; + }, + ); - const state = { - fullPath: 'gitlab-org', - boardId: '1', - boardType: 'group', - disabled: false, - boardLists: mockLists, - boardItemsByListId: listIssues, - boardItems: issues, - }; + it('moveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.moveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: moveMutations, + }); + }); - it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', (done) => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { - issueMoveList: { - issue: rawIssue, - errors: [], - }, - }, + it('undoMoveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.undoMoveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: undoMutations, + }); + }); }); - testAction( - actions.moveIssue, - { - itemId: '436', - itemIid: mockIssue.iid, - itemPath: mockIssue.referencePath, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, - state, + describe.each([ [ + 'issue moves out of backlog', { - type: types.MOVE_ISSUE, - payload: { - originalIssue: mockIssue, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, + fromListType: ListType.backlog, + toListType: ListType.label, }, + ], + [ + 'issue card moves to closed', { - type: types.MOVE_ISSUE_SUCCESS, - payload: { issue: rawIssue }, + fromListType: ListType.label, + toListType: ListType.closed, }, ], - [], - done, - ); + [ + 'issue card moves to non-closed, non-backlog list of the same type', + { + fromListType: ListType.label, + toListType: ListType.label, + }, + ], + ])('when %s', (_, { toListType, fromListType }) => { + beforeEach( + ({ + itemId = 123, + fromListId = 'gid://gitlab/List/1', + toListId = 'gid://gitlab/List/2', + originalIssue = { foo: 'bar' }, + originalIndex = 0, + moveBeforeId = undefined, + moveAfterId = undefined, + } = {}) => { + state = { + boardLists: { + [fromListId]: { listType: fromListType }, + [toListId]: { listType: toListType }, + }, + boardItems: { [itemId]: originalIssue }, + boardItemsByListId: { [fromListId]: [123], [toListId]: [] }, + }; + params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + moveMutations = [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + }, + ]; + undoMutations = [ + { type: types.UPDATE_BOARD_ITEM, payload: originalIssue }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; + }, + ); + + it('moveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.moveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: moveMutations, + }); + }); + + it('undoMoveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.undoMoveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: undoMutations, + }); + }); + }); + }); + + describe('card should clone on move', () => { + let state; + let params; + let moveMutations; + let undoMutations; + + describe.each([ + [ + 'issue card moves to non-closed, non-backlog list of a different type', + { + fromListType: ListType.label, + toListType: ListType.assignee, + }, + ], + ])('when %s', (_, { toListType, fromListType }) => { + beforeEach( + ({ + itemId = 123, + fromListId = 'gid://gitlab/List/1', + toListId = 'gid://gitlab/List/2', + originalIssue = { foo: 'bar' }, + originalIndex = 0, + moveBeforeId = undefined, + moveAfterId = undefined, + } = {}) => { + state = { + boardLists: { + [fromListId]: { listType: fromListType }, + [toListId]: { listType: toListType }, + }, + boardItems: { [itemId]: originalIssue }, + boardItemsByListId: { [fromListId]: [123], [toListId]: [] }, + }; + params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + moveMutations = [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; + undoMutations = [ + { type: types.UPDATE_BOARD_ITEM, payload: originalIssue }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; + }, + ); + + it('moveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.moveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: moveMutations, + }); + }); + + it('undoMoveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.undoMoveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: undoMutations, + }); + }); + }); }); +}); + +describe('updateMovedIssueCard', () => { + const label1 = { + id: 'label1', + }; + + it.each([ + [ + 'issue without a label is moved to a label list', + { + state: { + boardLists: { + from: {}, + to: { + listType: ListType.label, + label: label1, + }, + }, + boardItems: { + 1: { + labels: [], + }, + }, + }, + moveData: { + itemId: 1, + fromListId: 'from', + toListId: 'to', + }, + updatedIssue: { labels: [label1] }, + }, + ], + ])( + 'should commit UPDATE_BOARD_ITEM with a correctly updated issue data when %s', + (_, { state, moveData, updatedIssue }) => { + testAction({ + action: actions.updateMovedIssue, + payload: moveData, + state, + expectedMutations: [{ type: types.UPDATE_BOARD_ITEM, payload: updatedIssue }], + }); + }, + ); +}); + +describe('updateIssueOrder', () => { + const issues = { + 436: mockIssue, + 437: mockIssue2, + }; + + const state = { + boardItems: issues, + boardId: 'gid://gitlab/Board/1', + }; + + const moveData = { + itemId: 436, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + }; it('calls mutate with the correct variables', () => { const mutationVariables = { @@ -734,61 +977,56 @@ describe('moveIssue', () => { }, }); - actions.moveIssue( - { state, commit: () => {} }, - { - itemId: mockIssue.id, - itemIid: mockIssue.iid, - itemPath: mockIssue.referencePath, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, - ); + actions.updateIssueOrder({ state, commit: () => {}, dispatch: () => {} }, { moveData }); expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables); }); - it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', (done) => { + it('should commit MUTATE_ISSUE_SUCCESS mutation when successful', () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { issueMoveList: { - issue: {}, - errors: [{ foo: 'bar' }], + issue: rawIssue, + errors: [], }, }, }); testAction( - actions.moveIssue, - { - itemId: '436', - itemIid: mockIssue.iid, - itemPath: mockIssue.referencePath, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, + actions.updateIssueOrder, + { moveData }, state, [ { - type: types.MOVE_ISSUE, - payload: { - originalIssue: mockIssue, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, + type: types.MUTATE_ISSUE_SUCCESS, + payload: { issue: rawIssue }, + }, + ], + [], + ); + }); + + it('should commit SET_ERROR and dispatch undoMoveIssueCard', () => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + issueMoveList: { + issue: {}, + errors: [{ foo: 'bar' }], }, + }, + }); + + testAction( + actions.updateIssueOrder, + { moveData }, + state, + [ { - type: types.MOVE_ISSUE_FAILURE, - payload: { - originalIssue: mockIssue, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - originalIndex: 0, - }, + type: types.SET_ERROR, + payload: 'An error occurred while moving the issue. Please try again.', }, ], - [], - done, + [{ type: 'undoMoveIssueCard', payload: moveData }], ); }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index ded69fc0e0f..af6d439e294 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -394,41 +394,7 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR); }); - describe('MOVE_ISSUE', () => { - it('updates boardItemsByListId, moving issue between lists', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - 'gid://gitlab/List/2': [], - }; - - const issues = { - 1: mockIssue, - 2: mockIssue2, - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardLists: initialBoardListsState, - boardItems: issues, - }; - - mutations.MOVE_ISSUE(state, { - originalIssue: mockIssue2, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }); - - const updatedListIssues = { - 'gid://gitlab/List/1': [mockIssue.id], - 'gid://gitlab/List/2': [mockIssue2.id], - }; - - expect(state.boardItemsByListId).toEqual(updatedListIssues); - }); - }); - - describe('MOVE_ISSUE_SUCCESS', () => { + describe('MUTATE_ISSUE_SUCCESS', () => { it('updates issue in issues state', () => { const issues = { 436: { id: rawIssue.id }, @@ -439,7 +405,7 @@ describe('Board Store Mutations', () => { boardItems: issues, }; - mutations.MOVE_ISSUE_SUCCESS(state, { + mutations.MUTATE_ISSUE_SUCCESS(state, { issue: rawIssue, }); @@ -447,36 +413,6 @@ describe('Board Store Mutations', () => { }); }); - describe('MOVE_ISSUE_FAILURE', () => { - it('updates boardItemsByListId, reverting moving issue between lists, and sets error message', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id], - 'gid://gitlab/List/2': [mockIssue2.id], - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardLists: initialBoardListsState, - }; - - mutations.MOVE_ISSUE_FAILURE(state, { - originalIssue: mockIssue2, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - originalIndex: 1, - }); - - const updatedListIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - 'gid://gitlab/List/2': [], - }; - - expect(state.boardItemsByListId).toEqual(updatedListIssues); - expect(state.error).toEqual('An error occurred while moving the issue. Please try again.'); - }); - }); - describe('UPDATE_BOARD_ITEM', () => { it('updates the given issue in state.boardItems', () => { const updatedIssue = { id: 'some_gid', foo: 'bar' }; diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 90faf98c871..a339b7f8804 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -56,7 +56,7 @@ describe('diffs/components/app', () => { endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, endpointBatch: `${TEST_HOST}/diff/endpointBatch`, endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`, - endpointCodequality: `${TEST_HOST}/diff/endpointCodequality`, + endpointCodequality: '', projectPath: 'namespace/project', currentUser: {}, changesEmptyStateIllustration: '', @@ -143,19 +143,11 @@ describe('diffs/components/app', () => { }); describe('codequality diff', () => { - it('fetches code quality data when endpoint is provided', () => { + it('does not fetch code quality data on FOSS', async () => { createComponent(); jest.spyOn(wrapper.vm, 'fetchCodequality'); wrapper.vm.fetchData(false); - expect(wrapper.vm.fetchCodequality).toHaveBeenCalled(); - }); - - it('does not fetch code quality data when endpoint is blank', async () => { - createComponent({ endpointCodequality: '' }); - jest.spyOn(wrapper.vm, 'fetchCodequality'); - wrapper.vm.fetchData(false); - expect(wrapper.vm.fetchCodequality).not.toHaveBeenCalled(); }); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 1861de85ca9..f46a42fae7a 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -17,9 +17,6 @@ import { fetchDiffFilesBatch, fetchDiffFilesMeta, fetchCoverageFiles, - clearEtagPoll, - stopCodequalityPolling, - fetchCodequality, assignDiscussionsToDiff, removeDiscussionsFromDiff, startRenderDiffsQueue, @@ -101,7 +98,6 @@ describe('DiffsStoreActions', () => { const endpointMetadata = '/diffs/set/endpoint/metadata'; const endpointBatch = '/diffs/set/endpoint/batch'; const endpointCoverage = '/diffs/set/coverage_reports'; - const endpointCodequality = '/diffs/set/codequality_diff'; const projectPath = '/root/project'; const dismissEndpoint = '/-/user_callouts'; const showSuggestPopover = false; @@ -113,7 +109,6 @@ describe('DiffsStoreActions', () => { endpointBatch, endpointMetadata, endpointCoverage, - endpointCodequality, projectPath, dismissEndpoint, showSuggestPopover, @@ -123,7 +118,6 @@ describe('DiffsStoreActions', () => { endpointBatch: '', endpointMetadata: '', endpointCoverage: '', - endpointCodequality: '', projectPath: '', dismissEndpoint: '', showSuggestPopover: true, @@ -136,7 +130,6 @@ describe('DiffsStoreActions', () => { endpointMetadata, endpointBatch, endpointCoverage, - endpointCodequality, projectPath, dismissEndpoint, showSuggestPopover, @@ -306,47 +299,6 @@ describe('DiffsStoreActions', () => { }); }); - describe('fetchCodequality', () => { - let mock; - const endpointCodequality = '/fetch'; - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - stopCodequalityPolling(); - clearEtagPoll(); - }); - - it('should commit SET_CODEQUALITY_DATA with received response', (done) => { - const data = { - files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] }, - }; - - mock.onGet(endpointCodequality).reply(200, { data }); - - testAction( - fetchCodequality, - {}, - { endpointCodequality }, - [{ type: types.SET_CODEQUALITY_DATA, payload: { data } }], - [], - done, - ); - }); - - it('should show flash on API error', (done) => { - mock.onGet(endpointCodequality).reply(400); - - testAction(fetchCodequality, {}, { endpointCodequality }, [], [], () => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong')); - done(); - }); - }); - }); - describe('setHighlightedRow', () => { it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => { testAction(setHighlightedRow, 'ABC_123', {}, [ diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js index aac4909b151..2e3a66d5b01 100644 --- a/spec/frontend/diffs/store/getters_spec.js +++ b/spec/frontend/diffs/store/getters_spec.js @@ -376,26 +376,6 @@ describe('Diffs Module Getters', () => { }); }); - describe('fileCodequalityDiff', () => { - beforeEach(() => { - Object.assign(localState.codequalityDiff, { - files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] }, - }); - }); - - it('returns empty array when no codequality data is available', () => { - Object.assign(localState.codequalityDiff, {}); - - expect(getters.fileCodequalityDiff(localState)('test.js')).toEqual([]); - }); - - it('returns array when codequality data is available for given file', () => { - expect(getters.fileCodequalityDiff(localState)('app.js')).toEqual([ - { line: 1, description: 'Unexpected alert.', severity: 'minor' }, - ]); - }); - }); - describe('suggestionCommitMessage', () => { let rootState; diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index eb31724ee45..b549ca42634 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -115,19 +115,6 @@ describe('DiffsStoreMutations', () => { }); }); - describe('SET_CODEQUALITY_DATA', () => { - it('should set codequality data', () => { - const state = { codequalityDiff: {} }; - const codequality = { - files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] }, - }; - - mutations[types.SET_CODEQUALITY_DATA](state, codequality); - - expect(state.codequalityDiff).toEqual(codequality); - }); - }); - describe('SET_DIFF_VIEW_TYPE', () => { it('should set diff view type properly', () => { const state = {}; diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js index 60026f69b84..93bc8faa51b 100644 --- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js @@ -11,10 +11,15 @@ const dropdownPath = 'path.json'; describe('Pipelines stage component', () => { let wrapper; let mock; + let glTooltipDirectiveMock; const createComponent = (props = {}) => { + glTooltipDirectiveMock = jest.fn(); wrapper = mount(PipelineStage, { attachTo: document.body, + directives: { + GlTooltip: glTooltipDirectiveMock, + }, propsData: { stage: { status: { @@ -62,6 +67,10 @@ describe('Pipelines stage component', () => { createComponent(); }); + it('sets up the tooltip to not have a show delay animation', () => { + expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true); + }); + it('should render a dropdown with the status icon', () => { expect(findDropdown().exists()).toBe(true); expect(findDropdownToggle().exists()).toBe(true); diff --git a/spec/serializers/admin/user_entity_spec.rb b/spec/serializers/admin/user_entity_spec.rb index 42efe0eec54..5ab5210d3e1 100644 --- a/spec/serializers/admin/user_entity_spec.rb +++ b/spec/serializers/admin/user_entity_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Admin::UserEntity do subject { entity.as_json&.keys } it 'exposes correct attributes' do - is_expected.to contain_exactly( + is_expected.to include( :id, :name, :created_at, diff --git a/spec/serializers/admin/user_serializer_spec.rb b/spec/serializers/admin/user_serializer_spec.rb index 53a9457409c..ed78ea67bd1 100644 --- a/spec/serializers/admin/user_serializer_spec.rb +++ b/spec/serializers/admin/user_serializer_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Admin::UserSerializer do context 'when there is a single object provided' do it 'contains important elements for the admin user table' do - is_expected.to contain_exactly( + is_expected.to include( :id, :name, :created_at, diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 2ae1edcc804..83c6373c335 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -120,30 +120,6 @@ RSpec.describe Issues::CreateService do described_class.new(project, user, opts).execute end - context 'with issue_perform_after_creation_tasks_async feature disabled' do - before do - stub_feature_flags(issue_perform_after_creation_tasks_async: false) - end - - it 'calls Issues::AfterCreateService' do - expect_next(::Issues::AfterCreateService, project, user).to receive(:execute) - - described_class.new(project, user, opts).execute - end - end - - context 'with issue_perform_after_creation_tasks_async feature enabled' do - before do - stub_feature_flags(issue_perform_after_creation_tasks_async: true) - end - - it 'does not call Issues::AfterCreateService' do - expect(::Issues::AfterCreateService).not_to receive(:new) - - described_class.new(project, user, opts).execute - end - end - context 'when label belongs to project group' do let(:group) { create(:group) } let(:group_labels) { create_pair(:group_label, group: group) } diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb index 364dd98ee44..35b83c3bee8 100644 --- a/spec/workers/new_issue_worker_spec.rb +++ b/spec/workers/new_issue_worker_spec.rb @@ -83,29 +83,11 @@ RSpec.describe NewIssueWorker do worker.perform(issue.id, user.id) end - context 'with issue_perform_after_creation_tasks_async feature disabled' do - before do - stub_feature_flags(issue_perform_after_creation_tasks_async: false) - end + it 'calls Issues::AfterCreateService' do + expect_next(::Issues::AfterCreateService) + .to receive(:execute) - it 'does not call Issues::AfterCreateService' do - expect(::Issues::AfterCreateService).not_to receive(:execute) - - worker.perform(issue.id, user.id) - end - end - - context 'with issue_perform_after_creation_tasks_async feature enabled' do - before do - stub_feature_flags(issue_perform_after_creation_tasks_async: true) - end - - it 'calls Issues::AfterCreateService' do - expect_next(::Issues::AfterCreateService) - .to receive(:execute) - - worker.perform(issue.id, user.id) - end + worker.perform(issue.id, user.id) end end end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100755..100644 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100755..100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |