Commit fdec8e2b authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge branch 'docs-admin-remove-notes' into 'master'

Docs: Update Admin index page to remove notes

See merge request gitlab-org/gitlab!45070
parents d90ef71f 37b08fe6
......@@ -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 ",")'
......
......@@ -21,12 +21,10 @@ https://docs.gitlab.com/ee/development/documentation/index.html#move-or-rename-a
a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app,
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/documentation/index.html#redirections-for-pages-with-disqus-comments)
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/documentation/index.html#redirections-for-pages-with-disqus-comments)
to the new document if there are any Disqus comments on the old document thread.
- [ ] Update the link in `features.yml` (if applicable)
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
with the changes as well (https://docs.gitlab.com/ce/development/documentation/index.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review.
- [ ] Assign one of the technical writers for review.
/label ~documentation
......@@ -362,6 +362,15 @@ Graphql/AuthorizeTypes:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Graphql/GIDExpectedType:
Enabled: true
Include:
- 'app/graphql/**/*'
- 'ee/app/graphql/**/*'
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Graphql/JSONType:
Enabled: true
Include:
......
......@@ -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'
......
......@@ -259,7 +259,7 @@ gem 'asana', '0.10.2'
gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
gem 'kubeclient', '~> 4.6.0'
gem 'kubeclient', '~> 4.9.1'
# Sanitize user input
gem 'sanitize', '~> 5.2.1'
......
......@@ -253,7 +253,7 @@ GEM
discordrb-webhooks-blackst0ne (3.3.0)
rest-client (~> 2.0)
docile (1.3.2)
domain_name (0.5.20180417)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.3.3)
railties (>= 5)
......@@ -563,14 +563,15 @@ GEM
html2text (0.2.0)
nokogiri (~> 1.6)
htmlentities (4.3.4)
http (4.2.0)
http (4.4.1)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (~> 2.0)
http-form_data (~> 2.2)
http-parser (~> 1.2.0)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (2.1.1)
http-form_data (2.3.0)
http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0)
httparty (0.16.4)
......@@ -611,6 +612,9 @@ GEM
hana (~> 1.3)
regexp_parser (~> 1.5)
uri_template (~> 0.7)
jsonpath (1.0.5)
multi_json
to_regexp (~> 0.2.1)
jwt (2.1.0)
kaminari (1.2.1)
activesupport (>= 4.1.0)
......@@ -631,9 +635,10 @@ GEM
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
kubeclient (4.6.0)
kubeclient (4.9.1)
http (>= 3.0, < 5.0)
recursive-open-struct (~> 1.0, >= 1.0.4)
jsonpath (~> 1.0)
recursive-open-struct (~> 1.1, >= 1.1.1)
rest-client (~> 2.0)
launchy (2.4.3)
addressable (~> 2.3)
......@@ -847,7 +852,7 @@ GEM
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
public_suffix (4.0.6)
pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6)
rack (2.1.4)
......@@ -920,7 +925,7 @@ GEM
re2 (1.2.0)
recaptcha (4.13.1)
json
recursive-open-struct (1.1.1)
recursive-open-struct (1.1.2)
redis (4.1.3)
redis-actionpack (5.2.0)
actionpack (>= 5, < 7)
......@@ -951,7 +956,8 @@ GEM
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
rest-client (2.0.2)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
......@@ -1145,6 +1151,7 @@ GEM
timecop (0.9.1)
timeliness (0.3.10)
timfel-krb5-auth (0.8.3)
to_regexp (0.2.1)
toml (0.2.0)
parslet (~> 1.8.0)
toml-rb (1.0.0)
......@@ -1161,7 +1168,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
unicode_plot (0.0.4)
enumerable-statistics (>= 2.0.1)
......@@ -1371,7 +1378,7 @@ DEPENDENCIES
kaminari (~> 1.0)
knapsack (~> 1.17)
kramdown (~> 2.3.0)
kubeclient (~> 4.6.0)
kubeclient (~> 4.9.1)
letter_opener_web (~> 1.3.4)
license_finder (~> 6.0)
licensee (~> 8.9)
......
......@@ -73,7 +73,7 @@ sensitive as to how you word things. Use Emoji to express your feelings (heart,
star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines].
[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
[Code Review Guidelines]: https://docs.gitlab.com/ee/development/code_review.html
## Feature flags
......@@ -217,5 +217,5 @@ rebase with master to see if that solves the issue.
[team]: https://about.gitlab.com/team/
[done]: https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#definition-of-done
[automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html
[automatic_ce_ee_merge]: https://docs.gitlab.com/ee/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ee/development/ee_features.html
......@@ -376,7 +376,7 @@ const Api = {
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
// see https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitsPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
......
......@@ -5,6 +5,7 @@ import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../../notes/event_hub';
import { __ } from '~/locale';
import { fixTitle } from '~/tooltips';
const loadRichBlobViewer = type => {
switch (type) {
......@@ -124,7 +125,7 @@ export default class BlobViewer {
this.copySourceBtn.classList.add('disabled');
}
$(this.copySourceBtn).tooltip('_fixTitle');
fixTitle($(this.copySourceBtn));
}
switchToViewer(name) {
......
......@@ -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,
};
<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,
......
#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
}
......
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;
......
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';
......
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: {},
......
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);
});
}
};
......@@ -360,7 +360,7 @@ export default {
>
<template #link="{ content }">
<gl-link
href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html"
target="_blank"
>{{ content }}</gl-link
>
......
......@@ -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();
......
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);
}
......@@ -5,6 +5,14 @@ import getUnicodeSupportMap from './unicode_support_map';
let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) {
// Skipping the map creation for Bots + RSPec
if (
navigator.userAgent.indexOf('HeadlessChrome') > -1 ||
navigator.userAgent.indexOf('Lighthouse') > -1 ||
navigator.userAgent.indexOf('Speedindex') > -1
) {
return true;
}
browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
}
<script>
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { GlButton, GlModalDirective, GlTabs } from '@gitlab/ui';
import { GlAlert, GlButton, GlModalDirective, GlSprintf, GlTabs } from '@gitlab/ui';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
import FeatureFlagsTab from './feature_flags_tab.vue';
import FeatureFlagsTable from './feature_flags_table.vue';
......@@ -9,9 +10,9 @@ import UserListsTable from './user_lists_table.vue';
import { s__ } from '~/locale';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import {
buildUrlWithCurrentLocation,
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue';
......@@ -20,13 +21,15 @@ const SCOPES = { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE };
export default {
components: {
ConfigureFeatureFlagsModal,
FeatureFlagsTab,
FeatureFlagsTable,
UserListsTable,
TablePagination,
GlAlert,
GlButton,
GlSprintf,
GlTabs,
FeatureFlagsTab,
ConfigureFeatureFlagsModal,
TablePagination,
UserListsTable,
},
directives: {
GlModal: GlModalDirective,
......@@ -44,6 +47,20 @@ export default {
type: String,
required: true,
},
featureFlagsLimit: {
type: String,
required: true,
},
featureFlagsLimitExceeded: {
type: Boolean,
required: false,
default: false,
},
rotateInstanceIdPath: {
type: String,
required: false,
default: '',
},
unleashApiUrl: {
type: String,
required: true,
......@@ -69,6 +86,7 @@ export default {
scope,
page: getParameterByName('page') || '1',
isUserListAlertDismissed: false,
shouldShowFeatureFlagsLimitWarning: this.featureFlagsLimitExceeded,
selectedTab: Object.values(SCOPES).indexOf(scope),
};
},
......@@ -184,11 +202,36 @@ export default {
dataForScope(scope) {
return this[scope];
},
onDismissFeatureFlagsLimitWarning() {
this.shouldShowFeatureFlagsLimitWarning = false;
},
onNewFeatureFlagCLick() {
if (this.featureFlagsLimitExceeded) {
this.shouldShowFeatureFlagsLimitWarning = true;
}
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="shouldShowFeatureFlagsLimitWarning"
variant="warning"
@dismiss="onDismissFeatureFlagsLimitWarning"
>
<gl-sprintf
:message="
s__(
'FeatureFlags|Feature flags limit reached (%{featureFlagsLimit}). Delete one or more feature flags before adding new ones.',
)
"
>
<template #featureFlagsLimit>
<span>{{ featureFlagsLimit }}</span>
</template>
</gl-sprintf>
</gl-alert>
<configure-feature-flags-modal
v-if="canUserConfigure"
:help-client-libraries-path="featureFlagsClientLibrariesHelpPagePath"
......@@ -228,9 +271,10 @@ export default {
<gl-button
v-if="hasNewPath"
:href="newFeatureFlagPath"
:href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath"
variant="success"
data-testid="ff-new-button"
@click="onNewFeatureFlagCLick"
>
{{ s__('FeatureFlags|New feature flag') }}
</gl-button>
......@@ -306,9 +350,10 @@ export default {
<gl-button
v-if="hasNewPath"
:href="newFeatureFlagPath"
:href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath"
variant="success"
data-testid="ff-new-button"
@click="onNewFeatureFlagCLick"
>
{{ s__('FeatureFlags|New feature flag') }}
</gl-button>
......
......@@ -36,6 +36,8 @@ export default () => {
el.dataset.featureFlagsClientLibrariesHelpPagePath,
featureFlagsClientExampleHelpPagePath: el.dataset.featureFlagsClientExampleHelpPagePath,
unleashApiUrl: el.dataset.unleashApiUrl,
featureFlagsLimitExceeded: el.dataset.featureFlagsLimitExceeded,
featureFlagsLimit: el.dataset.featureFlagsLimit,
csrfToken: csrf.token,
canUserConfigure: el.dataset.canUserAdminFeatureFlag,
newFeatureFlagPath: el.dataset.newFeatureFlagPath,
......
......@@ -16,53 +16,63 @@ const frequentItemDropdowns = [
},
];
const initFrequentItemList = (namespace, key) => {
const el = document.getElementById(`js-${namespace}-dropdown`);
// Don't do anything if element doesn't exist (No groups dropdown)
// This is for when the user accesses GitLab without logging in
if (!el) {
return;
}
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
data() {
const { dataset } = this.$options.el;
const item = {
id: Number(dataset[`${key}Id`]),
name: dataset[`${key}Name`],
namespace: dataset[`${key}Namespace`],
webUrl: dataset[`${key}WebUrl`],
avatarUrl: dataset[`${key}AvatarUrl`] || null,
lastAccessedOn: Date.now(),
};
return {
currentUserName: dataset.userName,
currentItem: item,
};
},
render(createElement) {
return createElement(FrequentItems, {
props: {
namespace,
currentUserName: this.currentUserName,
currentItem: this.currentItem,
},
});
},
});
})
.catch(() => {});
};
export default function initFrequentItemDropdowns() {
frequentItemDropdowns.forEach(dropdown => {
const { namespace, key } = dropdown;
const el = document.getElementById(`js-${namespace}-dropdown`);
const navEl = document.getElementById(`nav-${namespace}-dropdown`);
// Don't do anything if element doesn't exist (No groups dropdown)
// This is for when the user accesses GitLab without logging in
if (!el || !navEl) {
if (!navEl) {
return;
}
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
data() {
const { dataset } = this.$options.el;
const item = {
id: Number(dataset[`${key}Id`]),
name: dataset[`${key}Name`],
namespace: dataset[`${key}Namespace`],
webUrl: dataset[`${key}WebUrl`],
avatarUrl: dataset[`${key}AvatarUrl`] || null,
lastAccessedOn: Date.now(),
};
return {
currentUserName: dataset.userName,
currentItem: item,
};
},
render(createElement) {
return createElement(FrequentItems, {
props: {
namespace,
currentUserName: this.currentUserName,
currentItem: this.currentItem,
},
});
},
});
})
.catch(() => {});
$(navEl).on('shown.bs.dropdown', () => {
initFrequentItemList(namespace, key);
eventHub.$emit(`${namespace}-dropdownOpen`);
});
});
......
......@@ -5,6 +5,7 @@ import { formatDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlLink,
IncidentSla: () => import('ee_component/issue_show/components/incidents/incident_sla.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -12,36 +13,51 @@ export default {
props: {
alert: {
type: Object,
required: true,
required: false,
default: null,
},
},
data() {
return { childHasData: false };
},
computed: {
startTime() {
return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z');
},
showHighlightBar() {
return this.alert || this.childHasData;
},
},
methods: {
update(hasData) {
this.childHasData = hasData;
},
},
};
</script>
<template>
<div
v-show="showHighlightBar"
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
>
<div class="gl-pr-3">
<div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
#{{ alert.iid }}
</gl-link>
</div>
<div class="gl-pr-3">
<div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
{{ startTime }}
</div>
<div>
<div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
<span>{{ alert.eventCount }}</span>
</div>
<incident-sla @update="update" />
</div>
</template>
......@@ -53,7 +53,7 @@ export default {
<div>
<gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs">
<gl-tab :title="s__('Incident|Summary')">
<highlight-bar v-if="alert" :alert="alert" />
<highlight-bar :alert="alert" />
<description-component v-bind="$attrs" />
</gl-tab>
<gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')">
......
......@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(VueApollo);
......@@ -11,7 +12,7 @@ export default function initIssuableApp(issuableData = {}) {
defaultClient: createDefaultClient(),
});
const { projectNamespace, projectPath, iid } = issuableData;
const { iid, projectNamespace, projectPath, slaFeatureAvailable } = issuableData;
return new Vue({
el: document.getElementById('js-issuable-app'),
......@@ -22,6 +23,7 @@ export default function initIssuableApp(issuableData = {}) {
provide: {
fullPath: `${projectNamespace}/${projectPath}`,
iid,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
},
render(createElement) {
return createElement('issuable-app', {
......
......@@ -13,6 +13,7 @@ import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { fixTitle } from '~/tooltips';
export default class LabelsSelect {
constructor(els, options = {}) {
......@@ -57,7 +58,6 @@ export default class LabelsSelect {
.get();
const scopedLabels = $dropdown.data('scopedLabels');
const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new CreateLabelDropdown(
......@@ -166,7 +166,8 @@ export default class LabelsSelect {
labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle');
$sidebarLabelTooltip.attr('title', labelTooltipTitle);
fixTitle($sidebarLabelTooltip);
$('.has-tooltip', $value).tooltip({
container: 'body',
......
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
......@@ -3,7 +3,7 @@ import {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
GlDeprecatedDropdownDivider,
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
......@@ -21,7 +21,7 @@ export default {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
GlDeprecatedDropdownDivider,
GlDropdownDivider,
GlLoadingIcon,
},
props: {
......@@ -94,7 +94,7 @@ export default {
<gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{
$options.anyTriggerAuthor
}}</gl-filtered-search-suggestion>
<gl-deprecated-dropdown-divider />
<gl-dropdown-divider />
<gl-loading-icon v-if="loading" />
<template v-else>
......
<script>
import { GlProgressBar } from '@gitlab/ui';
import { GlProgressBar, GlTooltipDirective } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
import { s__, sprintf } from '~/locale';
export default {
......@@ -10,7 +9,7 @@ export default {
GlProgressBar,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
timeSpent: {
......@@ -73,7 +72,7 @@ export default {
<template>
<div class="time-tracking-comparison-pane">
<div
v-tooltip
v-gl-tooltip
:title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
class="compare-meter"
......
......@@ -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';
......
......@@ -215,7 +215,7 @@
}
&.build-trace-rounded {
border-radius: $border-radius-base;
border-radius: $gl-border-radius-base;
}
}
......
......@@ -8,24 +8,24 @@
.external-url,
.dropdown-new {
color: $gl-text-color-secondary;
color: var(--gray-500, $gray-500);
}
.build-link,
.ref-name {
color: $gl-text-color;
color: var(--gray-900, $gray-900);
}
.folder-icon {
margin-right: 3px;
color: $gl-text-color-secondary;
color: var(--gray-500, $gray-500);
display: inline-block;
vertical-align: text-top;
}
.folder-name {
cursor: pointer;
color: $gl-text-color-secondary;
color: var(--gray-500, $gray-500);
display: inline-block;
}
......@@ -74,17 +74,17 @@
.x-axis path,
.y-axis path {
stroke: $gray-300;
stroke: var(--gray-300, $gray-300);
}
.label-x-axis-line,
.label-y-axis-line {
stroke: $border-color;
stroke: var(--gray-100, $gray-100);
}
.y-axis {
line {
stroke: $gray-300;
stroke: var(--gray-300, $gray-300);
stroke-width: 1;
}
}
......@@ -94,13 +94,13 @@
}
.rect-text-metric {
fill: $white;
fill: var(--white, $white);
stroke-width: 1;
stroke: $gray-darkest;
stroke: var(--gray-600, $gray-600);
}
.rect-axis-text {
fill: $white;
fill: var(--white, $white);
}
.text-metric {
......@@ -108,18 +108,18 @@
}
.selected-metric-line {
stroke: $gray-900;
stroke: var(--gray-900, $gray-900);
stroke-width: 1;
}
.deployment-line {
stroke: $black;
stroke: var(--white, $white);
stroke-width: 1;
}
.divider-line {
stroke-width: 1;
stroke: $gray-darkest;
stroke: var(--gray-600, $gray-600);
}
.environments-actions {
......
......@@ -476,3 +476,9 @@
height: auto !important;
}
}
.test-reports-table {
.build-trace {
@include build-trace();
}
}
......@@ -28,7 +28,10 @@
height: 14px;
width: 14px;
vertical-align: middle;
fill: $gl-text-color-secondary;
&:not(.text-warning) {
fill: $gl-text-color-secondary;
}
}
.sprite {
......@@ -130,12 +133,6 @@
float: none;
}
.test-reports-table {
.build-trace {
@include build-trace();
}
}
.progress-bar.bg-primary {
background-color: $blue-500 !important;
}
......
......@@ -72,6 +72,7 @@ class Admin::UsersController < Admin::ApplicationController
def deactivate
return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user cannot be deactivated")) if user.blocked?
return redirect_back_or_admin_user(notice: _("Successfully deactivated")) if user.deactivated?
return redirect_back_or_admin_user(notice: _("Internal users cannot be deactivated")) if user.internal?
return redirect_back_or_admin_user(notice: _("The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated") % { minimum_inactive_days: ::User::MINIMUM_INACTIVE_DAYS }) unless user.can_be_deactivated?
user.deactivate
......
......@@ -11,8 +11,8 @@ class Dashboard::LabelsController < Dashboard::ApplicationController
def labels
finder_params = { project_ids: projects.select(:id) }
labels = LabelsFinder.new(current_user, finder_params).execute
GlobalLabel.build_collection(labels)
LabelsFinder.new(current_user, finder_params).execute
.select('DISTINCT ON (labels.title) labels.*')
end
end
......@@ -25,7 +25,7 @@ module Ci
attr_reader :current_user, :pipeline, :project, :params, :type
def init_collection
if Feature.enabled?(:ci_jobs_finder_refactor)
if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true)
pipeline_jobs || project_jobs || all_jobs
else
project ? project_builds : all_jobs
......@@ -59,7 +59,7 @@ module Ci
end
def filter_by_scope(builds)
if Feature.enabled?(:ci_jobs_finder_refactor)
if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true)
return filter_by_statuses!(params[:scope], builds) if params[:scope].is_a?(Array)
end
......
......@@ -11,4 +11,11 @@ module TimeFrameFilter
rescue ArgumentError
items
end
def containing_date(items)
return items unless params[:containing_date]
date = params[:containing_date].to_date
items.within_timeframe(date, date)
end
end
......@@ -102,7 +102,7 @@ class IssuableFinder
items = filter_items(items)
# Let's see if we have to negate anything
items = filter_negated_items(items)
items = filter_negated_items(items) if should_filter_negated_args?
# This has to be last as we use a CTE as an optimization fence
# for counts by passing the force_cte param and passing the
......@@ -134,13 +134,15 @@ class IssuableFinder
by_my_reaction_emoji(items)
end
# Negates all params found in `negatable_params`
def filter_negated_items(items)
return items unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true)
def should_filter_negated_args?
return false unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true)
# API endpoints send in `nil` values so we test if there are any non-nil
return items unless not_params.present? && not_params.values.any?
not_params.present? && not_params.values.any?
end
# Negates all params found in `negatable_params`
def filter_negated_items(items)
items = by_negated_author(items)
items = by_negated_assignee(items)
items = by_negated_label(items)
......
......@@ -9,6 +9,8 @@
# order - Orders by field default due date asc.
# title - filter by title.
# state - filters by state.
# start_date & end_date - filters by timeframe (see TimeFrameFilter)
# containing_date - filters by point in time (see TimeFrameFilter)
class MilestonesFinder
include FinderMethods
......@@ -28,6 +30,7 @@ class MilestonesFinder
items = by_search_title(items)
items = by_state(items)
items = by_timeframe(items)
items = containing_date(items)
order(items)
end
......
# frozen_string_literal: true
module Mutations
module Issues
module CommonMutationArguments
extend ActiveSupport::Concern
included do
argument :description, GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :description)
argument :due_date, GraphQL::Types::ISO8601Date,
required: false,
description: copy_field_description(Types::IssueType, :due_date)
argument :confidential, GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :confidential)
argument :locked, GraphQL::BOOLEAN_TYPE,
as: :discussion_locked,
required: false,
description: copy_field_description(Types::IssueType, :discussion_locked)
end
end
end
end
Mutations::Issues::CommonMutationArguments.prepend_if_ee('::EE::Mutations::Issues::CommonMutationArguments')
# frozen_string_literal: true
module Mutations
module Issues
class Create < BaseMutation
include ResolvesProject
graphql_name 'CreateIssue'
authorize :create_issue
include CommonMutationArguments
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'Project full path the issue is associated with'
argument :iid, GraphQL::INT_TYPE,
required: false,
description: 'The IID (internal ID) of a project issue. Only admins and project owners can modify'
argument :title, GraphQL::STRING_TYPE,
required: true,
description: copy_field_description(Types::IssueType, :title)
argument :milestone_id, ::Types::GlobalIDType[::Milestone],
required: false,
description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null'
argument :labels, [GraphQL::STRING_TYPE],
required: false,
description: copy_field_description(Types::IssueType, :labels)
argument :label_ids, [::Types::GlobalIDType[::Label]],
required: false,
description: 'The IDs of labels to be added to the issue'
argument :created_at, Types::TimeType,
required: false,
description: 'Timestamp when the issue was created. Available only for admins and project owners'
argument :merge_request_to_resolve_discussions_of, ::Types::GlobalIDType[::MergeRequest],
required: false,
description: 'The IID of a merge request for which to resolve discussions'
argument :discussion_to_resolve, GraphQL::STRING_TYPE,
required: false,
description: 'The ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`'
argument :assignee_ids, [::Types::GlobalIDType[::User]],
required: false,
description: 'The array of user IDs to assign to the issue'
field :issue,
Types::IssueType,
null: true,
description: 'The issue after mutation'
def ready?(**args)
if args.slice(*mutually_exclusive_label_args).size > 1
arg_str = mutually_exclusive_label_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required."
end
if args[:discussion_to_resolve].present? && args[:merge_request_to_resolve_discussions_of].blank?
raise Gitlab::Graphql::Errors::ArgumentError,
'to resolve a discussion please also provide `merge_request_to_resolve_discussions_of` parameter'
end
super
end
def resolve(project_path:, **attributes)
project = authorized_find!(full_path: project_path)
params = build_create_issue_params(attributes.merge(author_id: current_user.id))
issue = ::Issues::CreateService.new(project, current_user, params).execute
if issue.spam?
issue.errors.add(:base, 'Spam detected.')
end
{
issue: issue.valid? ? issue : nil,
errors: errors_on_object(issue)
}
end
private
def build_create_issue_params(params)
params[:milestone_id] &&= params[:milestone_id]&.model_id
params[:assignee_ids] &&= params[:assignee_ids].map { |assignee_id| assignee_id&.model_id }
params[:label_ids] &&= params[:label_ids].map { |label_id| label_id&.model_id }
params
end
def mutually_exclusive_label_args
[:labels, :label_ids]
end
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
Mutations::Issues::Create.prepend_if_ee('::EE::Mutations::Issues::Create')
......@@ -5,49 +5,26 @@ module Mutations
class Update < Base
graphql_name 'UpdateIssue'
argument :title,
GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :title)
include CommonMutationArguments
argument :description,
GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :description)
argument :due_date,
Types::TimeType,
required: false,
description: copy_field_description(Types::IssueType, :due_date)
argument :confidential,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :confidential)
argument :locked,
GraphQL::BOOLEAN_TYPE,
as: :discussion_locked,
argument :title, GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :discussion_locked)
description: copy_field_description(Types::IssueType, :title)
argument :add_label_ids,
[GraphQL::ID_TYPE],
argument :milestone_id, GraphQL::ID_TYPE,
required: false,
description: 'The IDs of labels to be added to the issue.'
description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null'
argument :remove_label_ids,
[GraphQL::ID_TYPE],
argument :add_label_ids, [GraphQL::ID_TYPE],
required: false,
description: 'The IDs of labels to be removed from the issue.'
description: 'The IDs of labels to be added to the issue'
argument :milestone_id,
GraphQL::ID_TYPE,
argument :remove_label_ids, [GraphQL::ID_TYPE],
required: false,
description: 'The ID of the milestone to be assigned, milestone will be removed if set to null.'
description: 'The IDs of labels to be removed from the issue'
argument :state_event, Types::IssueStateEventEnum,
description: 'Close or reopen an issue.',
description: 'Close or reopen an issue',
required: false
def resolve(project_path:, iid:, **args)
......
......@@ -17,7 +17,7 @@ module Mutations
discussion_id = nil
if args[:discussion_id]
discussion = GitlabSchema.object_from_id(args[:discussion_id])
discussion = GitlabSchema.object_from_id(args[:discussion_id], expected_type: ::Discussion)
authorize_discussion!(discussion)
discussion_id = discussion.id
......
......@@ -6,6 +6,8 @@ module Resolvers
include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::GlobalIDCompatibility
argument_class ::Types::BaseArgument
def self.single
@single ||= Class.new(self) do
def ready?(**args)
......
......@@ -3,21 +3,33 @@
module TimeFrameArguments
extend ActiveSupport::Concern
OVERLAPPING_TIMEFRAME_DESC = 'List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present)'
included do
argument :start_date, Types::TimeType,
required: false,
description: 'List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)'
description: OVERLAPPING_TIMEFRAME_DESC,
deprecated: { reason: 'Use timeframe.start', milestone: '14.0' }
argument :end_date, Types::TimeType,
required: false,
description: 'List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)'
description: OVERLAPPING_TIMEFRAME_DESC,
deprecated: { reason: 'Use timeframe.end', milestone: '14.0' }
argument :timeframe, Types::TimeframeInputType,
required: false,
description: 'List items overlapping the given timeframe'
end
# TODO: remove when the start_date and end_date arguments are removed
def validate_timeframe_params!(args)
return unless args[:start_date].present? || args[:end_date].present?
return unless %i[start_date end_date timeframe].any? { |k| args[k].present? }
return if args[:timeframe] && %i[start_date end_date].all? { |k| args[k].nil? }
error_message =
if args[:start_date].nil? || args[:end_date].nil?
if args[:timeframe].present?
"startDate and endDate are deprecated in favor of timeframe. Please use only timeframe."
elsif args[:start_date].nil? || args[:end_date].nil?
"Both startDate and endDate must be present."
elsif args[:start_date] > args[:end_date]
"startDate is after endDate"
......
......@@ -13,6 +13,18 @@ module Resolvers
required: false,
description: 'Filter milestones by state'
argument :title, GraphQL::STRING_TYPE,
required: false,
description: 'The title of the milestone'
argument :search_title, GraphQL::STRING_TYPE,
required: false,
description: 'A search string for the title'
argument :containing_date, Types::TimeType,
required: false,
description: 'A date that the milestone contains'
type Types::MilestoneType, null: true
def resolve(**args)
......@@ -29,9 +41,18 @@ module Resolvers
{
ids: parse_gids(args[:ids]),
state: args[:state] || 'all',
start_date: args[:start_date],
end_date: args[:end_date]
}.merge(parent_id_parameters(args))
title: args[:title],
search_title: args[:search_title],
containing_date: args[:containing_date]
}.merge!(timeframe_parameters(args)).merge!(parent_id_parameters(args))
end
def timeframe_parameters(args)
if args[:timeframe]
args[:timeframe].transform_keys { |k| :"#{k}_date" }
else
args.slice(:start_date, :end_date)
end
end
def parent
......
# frozen_string_literal: true
module Types
class BaseArgument < GraphQL::Schema::Argument
include GitlabStyleDeprecations
def initialize(*args, **kwargs, &block)
kwargs = gitlab_deprecation(kwargs)
kwargs.delete(:deprecation_reason)
super(*args, **kwargs, &block)
end
end
end
......@@ -5,6 +5,8 @@ module Types
prepend Gitlab::Graphql::Authorize
include GitlabStyleDeprecations
argument_class ::Types::BaseArgument
DEFAULT_COMPLEXITY = 1
def initialize(*args, **kwargs, &block)
......
......@@ -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
# frozen_string_literal: true
module Types
class DateType < BaseScalar
graphql_name 'Date'
description 'Date represented in ISO 8601'
def self.coerce_input(value, ctx)
return if value.nil?
Date.iso8601(value)
rescue ArgumentError, TypeError => e
raise GraphQL::CoercionError, e.message
end
def self.coerce_result(value, ctx)
return if value.nil?
value.to_date.iso8601
end
end
end
......@@ -30,7 +30,7 @@ module Types
# most recent `Version` for an issue
Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'stateful_version', object.issue_id, version_gid]) do
if version_gid
GitlabSchema.object_from_id(version_gid)&.sync
GitlabSchema.object_from_id(version_gid, expected_type: ::DesignManagement::Version)&.sync
else
object.issue.design_versions.most_recent
end
......
......@@ -23,6 +23,7 @@ module Types
mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::Discussions::ToggleResolve
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class RangeInputType < BaseInputObject
def self.[](type, closed = true)
@subtypes ||= {}
@subtypes[[type, closed]] ||= Class.new(self) do
argument :start, type,
required: closed,
description: 'The start of the range'
argument :end, type,
required: closed,
description: 'The end of the range'
end
end
def prepare
if self[:end] && self[:start] && self[:end] < self[:start]
raise ::Gitlab::Graphql::Errors::ArgumentError, 'start must be before end'
end
to_h
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class TimeframeInputType < RangeInputType[::Types::DateType]
graphql_name 'Timeframe'
description 'A time-frame defined as a closed inclusive range of two dates'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -73,6 +73,32 @@ module Timebox
end
end
# A timebox is within the timeframe (start_date, end_date) if it overlaps
# with that timeframe:
#
# [ timeframe ]
# ----| ................ # Not overlapping
# |--| ................ # Not overlapping
# ------|............... # Overlapping
# -----------------------| # Overlapping
# ---------|............ # Overlapping
# |-----|............ # Overlapping
# |--------------| # Overlapping
# |--------------------| # Overlapping
# ...|-----|...... # Overlapping
# .........|-----| # Overlapping
# .........|--------- # Overlapping
# |-------------------- # Overlapping
# .........|--------| # Overlapping
# ...............|--| # Overlapping
# ............... |-| # Not Overlapping
# ............... |-- # Not Overlapping
#
# where: . = in timeframe
# ---| no start
# |--- no end
# |--| defined start and end
#
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date)
......
# frozen_string_literal: true
class GlobalLabel
include Presentable
attr_accessor :title, :labels
alias_attribute :name, :title
delegate :color, :text_color, :description, :scoped_label?, to: :@first_label
def for_display
@first_label
end
def self.build_collection(labels)
labels = labels.group_by(&:title)
labels.map do |title, labels|
new(title, labels)
end
end
def initialize(title, labels)
@title = title
@labels = labels
@first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
def present(attributes)
super(attributes.merge(presenter_class: ::LabelPresenter))
end
end
......@@ -46,6 +46,10 @@ class Milestone < ApplicationRecord
state :active
end
def self.min_chars_for_partial_matching
2
end
def self.reference_prefix
'%'
end
......
......@@ -3,7 +3,6 @@
class Packages::Event < ApplicationRecord
belongs_to :package, optional: true
# FIXME: Remove debian: 9 from here when it's added to the types in package.rb model
EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze
enum event_scope: EVENT_SCOPES
......
......@@ -1711,7 +1711,7 @@ class User < ApplicationRecord
end
def can_be_deactivated?
active? && no_recent_activity?
active? && no_recent_activity? && !internal?
end
def last_active_at
......
......@@ -11,3 +11,5 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
issue.subscribed?(current_user, issue.project)
end
end
IssuePresenter.prepend_if_ee('EE::IssuePresenter')
......@@ -133,7 +133,7 @@ class BuildDetailsEntity < JobEntity
def callout_message
return super unless build.failure_reason.to_sym == :missing_dependency_failure
docs_url = "https://docs.gitlab.com/ce/ci/yaml/README.html#dependencies"
docs_url = "https://docs.gitlab.com/ee/ci/yaml/README.html#dependencies"
[
failure_message.html_safe,
......
# frozen_string_literal: true
class LabelEntity < Grape::Entity
expose :id, if: ->(label, _) { !label.is_a?(GlobalLabel) }
expose :id
expose :title
expose :color
......
......@@ -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
......@@ -34,6 +34,18 @@ module Issues
private
def filter_params(merge_request)
super
moved_issue = params.delete(:moved_issue)
# Setting created_at, updated_at and iid is allowed only for admins and owners or
# when moving an issue as we preserve the original issue attributes except id and iid.
params.delete(:iid) unless current_user.can?(:set_issue_iid, project)
params.delete(:created_at) unless moved_issue || current_user.can?(:set_issue_created_at, project)
params.delete(:updated_at) unless moved_issue || current_user.can?(:set_issue_updated_at, project)
end
def create_assignee_note(issue, old_assignees)
SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees)
......
......@@ -52,7 +52,8 @@ module Issues
iid: nil,
project: target_project,
author: original_entity.author,
assignee_ids: original_entity.assignee_ids
assignee_ids: original_entity.assignee_ids,
moved_issue: true
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
......
......@@ -2,11 +2,12 @@
module PersonalAccessTokens
class RevokeService
attr_reader :token, :current_user
attr_reader :token, :current_user, :group
def initialize(current_user = nil, params = { token: nil })
def initialize(current_user = nil, params = { token: nil, group: nil })
@current_user = current_user
@token = params[:token]
@group = params[:group]
end
def execute
......@@ -34,3 +35,5 @@ module PersonalAccessTokens
end
end
end
PersonalAccessTokens::RevokeService.prepend_if_ee('EE::PersonalAccessTokens::RevokeService')
......@@ -150,26 +150,27 @@
= render 'admin/users/user_detail_note'
- if @user.deactivated?
.card.border-info
.card-header.bg-info.text-white
Reactivate this user
.card-body
= render partial: 'admin/users/user_activation_effects'
%br
= link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
- elsif @user.can_be_deactivated?
.card.border-warning
.card-header.bg-warning.text-white
Deactivate this user
.card-body
= render partial: 'admin/users/user_deactivation_effects'
%br
%button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'deactivate',
content: 'You can always re-activate their account, their data will remain intact.',
url: deactivate_admin_user_path(@user),
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Deactivate user')
- unless @user.internal?
- if @user.deactivated?
.card.border-info
.card-header.bg-info.text-white
Reactivate this user
.card-body
= render partial: 'admin/users/user_activation_effects'
%br
= link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
- elsif @user.can_be_deactivated?
.card.border-warning
.card-header.bg-warning.text-white
Deactivate this user
.card-body
= render partial: 'admin/users/user_deactivation_effects'
%br
%button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'deactivate',
content: 'You can always re-activate their account, their data will remain intact.',
url: deactivate_admin_user_path(@user),
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Deactivate user')
- if @user.blocked?
.card.border-info
......
......@@ -32,7 +32,7 @@
= hidden_field_tag 'new_parent_group_id'
%ul
- side_effects_link_start = '<a href="https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths" target="_blank">'
- side_effects_link_start = '<a href="https://docs.gitlab.com/ee/user/project/index.html#redirects-when-changing-repository-paths" target="_blank">'
- warning_text = s_("GroupSettings|Be careful. Changing a group's parent can have unintended %{side_effects_link_start}side effects%{side_effects_link_end}.") % { side_effects_link_start: side_effects_link_start, side_effects_link_end:'</a>' }
%li= warning_text.html_safe
%li= s_('GroupSettings|You can only transfer the group to a group you manage.')
......
......@@ -22,7 +22,7 @@
- if ref
- if job.ref
.icon-container.gl-display-inline-block
= job.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
= job.tag? ? sprite_icon('label', css_class: 'sprite') : sprite_icon('fork', css_class: 'sprite')
= link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else
.light= _('none')
......@@ -33,10 +33,12 @@
= link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha mr-0"
- if job.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.'))
%span.has-tooltip{ title: _('Job is stuck. Check runners.') }
= sprite_icon('warning', css_class: 'text-warning!')
- if retried
= icon('refresh', class: 'text-warning has-tooltip', title: _('Job was retried'))
%span.has-tooltip{ title: _('Job was retried') }
= sprite_icon('retry', css_class: 'text-warning')
.label-container
- if job.tags.any?
......@@ -87,7 +89,7 @@
- if job.finished_at
%p.finished-at
= icon("calendar")
= sprite_icon("calendar")
%span= time_ago_with_tooltip(job.finished_at)
%td.coverage
......
......@@ -7,7 +7,7 @@
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.files-changed-inner
.inline-parallel-buttons.d-none.d-sm-none.d-md-block
.inline-parallel-buttons.d-none.d-md-block
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
= link_to _('Expand all'), url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
......
......@@ -7,6 +7,8 @@
"feature-flags-help-page-path" => help_page_path("operations/feature_flags"),
"feature-flags-client-libraries-help-page-path" => help_page_path("operations/feature_flags", anchor: "choose-a-client-library"),
"feature-flags-client-example-help-page-path" => help_page_path("operations/feature_flags", anchor: "golang-application-example"),
"feature-flags-limit-exceeded" => @project.actual_limits.exceeded?(:project_feature_flags, @project.operations_feature_flags.count),
"feature-flags-limit" => @project.actual_limits.project_feature_flags,
"unleash-api-url" => (unleash_api_url(@project) if can?(current_user, :admin_feature_flag, @project)),
"unleash-api-instance-id" => (unleash_api_instance_id(@project) if can?(current_user, :admin_feature_flag, @project)),
"can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project),
......
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, currentRoutePath: current_route_path })
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path })
- breadcrumb_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
......
......@@ -118,7 +118,7 @@
- else
- selected_labels = issuable_sidebar[:labels]
.block.labels{ data: { qa_selector: 'labels_block' } }
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
.sidebar-collapsed-icon.has-tooltip.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
= sprite_icon('labels')
%span
= selected_labels.size
......
---
title: Add GraphQL mutation to create an issue
merge_request: 43735
author:
type: added
---
title: Schedule adding "Missed SLA" label to issues
merge_request: 44546
author:
type: added
---
title: Add Issuable Service Level Agreement (SLA) table
merge_request: 44253
author:
type: added
---
title: Add migration helpers for copying check constraints
merge_request: 44777
author:
type: other
---
title: Remove duplicated BS display properties from Diff's HAML
merge_request: 44848
author: Takuya Noguchi
type: other
---
title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
merge_request: 41424
author: nuwe1
type: other
---
title: Add filters on Milestone title in the GraphQL API
merge_request: 44208
author:
type: changed
---
title: Add product analytics for design created and modified events
merge_request: 44129
author:
type: added
---
title: Feature Flags limits UX and documentation
merge_request: 44089
author:
type: added
---
title: Bump kubeclient to 4.9.1 which includes ability to integrate Kubernetes clusters
where their API url is on a sub-path
merge_request: 44856
author:
type: other
---
title: 'GraphQL: Adds scheduledAt to CiJob'
merge_request: 44054
author:
type: added
---
title: 'GraphQL: Changes fields in detailedStatus to be nullable'
merge_request: 45072
author:
type: changed
---
title: Fix Rails/SaveBang offenses in spec/services/projects/*
merge_request: 44980
author: matthewbried
type: other
---
title: Replace fa icons in CI build table
merge_request: 45123
author:
type: changed
---
title: Fixed incorrect parameter in GraphQL startup call
merge_request: 45115
author:
type: fixed
---
title: Fix incorrect HTTP response in deactivate user API for internal user
merge_request: 43356
author: Sashi Kumar
type: fixed
......@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36622
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/245183
group: group::continuous integration
type: development
default_enabled: false
default_enabled: true
......@@ -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
......
......@@ -26,18 +26,18 @@ PATTERNS = %w[
gl-deprecated-dropdown-divider
gl-deprecated-dropdown-header
gl-deprecated-dropdown-item
graphql_pagination
has-tooltip
has_tooltip
initDeprecatedJQueryDropdown
loading-button
pagination-button
v-popover
v-tooltip
with_tooltip
].freeze
BLOCKING_PATTERNS = %w[
pagination-button
graphql_pagination
].freeze
def get_added_lines(files)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment