Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4a159b9f98
commit
6035fcc36e
|
|
@ -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 ",")'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default () => ({
|
|||
isShowingLabels: true,
|
||||
activeId: inactiveId,
|
||||
sidebarType: '',
|
||||
boardLists: [],
|
||||
boardLists: {},
|
||||
listsFlags: {},
|
||||
issuesByListId: {},
|
||||
pageInfoByListId: {},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Schedule adding "Missed SLA" label to issues
|
||||
merge_request: 44546
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add migration helpers for copying check constraints
|
||||
merge_request: 44777
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add product analytics for design created and modified events
|
||||
merge_request: 44129
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'GraphQL: Adds scheduledAt to CiJob'
|
||||
merge_request: 44054
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'GraphQL: Changes fields in detailedStatus to be nullable'
|
||||
merge_request: 45072
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Rails/SaveBang offenses in spec/services/projects/*
|
||||
merge_request: 44980
|
||||
author: matthewbried
|
||||
type: other
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@
|
|||
- 2
|
||||
- - incident_management
|
||||
- 2
|
||||
- - incident_management_apply_incident_sla_exceeded_label
|
||||
- 1
|
||||
- - invalid_gpg_signature_update
|
||||
- 2
|
||||
- - irker
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ module.exports = path => {
|
|||
'jest-junit',
|
||||
{
|
||||
outputName: './junit_jest.xml',
|
||||
addFileAttribute: 'true',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, lists);
|
||||
expect(state.boardLists).toEqual(initialBoardListsState);
|
||||
});
|
||||
});
|
||||
|
||||
expect(state.boardLists).toEqual(lists);
|
||||
describe('RECEIVE_BOARD_LISTS_FAILURE', () => {
|
||||
it('Should set error in state', () => {
|
||||
mutations[types.RECEIVE_BOARD_LISTS_FAILURE](state);
|
||||
|
||||
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 });
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RSpec.describe Types::Ci::JobType do
|
|||
name
|
||||
needs
|
||||
detailedStatus
|
||||
scheduledAt
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue