summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-14 09:08:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-14 09:08:46 +0000
commit6035fcc36ead3b415fa2422b0204a795a70f3e2f (patch)
tree011ffa756aa74a83dd1b6d5da5edb0380f4c52a2
parent4a159b9f98bf1c1a62035ea42e8ba56cafb48d98 (diff)
downloadgitlab-ce-6035fcc36ead3b415fa2422b0204a795a70f3e2f.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/dast.gitlab-ci.yml2
-rw-r--r--.rubocop_todo.yml13
-rw-r--r--app/assets/javascripts/boards/boards_util.js21
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js32
-rw-r--r--app/assets/javascripts/boards/queries/board_list_create.mutation.graphql20
-rw-r--r--app/assets/javascripts/boards/stores/actions.js65
-rw-r--r--app/assets/javascripts/boards/stores/getters.js13
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js36
-rw-r--r--app/assets/javascripts/boards/stores/state.js2
-rw-r--r--app/assets/javascripts/breadcrumb.js9
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue16
-rw-r--r--app/assets/javascripts/design_management/utils/tracking.js28
-rw-r--r--app/assets/javascripts/pages/projects/index.js6
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/graphql/types/ci/detailed_status_type.rb14
-rw-r--r--app/graphql/types/ci/job_type.rb2
-rw-r--r--app/serializers/label_entity.rb2
-rw-r--r--app/serializers/label_serializer.rb2
-rw-r--r--changelogs/unreleased/241663-incident-sla-cron-job.yml5
-rw-r--r--changelogs/unreleased/247489-update-create-column-from-to-also-copy-constraints-take-2.yml5
-rw-r--r--changelogs/unreleased/design-tracking-create-update.yml5
-rw-r--r--changelogs/unreleased/lm-add-scheduled-jobs.yml5
-rw-r--r--changelogs/unreleased/lm-update-status-null-fields.yml5
-rw-r--r--changelogs/unreleased/mb_rails_save_bang_fix4.yml5
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql19
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json84
-rw-r--r--doc/api/graphql/reference/index.md15
-rw-r--r--doc/operations/incident_management/incidents.md9
-rw-r--r--doc/policy/maintenance.md6
-rw-r--r--jest.config.base.js1
-rw-r--r--lib/gitlab/database/migration_helpers.rb90
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json2
-rw-r--r--spec/frontend/boards/stores/actions_spec.js20
-rw-r--r--spec/frontend/boards/stores/getters_spec.js30
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js55
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js28
-rw-r--r--spec/frontend/design_management/pages/index_spec.js34
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb1
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb140
-rw-r--r--spec/serializers/label_serializer_spec.rb3
-rw-r--r--spec/services/incident_management/create_incident_label_service_spec.rb62
-rw-r--r--spec/services/projects/after_rename_service_spec.rb2
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb2
-rw-r--r--spec/services/projects/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/fork_service_spec.rb4
-rw-r--r--spec/services/projects/hashed_storage/base_attachment_service_spec.rb2
-rw-r--r--spec/services/projects/move_access_service_spec.rb8
-rw-r--r--spec/services/projects/move_project_group_links_service_spec.rb14
-rw-r--r--spec/services/projects/overwrite_project_service_spec.rb6
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_service_spec.rb6
-rw-r--r--spec/services/projects/update_service_spec.rb8
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb71
-rw-r--r--yarn.lock8
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:
![Timeline view toggle](./img/timeline_view_toggle_v13_5.png)
+
+### 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"