diff options
| author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-14 09:08:46 +0000 |
|---|---|---|
| committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-14 09:08:46 +0000 |
| commit | 6035fcc36ead3b415fa2422b0204a795a70f3e2f (patch) | |
| tree | 011ffa756aa74a83dd1b6d5da5edb0380f4c52a2 | |
| parent | 4a159b9f98bf1c1a62035ea42e8ba56cafb48d98 (diff) | |
| download | gitlab-ce-6035fcc36ead3b415fa2422b0204a795a70f3e2f.tar.gz | |
Add latest changes from gitlab-org/gitlab@master
61 files changed, 779 insertions, 292 deletions
diff --git a/.gitlab/ci/dast.gitlab-ci.yml b/.gitlab/ci/dast.gitlab-ci.yml index 93f64930822..33778b9cbd0 100644 --- a/.gitlab/ci/dast.gitlab-ci.yml +++ b/.gitlab/ci/dast.gitlab-ci.yml @@ -28,6 +28,8 @@ # Help pages are excluded from scan as they are static pages. # profile/two_factor_auth is excluded from scan to prevent 2FA from being turned on from user profile, which will reduce coverage. - 'export DAST_AUTH_EXCLUDE_URLS="${DAST_WEBSITE}/help/.*,${DAST_WEBSITE}/profile/two_factor_auth,${DAST_WEBSITE}/users/sign_out"' + # Exclude the automatically generated monitoring project from being tested due to https://gitlab.com/gitlab-org/gitlab/-/issues/260362 + - 'DAST_AUTH_EXCLUDE_URLS="${DAST_AUTH_EXCLUDE_URLS},https://.*\.gitlab-review\.app/gitlab-instance-(administrators-)?[a-zA-Z0-9]{8}/.*"' - enable_rule () { read all_rules; rule=$1; echo $all_rules | sed -r "s/(,)?$rule(,)?/\1-1\2/" ; } # Sort ids in DAST_RULES ascendingly, which is required when using DAST_RULES as argument to enable_rule - 'DAST_RULES=$(echo $DAST_RULES | tr "," "\n" | sort -n | paste -sd ",")' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 84c4fe93ed8..8c45975eeca 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1141,19 +1141,6 @@ Rails/SaveBang: - 'spec/services/notification_recipients/build_service_spec.rb' - 'spec/services/notification_service_spec.rb' - 'spec/services/packages/conan/create_package_file_service_spec.rb' - - 'spec/services/projects/after_rename_service_spec.rb' - - 'spec/services/projects/autocomplete_service_spec.rb' - - 'spec/services/projects/create_service_spec.rb' - - 'spec/services/projects/destroy_service_spec.rb' - - 'spec/services/projects/fork_service_spec.rb' - - 'spec/services/projects/hashed_storage/base_attachment_service_spec.rb' - - 'spec/services/projects/move_access_service_spec.rb' - - 'spec/services/projects/move_project_group_links_service_spec.rb' - - 'spec/services/projects/overwrite_project_service_spec.rb' - - 'spec/services/projects/propagate_service_template_spec.rb' - - 'spec/services/projects/unlink_fork_service_spec.rb' - - 'spec/services/projects/update_pages_service_spec.rb' - - 'spec/services/projects/update_service_spec.rb' - 'spec/services/reset_project_cache_service_spec.rb' - 'spec/services/resource_events/change_milestone_service_spec.rb' - 'spec/services/system_hooks_service_spec.rb' diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index eea81b729f9..6b7b0c2e28d 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -2,11 +2,24 @@ import { sortBy } from 'lodash'; import ListIssue from 'ee_else_ce/boards/models/issue'; import { ListType } from './constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import boardsStore from '~/boards/stores/boards_store'; export function getMilestone() { return null; } +export function formatBoardLists(lists) { + const formattedLists = lists.nodes.map(list => + boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }), + ); + return formattedLists.reduce((map, list) => { + return { + ...map, + [list.id]: list, + }; + }, {}); +} + export function formatIssue(issue) { return new ListIssue({ ...issue, @@ -62,6 +75,13 @@ export function fullBoardId(boardId) { return `gid://gitlab/Board/${boardId}`; } +export function fullLabelId(label) { + if (label.project_id !== null) { + return `gid://gitlab/ProjectLabel/${label.id}`; + } + return `gid://gitlab/GroupLabel/${label.id}`; +} + export function moveIssueListHelper(issue, fromList, toList) { if (toList.type === ListType.label) { issue.addLabel(toList.label); @@ -85,4 +105,5 @@ export default { formatIssue, formatListIssues, fullBoardId, + fullLabelId, }; diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index c7b3da0e672..2515f471379 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -1,5 +1,6 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; +import { sortBy } from 'lodash'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; import { GlAlert } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -30,7 +31,9 @@ export default { ...mapState(['boardLists', 'error']), ...mapGetters(['isSwimlanesOn']), boardListsToUse() { - return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists; + const lists = + this.glFeatures.graphqlBoardLists || this.isSwimlanesOn ? this.boardLists : this.lists; + return sortBy([...Object.values(lists)], 'position'); }, }, mounted() { @@ -68,7 +71,7 @@ export default { <template v-else> <epics-swimlanes ref="swimlanes" - :lists="boardLists" + :lists="boardListsToUse" :can-admin-list="canAdminList" :disabled="disabled" /> diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index e2600883e89..0024438f6e4 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -34,7 +34,7 @@ export default { referencing a List Model class. Reactivity only applies to plain JS objects */ if (this.glFeatures.graphqlBoardLists) { - return this.boardLists.find(({ id }) => id === this.activeId); + return this.boardLists[this.activeId]; } return boardsStore.state.lists.find(({ id }) => id === this.activeId); }, diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 2e356f1353a..c8926c5ef2a 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -6,8 +6,14 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from '~/flash'; import CreateLabelDropdown from '../../create_label'; import boardsStore from '../stores/boards_store'; +import { fullLabelId } from '../boards_util'; +import store from '~/boards/stores'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; +function shouldCreateListGraphQL(label) { + return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label)); +} + $(document) .off('created.label') .on('created.label', (e, label, addNewList) => { @@ -15,16 +21,20 @@ $(document) return; } - boardsStore.new({ - title: label.title, - position: boardsStore.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, + if (shouldCreateListGraphQL(label)) { + store.dispatch('createList', { labelId: fullLabelId(label) }); + } else { + boardsStore.new({ title: label.title, - color: label.color, - }, - }); + position: boardsStore.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color, + }, + }); + } }); export default function initNewListDropdown() { @@ -74,7 +84,9 @@ export default function initNewListDropdown() { const label = options.selectedObj; e.preventDefault(); - if (!boardsStore.findListByLabelId(label.id)) { + if (shouldCreateListGraphQL(label)) { + store.dispatch('createList', { labelId: fullLabelId(label) }); + } else if (!boardsStore.findListByLabelId(label.id)) { boardsStore.new({ title: label.title, position: boardsStore.state.lists.length - 2, diff --git a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql index dcfe69222a0..48420b349ae 100644 --- a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql +++ b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql @@ -1,7 +1,21 @@ -#import "./board_list.fragment.graphql" +#import "ee_else_ce/boards/queries/board_list.fragment.graphql" -mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean) { - boardListCreate(input: { boardId: $boardId, backlog: $backlog }) { +mutation CreateBoardList( + $boardId: BoardID! + $backlog: Boolean + $labelId: LabelID + $milestoneId: MilestoneID + $assigneeId: UserID +) { + boardListCreate( + input: { + boardId: $boardId + backlog: $backlog + labelId: $labelId + milestoneId: $milestoneId + assigneeId: $assigneeId + } + ) { list { ...BoardListFragment } diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 292f4d3307a..a1fd05f2a3b 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,13 +1,17 @@ import Cookies from 'js-cookie'; -import { sortBy, pick } from 'lodash'; -import createFlash from '~/flash'; +import { pick } from 'lodash'; import { __ } from '~/locale'; import { parseBoolean } from '~/lib/utils/common_utils'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { BoardType, ListType, inactiveId } from '~/boards/constants'; import * as types from './mutation_types'; -import { formatListIssues, fullBoardId, formatListsPageInfo } from '../boards_util'; +import { + formatBoardLists, + formatListIssues, + fullBoardId, + formatListsPageInfo, +} from '../boards_util'; import boardStore from '~/boards/stores/boards_store'; import listsIssuesQuery from '../queries/lists_issues.query.graphql'; @@ -71,38 +75,29 @@ export default { variables, }) .then(({ data }) => { - let { lists } = data[boardType]?.board; - // Temporarily using positioning logic from boardStore - lists = lists.nodes.map(list => - boardStore.updateListPosition({ - ...list, - doNotFetchIssues: true, - }), - ); - commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position')); + const { lists } = data[boardType]?.board; + commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists)); // Backlog list needs to be created if it doesn't exist - if (!lists.find(l => l.type === ListType.backlog)) { + if (!lists.nodes.find(l => l.listType === ListType.backlog)) { dispatch('createList', { backlog: true }); } dispatch('showWelcomeList'); }) - .catch(() => { - createFlash( - __('An error occurred while fetching the board lists. Please reload the page.'), - ); - }); + .catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE)); }, - // This action only supports backlog list creation at this stage - // Future iterations will add the ability to create other list types - createList: ({ state, commit, dispatch }, { backlog = false }) => { + createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => { const { boardId } = state.endpoints; + gqlClient .mutate({ mutation: createBoardListMutation, variables: { boardId: fullBoardId(boardId), backlog, + labelId, + milestoneId, + assigneeId, }, }) .then(({ data }) => { @@ -113,16 +108,15 @@ export default { dispatch('addList', list); } }) - .catch(() => { - commit(types.CREATE_LIST_FAILURE); - }); + .catch(() => commit(types.CREATE_LIST_FAILURE)); }, - addList: ({ state, commit }, list) => { - const lists = state.boardLists; + addList: ({ commit }, list) => { // Temporarily using positioning logic from boardStore - lists.push(boardStore.updateListPosition({ ...list, doNotFetchIssues: true })); - commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position')); + commit( + types.RECEIVE_ADD_LIST_SUCCESS, + boardStore.updateListPosition({ ...list, doNotFetchIssues: true }), + ); }, showWelcomeList: ({ state, dispatch }) => { @@ -130,7 +124,9 @@ export default { return; } if ( - state.boardLists.find(list => list.type !== ListType.backlog && list.type !== ListType.closed) + Object.entries(state.boardLists).find( + ([, list]) => list.type !== ListType.backlog && list.type !== ListType.closed, + ) ) { return; } @@ -152,13 +148,16 @@ export default { notImplemented(); }, - moveList: ({ state, commit, dispatch }, { listId, newIndex, adjustmentValue }) => { + moveList: ( + { state, commit, dispatch }, + { listId, replacedListId, newIndex, adjustmentValue }, + ) => { const { boardLists } = state; - const backupList = [...boardLists]; - const movedList = boardLists.find(({ id }) => id === listId); + const backupList = { ...boardLists }; + const movedList = boardLists[listId]; const newPosition = newIndex - 1; - const listAtNewIndex = boardLists[newIndex]; + const listAtNewIndex = boardLists[replacedListId]; movedList.position = newPosition; listAtNewIndex.position += adjustmentValue; diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index 3688476dc5f..9279d18ff1e 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -1,3 +1,4 @@ +import { find } from 'lodash'; import { inactiveId } from '../constants'; export default { @@ -22,4 +23,16 @@ export default { getActiveIssue: state => { return state.issues[state.activeId] || {}; }, + + getListByLabelId: state => labelId => { + return find(state.boardLists, l => l.label?.id === labelId); + }, + + getListByTitle: state => title => { + return find(state.boardLists, l => l.title === title); + }, + + shouldUseGraphQL: () => { + return gon?.features?.graphqlBoardLists; + }, }; diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 8bf8fb2e7b4..09ab08062df 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -3,6 +3,7 @@ export const SET_FILTERS = 'SET_FILTERS'; export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS'; export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE'; export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; +export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE'; export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST'; export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST'; export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 773f4a32c1d..0c7dbc0d2ef 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { sortBy, pull, union } from 'lodash'; +import { pull, union } from 'lodash'; import { formatIssue, moveIssueListHelper } from '../boards_util'; import * as mutationTypes from './mutation_types'; import { s__ } from '~/locale'; @@ -10,16 +10,10 @@ const notImplemented = () => { throw new Error('Not implemented!'); }; -const getListById = ({ state, listId }) => { - const listIndex = state.boardLists.findIndex(l => l.id === listId); - const list = state.boardLists[listIndex]; - return { listIndex, list }; -}; - export const removeIssueFromList = ({ state, listId, issueId }) => { Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId)); - const { listIndex, list } = getListById({ state, listId }); - Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize - 1 }); + const list = state.boardLists[listId]; + Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize - 1 }); }; export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => { @@ -32,8 +26,8 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter } listIssues.splice(newIndex, 0, issueId); Vue.set(state.issuesByListId, listId, listIssues); - const { listIndex, list } = getListById({ state, listId }); - Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize + 1 }); + const list = state.boardLists[listId]; + Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize + 1 }); }; export default { @@ -49,6 +43,12 @@ export default { state.boardLists = lists; }, + [mutationTypes.RECEIVE_BOARD_LISTS_FAILURE]: state => { + state.error = s__( + 'Boards|An error occurred while fetching the board lists. Please reload the page.', + ); + }, + [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) { state.activeId = id; state.sidebarType = sidebarType; @@ -66,8 +66,8 @@ export default { notImplemented(); }, - [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: () => { - notImplemented(); + [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: (state, list) => { + Vue.set(state.boardLists, list.id, list); }, [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => { @@ -76,10 +76,8 @@ export default { [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => { const { boardLists } = state; - const movedListIndex = state.boardLists.findIndex(l => l.id === movedList.id); - Vue.set(boardLists, movedListIndex, movedList); - Vue.set(boardLists, movedListIndex.position + 1, listAtNewIndex); - Vue.set(state, 'boardLists', sortBy(boardLists, 'position')); + Vue.set(boardLists, movedList.id, movedList); + Vue.set(boardLists, listAtNewIndex.id, listAtNewIndex); }, [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => { @@ -156,8 +154,8 @@ export default { state, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }, ) => { - const fromList = state.boardLists.find(l => l.id === fromListId); - const toList = state.boardLists.find(l => l.id === toListId); + const fromList = state.boardLists[fromListId]; + const toList = state.boardLists[toListId]; const issue = moveIssueListHelper(originalIssue, fromList, toList); Vue.set(state.issues, issue.id, issue); diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index ff1330f00f4..b91c09f8051 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -8,7 +8,7 @@ export default () => ({ isShowingLabels: true, activeId: inactiveId, sidebarType: '', - boardLists: [], + boardLists: {}, listsFlags: {}, issuesByListId: {}, pageInfoByListId: {}, diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js index a37838694ec..ff7f734f998 100644 --- a/app/assets/javascripts/breadcrumb.js +++ b/app/assets/javascripts/breadcrumb.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { hide } from '~/tooltips'; export const addTooltipToEl = el => { const textEl = el.querySelector('.js-breadcrumb-item-text'); @@ -23,9 +24,11 @@ export default () => { topLevelLinks.forEach(el => addTooltipToEl(el)); $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => { - $('.js-breadcrumbs-collapsed-expander', e.currentTarget) - .toggleClass('open') - .tooltip('hide'); + const $el = $('.js-breadcrumbs-collapsed-expander', e.currentTarget); + + $el.toggleClass('open'); + + hide($el); }); } }; diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index fb6a91abcdc..eab46d146da 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -3,6 +3,7 @@ import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui'; import VueDraggable from 'vuedraggable'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { s__, sprintf } from '~/locale'; +import { getFilename } from '~/lib/utils/file_upload'; import UploadButton from '../components/upload/button.vue'; import DeleteButton from '../components/delete_button.vue'; import Design from '../components/list/item.vue'; @@ -31,7 +32,7 @@ import { isValidDesignFile, moveDesignOptimisticResponse, } from '../utils/design_management_utils'; -import { getFilename } from '~/lib/utils/file_upload'; +import { trackDesignCreate, trackDesignUpdate } from '../utils/tracking'; import { DESIGNS_ROUTE_NAME } from '../router/constants'; const MAXIMUM_FILE_UPLOAD_LIMIT = 10; @@ -186,6 +187,7 @@ export default { updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody); }, onUploadDesignDone(res) { + // display any warnings, if necessary const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || []; const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles); if (skippedWarningMessage) { @@ -196,7 +198,19 @@ export default { if (!this.isLatestVersion) { this.$router.push({ name: DESIGNS_ROUTE_NAME }); } + + // reset state this.resetFilesToBeSaved(); + this.trackUploadDesign(res); + }, + trackUploadDesign(res) { + (res?.data?.designManagementUpload?.designs || []).forEach(design => { + if (design.event === 'CREATION') { + trackDesignCreate(); + } else if (design.event === 'MODIFICATION') { + trackDesignUpdate(); + } + }); }, onUploadDesignError() { this.resetFilesToBeSaved(); diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js index 49fa306914c..4a39268c38b 100644 --- a/app/assets/javascripts/design_management/utils/tracking.js +++ b/app/assets/javascripts/design_management/utils/tracking.js @@ -1,9 +1,16 @@ import Tracking from '~/tracking'; // Tracking Constants -const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0'; -const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; -const DESIGN_TRACKING_EVENT_NAME = 'view_design'; +const DESIGN_TRACKING_CONTEXT_SCHEMAS = { + VIEW_DESIGN_SCHEMA: 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0', +}; +const DESIGN_TRACKING_EVENTS = { + VIEW_DESIGN: 'view_design', + CREATE_DESIGN: 'create_design', + UPDATE_DESIGN: 'update_design', +}; + +export const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; export function trackDesignDetailView( referer = '', @@ -11,10 +18,11 @@ export function trackDesignDetailView( designVersion = 1, latestVersion = false, ) { - Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, { - label: DESIGN_TRACKING_EVENT_NAME, + const eventName = DESIGN_TRACKING_EVENTS.VIEW_DESIGN; + Tracking.event(DESIGN_TRACKING_PAGE_NAME, eventName, { + label: eventName, context: { - schema: DESIGN_TRACKING_CONTEXT_SCHEMA, + schema: DESIGN_TRACKING_CONTEXT_SCHEMAS.VIEW_DESIGN_SCHEMA, data: { 'design-version-number': designVersion, 'design-is-current-version': latestVersion, @@ -24,3 +32,11 @@ export function trackDesignDetailView( }, }); } + +export function trackDesignCreate() { + return Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENTS.CREATE_DESIGN); +} + +export function trackDesignUpdate() { + return Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENTS.UPDATE_DESIGN); +} diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 8e0af018b61..3e9962a4e72 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,7 +1,5 @@ import Project from './project'; import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; -document.addEventListener('DOMContentLoaded', () => { - new Project(); // eslint-disable-line no-new - new ShortcutsNavigation(); // eslint-disable-line no-new -}); +new Project(); // eslint-disable-line no-new +new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 840c21db20f..4b1139d2354 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -6,7 +6,6 @@ @import '@gitlab/at.js/dist/css/jquery.atwho'; @import 'dropzone/dist/basic'; @import 'select2'; -@import 'cropper/dist/cropper'; // GitLab UI framework @import 'framework'; diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb index 4086ca46a60..f4a50115ee6 100644 --- a/app/graphql/types/ci/detailed_status_type.rb +++ b/app/graphql/types/ci/detailed_status_type.rb @@ -6,22 +6,22 @@ module Types class DetailedStatusType < BaseObject graphql_name 'DetailedStatus' - field :group, GraphQL::STRING_TYPE, null: false, + field :group, GraphQL::STRING_TYPE, null: true, description: 'Group of the status' - field :icon, GraphQL::STRING_TYPE, null: false, + field :icon, GraphQL::STRING_TYPE, null: true, description: 'Icon of the status' - field :favicon, GraphQL::STRING_TYPE, null: false, + field :favicon, GraphQL::STRING_TYPE, null: true, description: 'Favicon of the status' field :details_path, GraphQL::STRING_TYPE, null: true, description: 'Path of the details for the status' - field :has_details, GraphQL::BOOLEAN_TYPE, null: false, + field :has_details, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if the status has further details', method: :has_details? - field :label, GraphQL::STRING_TYPE, null: false, + field :label, GraphQL::STRING_TYPE, null: true, description: 'Label of the status' - field :text, GraphQL::STRING_TYPE, null: false, + field :text, GraphQL::STRING_TYPE, null: true, description: 'Text of the status' - field :tooltip, GraphQL::STRING_TYPE, null: false, + field :tooltip, GraphQL::STRING_TYPE, null: true, description: 'Tooltip associated with the status', method: :status_tooltip field :action, Types::Ci::StatusActionType, null: true, diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index bed0e74a920..0ee1ad47b62 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -13,6 +13,8 @@ module Types field :detailed_status, Types::Ci::DetailedStatusType, null: true, description: 'Detailed status of the job', resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } + field :scheduled_at, Types::TimeType, null: true, + description: 'Schedule for the build' end end end diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb index 5082245dda9..be42c6dd57f 100644 --- a/app/serializers/label_entity.rb +++ b/app/serializers/label_entity.rb @@ -7,7 +7,7 @@ class LabelEntity < Grape::Entity expose :color expose :description expose :group_id - expose :project_id + expose :project_id, if: ->(label, _) { !label.is_a?(GlobalLabel) } expose :template expose :text_color expose :created_at diff --git a/app/serializers/label_serializer.rb b/app/serializers/label_serializer.rb index 25b9f7de243..b09592da67f 100644 --- a/app/serializers/label_serializer.rb +++ b/app/serializers/label_serializer.rb @@ -4,6 +4,6 @@ class LabelSerializer < BaseSerializer entity LabelEntity def represent_appearance(resource) - represent(resource, { only: [:id, :title, :color, :text_color] }) + represent(resource, { only: [:id, :title, :color, :text_color, :project_id] }) end end diff --git a/changelogs/unreleased/241663-incident-sla-cron-job.yml b/changelogs/unreleased/241663-incident-sla-cron-job.yml new file mode 100644 index 00000000000..1c94d76a104 --- /dev/null +++ b/changelogs/unreleased/241663-incident-sla-cron-job.yml @@ -0,0 +1,5 @@ +--- +title: Schedule adding "Missed SLA" label to issues +merge_request: 44546 +author: +type: added diff --git a/changelogs/unreleased/247489-update-create-column-from-to-also-copy-constraints-take-2.yml b/changelogs/unreleased/247489-update-create-column-from-to-also-copy-constraints-take-2.yml new file mode 100644 index 00000000000..d1e0bccd923 --- /dev/null +++ b/changelogs/unreleased/247489-update-create-column-from-to-also-copy-constraints-take-2.yml @@ -0,0 +1,5 @@ +--- +title: Add migration helpers for copying check constraints +merge_request: 44777 +author: +type: other diff --git a/changelogs/unreleased/design-tracking-create-update.yml b/changelogs/unreleased/design-tracking-create-update.yml new file mode 100644 index 00000000000..e55684ebf64 --- /dev/null +++ b/changelogs/unreleased/design-tracking-create-update.yml @@ -0,0 +1,5 @@ +--- +title: Add product analytics for design created and modified events +merge_request: 44129 +author: +type: added diff --git a/changelogs/unreleased/lm-add-scheduled-jobs.yml b/changelogs/unreleased/lm-add-scheduled-jobs.yml new file mode 100644 index 00000000000..a48cffe4617 --- /dev/null +++ b/changelogs/unreleased/lm-add-scheduled-jobs.yml @@ -0,0 +1,5 @@ +--- +title: 'GraphQL: Adds scheduledAt to CiJob' +merge_request: 44054 +author: +type: added diff --git a/changelogs/unreleased/lm-update-status-null-fields.yml b/changelogs/unreleased/lm-update-status-null-fields.yml new file mode 100644 index 00000000000..998f6a02213 --- /dev/null +++ b/changelogs/unreleased/lm-update-status-null-fields.yml @@ -0,0 +1,5 @@ +--- +title: 'GraphQL: Changes fields in detailedStatus to be nullable' +merge_request: 45072 +author: +type: changed diff --git a/changelogs/unreleased/mb_rails_save_bang_fix4.yml b/changelogs/unreleased/mb_rails_save_bang_fix4.yml new file mode 100644 index 00000000000..e4e48e26361 --- /dev/null +++ b/changelogs/unreleased/mb_rails_save_bang_fix4.yml @@ -0,0 +1,5 @@ +--- +title: Fix Rails/SaveBang offenses in spec/services/projects/* +merge_request: 44980 +author: matthewbried +type: other diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d8fa6f0179e..baf728fb0dc 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -574,6 +574,9 @@ Gitlab.ee do Settings.cron_jobs['historical_data_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['historical_data_worker']['cron'] ||= '0 12 * * *' Settings.cron_jobs['historical_data_worker']['job_class'] = 'HistoricalDataWorker' + Settings.cron_jobs['incident_sla_exceeded_check_worker'] ||= Settingslogic.new({}) + Settings.cron_jobs['incident_sla_exceeded_check_worker']['cron'] ||= '*/2 * * * *' + Settings.cron_jobs['incident_sla_exceeded_check_worker']['job_class'] = 'IncidentManagement::IncidentSlaExceededCheckWorker' Settings.cron_jobs['import_software_licenses_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['import_software_licenses_worker']['cron'] ||= '0 3 * * 0' Settings.cron_jobs['import_software_licenses_worker']['job_class'] = 'ImportSoftwareLicensesWorker' diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 364479da209..52ec38c8ef6 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -142,6 +142,8 @@ - 2 - - incident_management - 2 +- - incident_management_apply_incident_sla_exceeded_label + - 1 - - invalid_gpg_signature_update - 2 - - irker diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index fd26d567574..aedde4928bb 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2127,6 +2127,11 @@ type CiJob { """ last: Int ): CiJobConnection + + """ + Schedule for the build + """ + scheduledAt: Time } """ @@ -5328,37 +5333,37 @@ type DetailedStatus { """ Favicon of the status """ - favicon: String! + favicon: String """ Group of the status """ - group: String! + group: String """ Indicates if the status has further details """ - hasDetails: Boolean! + hasDetails: Boolean """ Icon of the status """ - icon: String! + icon: String """ Label of the status """ - label: String! + label: String """ Text of the status """ - text: String! + text: String """ Tooltip associated with the status """ - tooltip: String! + tooltip: String } input DiffImagePositionInput { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 7e6266ca26c..f44eda3709f 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -5677,6 +5677,20 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "scheduledAt", + "description": "Schedule for the build", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -14560,13 +14574,9 @@ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -14578,13 +14588,9 @@ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -14596,13 +14602,9 @@ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -14614,13 +14616,9 @@ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -14632,13 +14630,9 @@ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -14650,13 +14644,9 @@ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -14668,13 +14658,9 @@ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 51bc2176102..62ccf4a633b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -334,6 +334,7 @@ Represents the total number of issues and their weights for a particular day. | ----- | ---- | ----------- | | `detailedStatus` | DetailedStatus | Detailed status of the job | | `name` | String | Name of the job | +| `scheduledAt` | Time | Schedule for the build | ### CiStage @@ -855,13 +856,13 @@ Autogenerated return type of DestroySnippet. | ----- | ---- | ----------- | | `action` | StatusAction | Action information for the status. This includes method, button title, icon, path, and title | | `detailsPath` | String | Path of the details for the status | -| `favicon` | String! | Favicon of the status | -| `group` | String! | Group of the status | -| `hasDetails` | Boolean! | Indicates if the status has further details | -| `icon` | String! | Icon of the status | -| `label` | String! | Label of the status | -| `text` | String! | Text of the status | -| `tooltip` | String! | Tooltip associated with the status | +| `favicon` | String | Favicon of the status | +| `group` | String | Group of the status | +| `hasDetails` | Boolean | Indicates if the status has further details | +| `icon` | String | Icon of the status | +| `label` | String | Label of the status | +| `text` | String | Text of the status | +| `tooltip` | String | Tooltip associated with the status | ### DiffPosition diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index 9c6bca10abc..e6eda180eb5 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -184,3 +184,12 @@ To quickly see the latest updates on an incident, click un-threaded and ordered chronologically, newest to oldest:  + +### Service Level Agreement countdown timer + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241663) in GitLab 13.5. + +After enabling **Incident SLA** in the Incident Management configuration, newly-created +incidents display a SLA (Service Level Agreement) timer showing the time remaining before +the SLA period expires. If the incident is not closed before the SLA period ends, GitLab +adds a `missed::SLA` label to the incident. diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md index d2e556f3c8d..77b4b22e6a8 100644 --- a/doc/policy/maintenance.md +++ b/doc/policy/maintenance.md @@ -115,9 +115,9 @@ Please see the table below for some examples: | Target version | Your version | Recommended upgrade path | Note | | --------------------- | ------------ | ------------------------ | ---- | -| `13.2.3` | `11.5.0` | `11.5.0` -> `11.11.8` -> `12.0.12` -> `12.10.14` -> `13.0.12` -> `13.2.3` | Four intermediate versions are required: the final `11.11`, `12.0`, and `12.10` releases, plus `13.0`. | -| `13.0.12` | `11.10.8` | `11.10.5` -> `11.11.8` -> `12.0.12` -> `12.10.14` -> `13.0.12` | Three intermediate versions are required: `11.11`, `12.0`, and `12.10`. | -| `12.10.14` | `11.3.4` | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.10.14` | Two intermediate versions are required: `11.11` and `12.0` | +| `13.4.3` | `12.9.2` | `12.9.2` -> `12.10.14` -> `13.0.14` -> `13.4.3` | Two intermediate versions are required: the final `12.10` release, plus `13.0`. | +| `13.2.10` | `11.5.0` | `11.5.0` -> `11.11.8` -> `12.0.12` -> `12.10.14` -> `13.0.14` -> `13.2.10` | Four intermediate versions are required: the final `11.11`, `12.0`, and `12.10` releases, plus `13.0`. | +| `12.10.14` | `11.3.4` | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.10.14` | Two intermediate versions are required: the final `11.11` release and `12.0.12` | | `12.9.5` | `10.4.5` | `10.4.5` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.9.5` | Three intermediate versions are required: `10.8`, `11.11`, and `12.0`, then `12.9.5` | | `12.2.5` | `9.2.6` | `9.2.6` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.2.5` | Four intermediate versions are required: `9.5`, `10.8`, `11.11`, `12.0`, then `12.2`. | | `11.3.4` | `8.13.4` | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version 8, `9.5.10` is the last version in version 9, `10.8.7` is the last version in version 10. | diff --git a/jest.config.base.js b/jest.config.base.js index 95b3e810200..9f611775776 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -13,6 +13,7 @@ module.exports = path => { 'jest-junit', { outputName: './junit_jest.xml', + addFileAttribute: 'true', }, ]); } diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 4e2e1eaf21c..373170c8e12 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1151,6 +1151,64 @@ into similar problems in the future (e.g. when new tables are created). end end + # Copies all check constraints for the old column to the new column. + # + # table - The table containing the columns. + # old - The old column. + # new - The new column. + # schema - The schema the table is defined for + # If it is not provided, then the current_schema is used + def copy_check_constraints(table, old, new, schema: nil) + if transaction_open? + raise 'copy_check_constraints can not be run inside a transaction' + end + + unless column_exists?(table, old) + raise "Column #{old} does not exist on #{table}" + end + + unless column_exists?(table, new) + raise "Column #{new} does not exist on #{table}" + end + + table_with_schema = schema.present? ? "#{schema}.#{table}" : table + + check_constraints_for(table, old, schema: schema).each do |check_c| + validate = !(check_c["constraint_def"].end_with? "NOT VALID") + + # Normalize: + # - Old constraint definitions: + # '(char_length(entity_path) <= 5500)' + # - Definitionss from pg_get_constraintdef(oid): + # 'CHECK ((char_length(entity_path) <= 5500))' + # - Definitions from pg_get_constraintdef(oid, pretty_bool): + # 'CHECK (char_length(entity_path) <= 5500)' + # - Not valid constraints: 'CHECK (...) NOT VALID' + # to a single format that we can use: + # '(char_length(entity_path) <= 5500)' + check_definition = check_c["constraint_def"] + .sub(/^\s*(CHECK)?\s*\({0,2}/, '(') + .sub(/\){0,2}\s*(NOT VALID)?\s*$/, ')') + + constraint_name = begin + if check_definition == "(#{old} IS NOT NULL)" + not_null_constraint_name(table_with_schema, new) + elsif check_definition.start_with? "(char_length(#{old}) <=" + text_limit_name(table_with_schema, new) + else + check_constraint_name(table_with_schema, new, 'copy_check_constraint') + end + end + + add_check_constraint( + table_with_schema, + check_definition.gsub(old.to_s, new.to_s), + constraint_name, + validate: validate + ) + end + end + # Migration Helpers for adding limit to text columns def add_text_limit(table, column, limit, constraint_name: nil, validate: true) add_check_constraint( @@ -1278,6 +1336,37 @@ into similar problems in the future (e.g. when new tables are created). end end + # Returns an ActiveRecord::Result containing the check constraints + # defined for the given column. + # + # If the schema is not provided, then the current_schema is used + def check_constraints_for(table, column, schema: nil) + check_sql = <<~SQL + SELECT + ccu.table_schema as schema_name, + ccu.table_name as table_name, + ccu.column_name as column_name, + con.conname as constraint_name, + pg_get_constraintdef(con.oid) as constraint_def + FROM pg_catalog.pg_constraint con + INNER JOIN pg_catalog.pg_class rel + ON rel.oid = con.conrelid + INNER JOIN pg_catalog.pg_namespace nsp + ON nsp.oid = con.connamespace + INNER JOIN information_schema.constraint_column_usage ccu + ON con.conname = ccu.constraint_name + AND nsp.nspname = ccu.constraint_schema + AND rel.relname = ccu.table_name + WHERE nsp.nspname = #{connection.quote(schema.presence || current_schema)} + AND rel.relname = #{connection.quote(table)} + AND ccu.column_name = #{connection.quote(column)} + AND con.contype = 'c' + ORDER BY constraint_name + SQL + + connection.exec_query(check_sql) + end + def statement_timeout_disabled? # This is a string of the form "100ms" or "0" when disabled connection.select_value('SHOW statement_timeout') == "0" @@ -1357,6 +1446,7 @@ into similar problems in the future (e.g. when new tables are created). copy_indexes(table, old, new) copy_foreign_keys(table, old, new) + copy_check_constraints(table, old, new) end def validate_timestamp_column_name!(column_name) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a1006b2c199..d5c2584ca1b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2851,9 +2851,6 @@ msgstr "" msgid "An error occurred while fetching the Service Desk address." msgstr "" -msgid "An error occurred while fetching the board lists. Please reload the page." -msgstr "" - msgid "An error occurred while fetching the board lists. Please try again." msgstr "" @@ -4185,6 +4182,9 @@ msgstr "" msgid "Boards|An error occurred while fetching the board issues. Please reload the page." msgstr "" +msgid "Boards|An error occurred while fetching the board lists. Please reload the page." +msgstr "" + msgid "Boards|An error occurred while fetching the board swimlanes. Please reload the page." msgstr "" diff --git a/package.json b/package.json index 1fcfe0565d8..f7d77934458 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.171.0", - "@gitlab/ui": "21.27.0", + "@gitlab/ui": "21.28.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-3", "@rails/ujs": "^6.0.3-2", diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 0e5fee9a563..859b95347a6 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -177,16 +177,26 @@ describe('createList', () => { describe('moveList', () => { it('should commit MOVE_LIST mutation and dispatch updateList action', done => { + const initialBoardListsState = { + 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockListsWithModel[1], + }; + const state = { endpoints: { fullPath: 'gitlab-org', boardId: '1' }, boardType: 'group', disabled: false, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, }; testAction( actions.moveList, - { listId: 'gid://gitlab/List/1', newIndex: 1, adjustmentValue: 1 }, + { + listId: 'gid://gitlab/List/1', + replacedListId: 'gid://gitlab/List/2', + newIndex: 1, + adjustmentValue: 1, + }, state, [ { @@ -197,7 +207,11 @@ describe('moveList', () => { [ { type: 'updateList', - payload: { listId: 'gid://gitlab/List/1', position: 0, backupList: mockListsWithModel }, + payload: { + listId: 'gid://gitlab/List/1', + position: 0, + backupList: initialBoardListsState, + }, }, ], done, diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js index 288143a0f21..b987080abab 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -1,6 +1,13 @@ import getters from '~/boards/stores/getters'; import { inactiveId } from '~/boards/constants'; -import { mockIssue, mockIssue2, mockIssues, mockIssuesByListId, issues } from '../mock_data'; +import { + mockIssue, + mockIssue2, + mockIssues, + mockIssuesByListId, + issues, + mockListsWithModel, +} from '../mock_data'; describe('Boards - Getters', () => { describe('getLabelToggleState', () => { @@ -130,4 +137,25 @@ describe('Boards - Getters', () => { ); }); }); + + const boardsState = { + boardLists: { + 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockListsWithModel[1], + }, + }; + + describe('getListByLabelId', () => { + it('returns list for a given label id', () => { + expect(getters.getListByLabelId(boardsState)('gid://gitlab/GroupLabel/121')).toEqual( + mockListsWithModel[1], + ); + }); + }); + + describe('getListByTitle', () => { + it('returns list for a given list title', () => { + expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockListsWithModel[1]); + }); + }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 4e60a78f443..6e53f184bb3 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -2,8 +2,6 @@ import mutations from '~/boards/stores/mutations'; import * as types from '~/boards/stores/mutation_types'; import defaultState from '~/boards/stores/state'; import { - listObj, - listObjDuplicate, mockListsWithModel, mockLists, rawIssue, @@ -22,6 +20,11 @@ const expectNotImplemented = action => { describe('Board Store Mutations', () => { let state; + const initialBoardListsState = { + 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockListsWithModel[1], + }; + beforeEach(() => { state = defaultState(); }); @@ -56,11 +59,19 @@ describe('Board Store Mutations', () => { describe('RECEIVE_BOARD_LISTS_SUCCESS', () => { it('Should set boardLists to state', () => { - const lists = [listObj, listObjDuplicate]; + mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, initialBoardListsState); + + expect(state.boardLists).toEqual(initialBoardListsState); + }); + }); - mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, lists); + describe('RECEIVE_BOARD_LISTS_FAILURE', () => { + it('Should set error in state', () => { + mutations[types.RECEIVE_BOARD_LISTS_FAILURE](state); - expect(state.boardLists).toEqual(lists); + expect(state.error).toEqual( + 'An error occurred while fetching the board lists. Please reload the page.', + ); }); }); @@ -95,7 +106,13 @@ describe('Board Store Mutations', () => { }); describe('RECEIVE_ADD_LIST_SUCCESS', () => { - expectNotImplemented(mutations.RECEIVE_ADD_LIST_SUCCESS); + it('adds list to boardLists state', () => { + mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockListsWithModel[0]); + + expect(state.boardLists).toEqual({ + [mockListsWithModel[0].id]: mockListsWithModel[0], + }); + }); }); describe('RECEIVE_ADD_LIST_ERROR', () => { @@ -106,7 +123,7 @@ describe('Board Store Mutations', () => { it('updates boardLists state with reordered lists', () => { state = { ...state, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, }; mutations.MOVE_LIST(state, { @@ -114,7 +131,10 @@ describe('Board Store Mutations', () => { listAtNewIndex: mockListsWithModel[1], }); - expect(state.boardLists).toEqual([mockListsWithModel[1], mockListsWithModel[0]]); + expect(state.boardLists).toEqual({ + 'gid://gitlab/List/2': mockListsWithModel[1], + 'gid://gitlab/List/1': mockListsWithModel[0], + }); }); }); @@ -122,13 +142,16 @@ describe('Board Store Mutations', () => { it('updates boardLists state with previous order and sets error message', () => { state = { ...state, - boardLists: [mockListsWithModel[1], mockListsWithModel[0]], + boardLists: { + 'gid://gitlab/List/2': mockListsWithModel[1], + 'gid://gitlab/List/1': mockListsWithModel[0], + }, error: undefined, }; - mutations.UPDATE_LIST_FAILURE(state, mockListsWithModel); + mutations.UPDATE_LIST_FAILURE(state, initialBoardListsState); - expect(state.boardLists).toEqual(mockListsWithModel); + expect(state.boardLists).toEqual(initialBoardListsState); expect(state.error).toEqual('An error occurred while updating the list. Please try again.'); }); }); @@ -177,7 +200,7 @@ describe('Board Store Mutations', () => { 'gid://gitlab/List/1': [], }, issues: {}, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, }; const listPageInfo = { @@ -202,7 +225,7 @@ describe('Board Store Mutations', () => { it('sets error message', () => { state = { ...state, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, error: undefined, }; @@ -284,7 +307,7 @@ describe('Board Store Mutations', () => { state = { ...state, issuesByListId: listIssues, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, issues, }; @@ -332,7 +355,7 @@ describe('Board Store Mutations', () => { state = { ...state, issuesByListId: listIssues, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, }; mutations.MOVE_ISSUE_FAILURE(state, { @@ -400,7 +423,7 @@ describe('Board Store Mutations', () => { ...state, issuesByListId: listIssues, issues, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, }; mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 }); diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js index a8b335c2c46..5e41210221b 100644 --- a/spec/frontend/design_management/mock_data/apollo_mock.js +++ b/spec/frontend/design_management/mock_data/apollo_mock.js @@ -51,6 +51,34 @@ export const designListQueryResponse = { }, }; +export const designUploadMutationCreatedResponse = { + data: { + designManagementUpload: { + designs: [ + { + id: '1', + event: 'CREATION', + filename: 'fox_1.jpg', + }, + ], + }, + }, +}; + +export const designUploadMutationUpdatedResponse = { + data: { + designManagementUpload: { + designs: [ + { + id: '1', + event: 'MODIFICATION', + filename: 'fox_1.jpg', + }, + ], + }, + }, +}; + export const permissionsQueryResponse = { data: { project: { diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index e44333a5fce..27a91b11448 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -4,6 +4,7 @@ import VueDraggable from 'vuedraggable'; import VueRouter from 'vue-router'; import { GlEmptyState } from '@gitlab/ui'; import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import Index from '~/design_management/pages/index.vue'; import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql'; import DesignDestroyer from '~/design_management/components/design_destroyer.vue'; @@ -21,6 +22,8 @@ import * as utils from '~/design_management/utils/design_management_utils'; import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants'; import { designListQueryResponse, + designUploadMutationCreatedResponse, + designUploadMutationUpdatedResponse, permissionsQueryResponse, moveDesignMutationResponse, reorderedDesigns, @@ -29,6 +32,7 @@ import { import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql'; import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql'; import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql'; +import { DESIGN_TRACKING_PAGE_NAME } from '~/design_management/utils/tracking'; jest.mock('~/flash.js'); const mockPageEl = { @@ -370,7 +374,7 @@ describe('Design management index page', () => { createComponent({ stubs: { GlEmptyState } }); wrapper.setData({ filesToBeSaved: [{ name: 'test' }] }); - wrapper.vm.onUploadDesignDone(); + wrapper.vm.onUploadDesignDone(designUploadMutationCreatedResponse); return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.filesToBeSaved).toEqual([]); expect(wrapper.vm.isSaving).toBeFalsy(); @@ -482,6 +486,34 @@ describe('Design management index page', () => { expect(createFlash).toHaveBeenCalledWith(message); }); }); + + describe('tracking', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking('_category_', undefined, jest.spyOn); + + createComponent({ stubs: { GlEmptyState } }); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks design creation', () => { + wrapper.vm.onUploadDesignDone(designUploadMutationCreatedResponse); + + expect(trackingSpy).toHaveBeenCalledTimes(1); + expect(trackingSpy).toHaveBeenCalledWith(DESIGN_TRACKING_PAGE_NAME, 'create_design'); + }); + + it('tracks design modification', () => { + wrapper.vm.onUploadDesignDone(designUploadMutationUpdatedResponse); + + expect(trackingSpy).toHaveBeenCalledTimes(1); + expect(trackingSpy).toHaveBeenCalledWith(DESIGN_TRACKING_PAGE_NAME, 'update_design'); + }); + }); }); describe('on latest version when has designs', () => { diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index 32382bf21ed..3a54ed2efed 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Types::Ci::JobType do name needs detailedStatus + scheduledAt ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 727ad243349..4cf207bf9d8 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -699,6 +699,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:copy_indexes).with(:users, :old, :new) expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new) + expect(model).to receive(:copy_check_constraints).with(:users, :old, :new) model.rename_column_concurrently(:users, :old, :new) end @@ -761,6 +762,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:change_column_default) .with(:users, :new, old_column.default) + expect(model).to receive(:copy_check_constraints) + .with(:users, :old, :new) + model.rename_column_concurrently(:users, :old, :new) end end @@ -856,6 +860,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:copy_indexes).with(:users, :new, :old) expect(model).to receive(:copy_foreign_keys).with(:users, :new, :old) + expect(model).to receive(:copy_check_constraints).with(:users, :new, :old) model.undo_cleanup_concurrent_column_rename(:users, :old, :new) end @@ -894,6 +899,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:change_column_default) .with(:users, :old, new_column.default) + expect(model).to receive(:copy_check_constraints) + .with(:users, :new, :old) + model.undo_cleanup_concurrent_column_rename(:users, :old, :new) end end @@ -2172,6 +2180,138 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#copy_check_constraints' do + context 'inside a transaction' do + it 'raises an error' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect do + model.copy_check_constraints(:test_table, :old_column, :new_column) + end.to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + allow(model).to receive(:column_exists?).and_return(true) + end + + let(:old_column_constraints) do + [ + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'check_d7d49d475d', + 'constraint_def' => 'CHECK ((old_column IS NOT NULL))' + }, + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'check_48560e521e', + 'constraint_def' => 'CHECK ((char_length(old_column) <= 255))' + }, + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'custom_check_constraint', + 'constraint_def' => 'CHECK (((old_column IS NOT NULL) AND (another_column IS NULL)))' + }, + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'not_valid_check_constraint', + 'constraint_def' => 'CHECK ((old_column IS NOT NULL)) NOT VALID' + } + ] + end + + it 'copies check constraints from one column to another' do + allow(model).to receive(:check_constraints_for) + .with(:test_table, :old_column, schema: nil) + .and_return(old_column_constraints) + + allow(model).to receive(:not_null_constraint_name).with(:test_table, :new_column) + .and_return('check_1') + + allow(model).to receive(:text_limit_name).with(:test_table, :new_column) + .and_return('check_2') + + allow(model).to receive(:check_constraint_name) + .with(:test_table, :new_column, 'copy_check_constraint') + .and_return('check_3') + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '(new_column IS NOT NULL)', + 'check_1', + validate: true + ).once + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '(char_length(new_column) <= 255)', + 'check_2', + validate: true + ).once + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '((new_column IS NOT NULL) AND (another_column IS NULL))', + 'check_3', + validate: true + ).once + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '(new_column IS NOT NULL)', + 'check_1', + validate: false + ).once + + model.copy_check_constraints(:test_table, :old_column, :new_column) + end + + it 'does nothing if there are no constraints defined for the old column' do + allow(model).to receive(:check_constraints_for) + .with(:test_table, :old_column, schema: nil) + .and_return([]) + + expect(model).not_to receive(:add_check_constraint) + + model.copy_check_constraints(:test_table, :old_column, :new_column) + end + + it 'raises an error when the orginating column does not exist' do + allow(model).to receive(:column_exists?).with(:test_table, :old_column).and_return(false) + + error_message = /Column old_column does not exist on test_table/ + + expect do + model.copy_check_constraints(:test_table, :old_column, :new_column) + end.to raise_error(RuntimeError, error_message) + end + + it 'raises an error when the target column does not exist' do + allow(model).to receive(:column_exists?).with(:test_table, :new_column).and_return(false) + + error_message = /Column new_column does not exist on test_table/ + + expect do + model.copy_check_constraints(:test_table, :old_column, :new_column) + end.to raise_error(RuntimeError, error_message) + end + end + end + describe '#add_text_limit' do context 'when it is called with the default options' do it 'calls add_check_constraint with an infered constraint name and validate: true' do diff --git a/spec/serializers/label_serializer_spec.rb b/spec/serializers/label_serializer_spec.rb index ae1466b16e5..40249450f7f 100644 --- a/spec/serializers/label_serializer_spec.rb +++ b/spec/serializers/label_serializer_spec.rb @@ -37,11 +37,12 @@ RSpec.describe LabelSerializer do subject { serializer.represent_appearance(resource) } it 'serializes only attributes used for appearance' do - expect(subject.keys).to eq([:id, :title, :color, :text_color]) + expect(subject.keys).to eq([:id, :title, :color, :project_id, :text_color]) expect(subject[:id]).to eq(resource.id) expect(subject[:title]).to eq(resource.title) expect(subject[:color]).to eq(resource.color) expect(subject[:text_color]).to eq(resource.text_color) + expect(subject[:project_id]).to eq(resource.project_id) end end end diff --git a/spec/services/incident_management/create_incident_label_service_spec.rb b/spec/services/incident_management/create_incident_label_service_spec.rb index 4771dfc9e64..441cddf1d2e 100644 --- a/spec/services/incident_management/create_incident_label_service_spec.rb +++ b/spec/services/incident_management/create_incident_label_service_spec.rb @@ -3,65 +3,5 @@ require 'spec_helper' RSpec.describe IncidentManagement::CreateIncidentLabelService do - let_it_be(:project) { create(:project, :private) } - let_it_be(:user) { User.alert_bot } - let(:service) { described_class.new(project, user) } - - subject(:execute) { service.execute } - - describe 'execute' do - let(:incident_label_attributes) { attributes_for(:label, :incident) } - let(:title) { incident_label_attributes[:title] } - let(:color) { incident_label_attributes[:color] } - let(:description) { incident_label_attributes[:description] } - - shared_examples 'existing label' do - it 'returns the existing label' do - expect { execute }.not_to change(Label, :count) - - expect(execute).to be_success - expect(execute.payload).to eq(label: label) - end - end - - shared_examples 'new label' do - it 'creates a new label' do - expect { execute }.to change(Label, :count).by(1) - - label = project.reload.labels.last - expect(execute).to be_success - expect(execute.payload).to eq(label: label) - expect(label.title).to eq(title) - expect(label.color).to eq(color) - expect(label.description).to eq(description) - end - end - - context 'with predefined project label' do - it_behaves_like 'existing label' do - let!(:label) { create(:label, project: project, title: title) } - end - end - - context 'with predefined group label' do - let(:project) { create(:project, group: group) } - let(:group) { create(:group) } - - it_behaves_like 'existing label' do - let!(:label) { create(:group_label, group: group, title: title) } - end - end - - context 'without label' do - context 'when user has permissions to create labels' do - it_behaves_like 'new label' - end - - context 'when user has no permissions to create labels' do - let_it_be(:user) { create(:user) } - - it_behaves_like 'new label' - end - end - end + it_behaves_like 'incident management label service' end diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index f03e1ed0e22..a8db87e48d0 100644 --- a/spec/services/projects/after_rename_service_spec.rb +++ b/spec/services/projects/after_rename_service_spec.rb @@ -243,7 +243,7 @@ RSpec.describe Projects::AfterRenameService do def service_execute # AfterRenameService is called by UpdateService after a successful model.update # the initialization will include before and after paths values - project.update(path: path_after_rename) + project.update!(path: path_after_rename) described_class.new(project, path_before: path_before_rename, full_path_before: full_path_before_rename).execute end diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index 6231ac71987..aff1aa41091 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -123,7 +123,7 @@ RSpec.describe Projects::AutocompleteService do let!(:subgroup_milestone) { create(:milestone, group: subgroup) } before do - project.update(namespace: subgroup) + project.update!(namespace: subgroup) end it 'includes project milestones and all acestors milestones' do diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index b81b3e095cf..717358ef814 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Projects::CreateService, '#execute' do end it 'creates labels on Project creation if there are templates' do - Label.create(title: "bug", template: true) + Label.create!(title: "bug", template: true) project = create_project(user, opts) created_label = project.reload.labels.last diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index a3711c9e17f..f0f09218b06 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -72,7 +72,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do context 'when project has remote mirrors' do let!(:project) do create(:project, :repository, namespace: user.namespace).tap do |project| - project.remote_mirrors.create(url: 'http://test.com') + project.remote_mirrors.create!(url: 'http://test.com') end end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 166a2dae55b..555f2f5a5e5 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -179,7 +179,7 @@ RSpec.describe Projects::ForkService do context "when origin has git depth specified" do before do - @from_project.update(ci_default_git_depth: 42) + @from_project.update!(ci_default_git_depth: 42) end it "inherits default_git_depth from the origin project" do @@ -201,7 +201,7 @@ RSpec.describe Projects::ForkService do context "when project has restricted visibility level" do context "and only one visibility level is restricted" do before do - @from_project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + @from_project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) end diff --git a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb index 969381b8748..86e3fb3820c 100644 --- a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb +++ b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Projects::HashedStorage::BaseAttachmentService do describe '#move_folder!' do context 'when old_path is not a directory' do it 'adds information to the logger and returns true' do - Tempfile.create do |old_path| + Tempfile.create do |old_path| # rubocop:disable Rails/SaveBang new_path = "#{old_path}-new" expect(subject.send(:move_folder!, old_path, new_path)).to be_truthy diff --git a/spec/services/projects/move_access_service_spec.rb b/spec/services/projects/move_access_service_spec.rb index de3871414af..02f80988dd1 100644 --- a/spec/services/projects/move_access_service_spec.rb +++ b/spec/services/projects/move_access_service_spec.rb @@ -17,9 +17,9 @@ RSpec.describe Projects::MoveAccessService do project_with_access.add_maintainer(maintainer_user) project_with_access.add_developer(developer_user) project_with_access.add_reporter(reporter_user) - project_with_access.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) - project_with_access.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) - project_with_access.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER) + project_with_access.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) + project_with_access.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + project_with_access.project_group_links.create!(group: reporter_group, group_access: Gitlab::Access::REPORTER) end subject { described_class.new(target_project, user) } @@ -97,7 +97,7 @@ RSpec.describe Projects::MoveAccessService do end it 'does not remove remaining group links' do - target_project.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) + target_project.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) subject.execute(project_with_access, options) diff --git a/spec/services/projects/move_project_group_links_service_spec.rb b/spec/services/projects/move_project_group_links_service_spec.rb index 196a8f2b339..6304eded8d3 100644 --- a/spec/services/projects/move_project_group_links_service_spec.rb +++ b/spec/services/projects/move_project_group_links_service_spec.rb @@ -14,9 +14,9 @@ RSpec.describe Projects::MoveProjectGroupLinksService do describe '#execute' do before do - project_with_groups.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) - project_with_groups.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) - project_with_groups.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER) + project_with_groups.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) + project_with_groups.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + project_with_groups.project_group_links.create!(group: reporter_group, group_access: Gitlab::Access::REPORTER) end it 'moves the group links from one project to another' do @@ -30,8 +30,8 @@ RSpec.describe Projects::MoveProjectGroupLinksService do end it 'does not move existent group links in the current project' do - target_project.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) - target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + target_project.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) + target_project.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER) expect(project_with_groups.project_group_links.count).to eq 3 expect(target_project.project_group_links.count).to eq 2 @@ -55,8 +55,8 @@ RSpec.describe Projects::MoveProjectGroupLinksService do let(:options) { { remove_remaining_elements: false } } it 'does not remove remaining project group links' do - target_project.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) - target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + target_project.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) + target_project.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER) subject.execute(project_with_groups, options) diff --git a/spec/services/projects/overwrite_project_service_spec.rb b/spec/services/projects/overwrite_project_service_spec.rb index a03746d0271..cc6a863a11d 100644 --- a/spec/services/projects/overwrite_project_service_spec.rb +++ b/spec/services/projects/overwrite_project_service_spec.rb @@ -111,9 +111,9 @@ RSpec.describe Projects::OverwriteProjectService do create_list(:deploy_keys_project, 2, project: project_from) create_list(:notification_setting, 2, source: project_from) create_list(:users_star_project, 2, project: project_from) - project_from.project_group_links.create(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) - project_from.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) - project_from.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER) + project_from.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER) + project_from.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + project_from.project_group_links.create!(group: reporter_group, group_access: Gitlab::Access::REPORTER) project_from.add_maintainer(maintainer_user) project_from.add_developer(developer_user) project_from.add_reporter(reporter_user) diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 073e2e09397..2a8965e62ce 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -61,7 +61,7 @@ RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_cachin context 'when the original project was deleted' do it 'does not fail when the original project is deleted' do source = forked_project.forked_from_project - source.destroy + source.destroy! forked_project.reload expect { subject.execute }.not_to raise_error diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 2273ddf813b..d3eb84a3137 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -95,14 +95,14 @@ RSpec.describe Projects::UpdatePagesService do expect(project.pages_deployed?).to be_truthy expect(Dir.exist?(File.join(project.pages_path))).to be_truthy - project.destroy + project.destroy! expect(Dir.exist?(File.join(project.pages_path))).to be_falsey expect(ProjectPagesMetadatum.find_by_project_id(project)).to be_nil end it 'fails if sha on branch is not latest' do - build.update(ref: 'feature') + build.update!(ref: 'feature') expect(execute).not_to eq(:success) expect(project.pages_metadatum).not_to be_deployed @@ -191,7 +191,7 @@ RSpec.describe Projects::UpdatePagesService do it 'fails to remove project pages when no pages is deployed' do expect(PagesWorker).not_to receive(:perform_in) expect(project.pages_deployed?).to be_falsey - project.destroy + project.destroy! end it 'fails if no artifacts' do diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 3375d9762c8..989426fde8b 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -141,7 +141,7 @@ RSpec.describe Projects::UpdateService do let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } before do - project.update(namespace: group, visibility_level: group.visibility_level) + project.update!(namespace: group, visibility_level: group.visibility_level) end it 'does not update project visibility level' do @@ -256,7 +256,7 @@ RSpec.describe Projects::UpdateService do end it 'handles empty project feature attributes' do - project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED) + project.project_feature.update!(wiki_access_level: ProjectFeature::DISABLED) result = update_project(project, user, { name: 'test1' }) @@ -267,7 +267,7 @@ RSpec.describe Projects::UpdateService do context 'when enabling a wiki' do it 'creates a wiki' do - project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED) + project.project_feature.update!(wiki_access_level: ProjectFeature::DISABLED) TestEnv.rm_storage_dir(project.repository_storage, project.wiki.path) result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED }) @@ -278,7 +278,7 @@ RSpec.describe Projects::UpdateService do end it 'logs an error and creates a metric when wiki can not be created' do - project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED) + project.project_feature.update!(wiki_access_level: ProjectFeature::DISABLED) expect_any_instance_of(ProjectWiki).to receive(:wiki).and_raise(Wiki::CouldNotCreateWikiError) expect_any_instance_of(described_class).to receive(:log_error).with("Could not create wiki for #{project.full_name}") diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index d6e79931df5..39c22ac8aa3 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -45,3 +45,74 @@ RSpec.shared_examples 'not an incident issue' do expect(issue.labels).not_to include(have_attributes(label_properties)) end end + +# This shared example is to test the execution of incident management label services +# For example: +# - IncidentManagement::CreateIncidentSlaExceededLabelService +# - IncidentManagement::CreateIncidentLabelService + +# It doesn't require any defined variables + +RSpec.shared_examples 'incident management label service' do + let_it_be(:project) { create(:project, :private) } + let_it_be(:user) { User.alert_bot } + let(:service) { described_class.new(project, user) } + + subject(:execute) { service.execute } + + describe 'execute' do + let(:incident_label_attributes) { described_class::LABEL_PROPERTIES } + let(:title) { incident_label_attributes[:title] } + let(:color) { incident_label_attributes[:color] } + let(:description) { incident_label_attributes[:description] } + + shared_examples 'existing label' do + it 'returns the existing label' do + expect { execute }.not_to change(Label, :count) + + expect(execute).to be_success + expect(execute.payload).to eq(label: label) + end + end + + shared_examples 'new label' do + it 'creates a new label' do + expect { execute }.to change(Label, :count).by(1) + + label = project.reload.labels.last + expect(execute).to be_success + expect(execute.payload).to eq(label: label) + expect(label.title).to eq(title) + expect(label.color).to eq(color) + expect(label.description).to eq(description) + end + end + + context 'with predefined project label' do + it_behaves_like 'existing label' do + let!(:label) { create(:label, project: project, title: title) } + end + end + + context 'with predefined group label' do + let(:project) { create(:project, group: group) } + let(:group) { create(:group) } + + it_behaves_like 'existing label' do + let!(:label) { create(:group_label, group: group, title: title) } + end + end + + context 'without label' do + context 'when user has permissions to create labels' do + it_behaves_like 'new label' + end + + context 'when user has no permissions to create labels' do + let_it_be(:user) { create(:user) } + + it_behaves_like 'new label' + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 750b2099542..d0251a2e627 100644 --- a/yarn.lock +++ b/yarn.lock @@ -866,10 +866,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.171.0.tgz#abc3092bf804f0898301626130e0f3231834924a" integrity sha512-TPfdqIxQDda+0CQHhb9XdF50lmqDmADu6yT8R4oZi6BoUtWLdiHbyFt+RnVU6t7EmjIKicNAii7Ga+f2ljCfUA== -"@gitlab/ui@21.27.0": - version "21.27.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.27.0.tgz#4463adc552bb7b7f9a22e0a0281ca761a3daa70a" - integrity sha512-9bMZZebdXWXhPnXbklcragfGosNwZEcqulITWvPSwXcFJwNk2xEHpKy7b/SwQMcErpDjne/eduEnWEGtT+aFNw== +"@gitlab/ui@21.28.0": + version "21.28.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.28.0.tgz#28455d9f53ed34c0b17ea8e1073b670c59617032" + integrity sha512-skhWKaC3hzWpLA6GoDLG5qJqdgRhYNfAtE2W7pONyfi21eUgZuMbzCVSX3dYLm6v2LEBsJRZXbguWmCOT2ZilQ== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |
