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 @@ ...@@ -28,6 +28,8 @@
# Help pages are excluded from scan as they are static pages. # 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. # 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"' - '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/" ; } - 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 # 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 ",")' - '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 ...@@ -21,12 +21,10 @@ https://docs.gitlab.com/ee/development/documentation/index.html#move-or-rename-a
a link to the new location. a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken. - [ ] 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, - [ ] 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. 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) - [ ] 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. to the new document if there are any Disqus comments on the old document thread.
- [ ] Update the link in `features.yml` (if applicable) - [ ] Update the link in `features.yml` (if applicable)
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE - [ ] Assign one of the technical writers for review.
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.
/label ~documentation /label ~documentation
...@@ -362,6 +362,15 @@ Graphql/AuthorizeTypes: ...@@ -362,6 +362,15 @@ Graphql/AuthorizeTypes:
- 'spec/**/*.rb' - 'spec/**/*.rb'
- 'ee/spec/**/*.rb' - 'ee/spec/**/*.rb'
Graphql/GIDExpectedType:
Enabled: true
Include:
- 'app/graphql/**/*'
- 'ee/app/graphql/**/*'
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Graphql/JSONType: Graphql/JSONType:
Enabled: true Enabled: true
Include: Include:
......
...@@ -1141,19 +1141,6 @@ Rails/SaveBang: ...@@ -1141,19 +1141,6 @@ Rails/SaveBang:
- 'spec/services/notification_recipients/build_service_spec.rb' - 'spec/services/notification_recipients/build_service_spec.rb'
- 'spec/services/notification_service_spec.rb' - 'spec/services/notification_service_spec.rb'
- 'spec/services/packages/conan/create_package_file_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/reset_project_cache_service_spec.rb'
- 'spec/services/resource_events/change_milestone_service_spec.rb' - 'spec/services/resource_events/change_milestone_service_spec.rb'
- 'spec/services/system_hooks_service_spec.rb' - 'spec/services/system_hooks_service_spec.rb'
......
...@@ -259,7 +259,7 @@ gem 'asana', '0.10.2' ...@@ -259,7 +259,7 @@ gem 'asana', '0.10.2'
gem 'ruby-fogbugz', '~> 0.2.1' gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration # Kubernetes integration
gem 'kubeclient', '~> 4.6.0' gem 'kubeclient', '~> 4.9.1'
# Sanitize user input # Sanitize user input
gem 'sanitize', '~> 5.2.1' gem 'sanitize', '~> 5.2.1'
......
...@@ -253,7 +253,7 @@ GEM ...@@ -253,7 +253,7 @@ GEM
discordrb-webhooks-blackst0ne (3.3.0) discordrb-webhooks-blackst0ne (3.3.0)
rest-client (~> 2.0) rest-client (~> 2.0)
docile (1.3.2) docile (1.3.2)
domain_name (0.5.20180417) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.3.3) doorkeeper (5.3.3)
railties (>= 5) railties (>= 5)
...@@ -563,14 +563,15 @@ GEM ...@@ -563,14 +563,15 @@ GEM
html2text (0.2.0) html2text (0.2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
htmlentities (4.3.4) htmlentities (4.3.4)
http (4.2.0) http (4.4.1)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.0) http-form_data (~> 2.2)
http-parser (~> 1.2.0) http-parser (~> 1.2.0)
http-accept (1.7.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.3.0)
http-parser (1.2.1) http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0) ffi-compiler (>= 1.0, < 2.0)
httparty (0.16.4) httparty (0.16.4)
...@@ -611,6 +612,9 @@ GEM ...@@ -611,6 +612,9 @@ GEM
hana (~> 1.3) hana (~> 1.3)
regexp_parser (~> 1.5) regexp_parser (~> 1.5)
uri_template (~> 0.7) uri_template (~> 0.7)
jsonpath (1.0.5)
multi_json
to_regexp (~> 0.2.1)
jwt (2.1.0) jwt (2.1.0)
kaminari (1.2.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
...@@ -631,9 +635,10 @@ GEM ...@@ -631,9 +635,10 @@ GEM
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
kubeclient (4.6.0) kubeclient (4.9.1)
http (>= 3.0, < 5.0) 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) rest-client (~> 2.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
...@@ -847,7 +852,7 @@ GEM ...@@ -847,7 +852,7 @@ GEM
pry (~> 0.13.0) pry (~> 0.13.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.3) public_suffix (4.0.6)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6) raabro (1.1.6)
rack (2.1.4) rack (2.1.4)
...@@ -920,7 +925,7 @@ GEM ...@@ -920,7 +925,7 @@ GEM
re2 (1.2.0) re2 (1.2.0)
recaptcha (4.13.1) recaptcha (4.13.1)
json json
recursive-open-struct (1.1.1) recursive-open-struct (1.1.2)
redis (4.1.3) redis (4.1.3)
redis-actionpack (5.2.0) redis-actionpack (5.2.0)
actionpack (>= 5, < 7) actionpack (>= 5, < 7)
...@@ -951,7 +956,8 @@ GEM ...@@ -951,7 +956,8 @@ GEM
responders (3.0.0) responders (3.0.0)
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 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) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
...@@ -1145,6 +1151,7 @@ GEM ...@@ -1145,6 +1151,7 @@ GEM
timecop (0.9.1) timecop (0.9.1)
timeliness (0.3.10) timeliness (0.3.10)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
to_regexp (0.2.1)
toml (0.2.0) toml (0.2.0)
parslet (~> 1.8.0) parslet (~> 1.8.0)
toml-rb (1.0.0) toml-rb (1.0.0)
...@@ -1161,7 +1168,7 @@ GEM ...@@ -1161,7 +1168,7 @@ GEM
uber (0.1.0) uber (0.1.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.7)
unicode-display_width (1.7.0) unicode-display_width (1.7.0)
unicode_plot (0.0.4) unicode_plot (0.0.4)
enumerable-statistics (>= 2.0.1) enumerable-statistics (>= 2.0.1)
...@@ -1371,7 +1378,7 @@ DEPENDENCIES ...@@ -1371,7 +1378,7 @@ DEPENDENCIES
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.17) knapsack (~> 1.17)
kramdown (~> 2.3.0) kramdown (~> 2.3.0)
kubeclient (~> 4.6.0) kubeclient (~> 4.9.1)
letter_opener_web (~> 1.3.4) letter_opener_web (~> 1.3.4)
license_finder (~> 6.0) license_finder (~> 6.0)
licensee (~> 8.9) licensee (~> 8.9)
......
...@@ -73,7 +73,7 @@ sensitive as to how you word things. Use Emoji to express your feelings (heart, ...@@ -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 star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines]. [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 ## Feature flags
...@@ -217,5 +217,5 @@ rebase with master to see if that solves the issue. ...@@ -217,5 +217,5 @@ rebase with master to see if that solves the issue.
[team]: https://about.gitlab.com/team/ [team]: https://about.gitlab.com/team/
[done]: https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#definition-of-done [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 [automatic_ce_ee_merge]: https://docs.gitlab.com/ee/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html [ee_features]: https://docs.gitlab.com/ee/development/ee_features.html
...@@ -376,7 +376,7 @@ const Api = { ...@@ -376,7 +376,7 @@ const Api = {
}, },
commitMultiple(id, data) { 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)); const url = Api.buildUrl(Api.commitsPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), { return axios.post(url, JSON.stringify(data), {
headers: { headers: {
......
...@@ -5,6 +5,7 @@ import { handleLocationHash } from '../../lib/utils/common_utils'; ...@@ -5,6 +5,7 @@ import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { fixTitle } from '~/tooltips';
const loadRichBlobViewer = type => { const loadRichBlobViewer = type => {
switch (type) { switch (type) {
...@@ -124,7 +125,7 @@ export default class BlobViewer { ...@@ -124,7 +125,7 @@ export default class BlobViewer {
this.copySourceBtn.classList.add('disabled'); this.copySourceBtn.classList.add('disabled');
} }
$(this.copySourceBtn).tooltip('_fixTitle'); fixTitle($(this.copySourceBtn));
} }
switchToViewer(name) { switchToViewer(name) {
......
...@@ -2,11 +2,24 @@ import { sortBy } from 'lodash'; ...@@ -2,11 +2,24 @@ import { sortBy } from 'lodash';
import ListIssue from 'ee_else_ce/boards/models/issue'; import ListIssue from 'ee_else_ce/boards/models/issue';
import { ListType } from './constants'; import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
export function getMilestone() { export function getMilestone() {
return null; 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) { export function formatIssue(issue) {
return new ListIssue({ return new ListIssue({
...issue, ...issue,
...@@ -62,6 +75,13 @@ export function fullBoardId(boardId) { ...@@ -62,6 +75,13 @@ export function fullBoardId(boardId) {
return `gid://gitlab/Board/${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) { export function moveIssueListHelper(issue, fromList, toList) {
if (toList.type === ListType.label) { if (toList.type === ListType.label) {
issue.addLabel(toList.label); issue.addLabel(toList.label);
...@@ -85,4 +105,5 @@ export default { ...@@ -85,4 +105,5 @@ export default {
formatIssue, formatIssue,
formatListIssues, formatListIssues,
fullBoardId, fullBoardId,
fullLabelId,
}; };
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { sortBy } from 'lodash';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
...@@ -30,7 +31,9 @@ export default { ...@@ -30,7 +31,9 @@ export default {
...mapState(['boardLists', 'error']), ...mapState(['boardLists', 'error']),
...mapGetters(['isSwimlanesOn']), ...mapGetters(['isSwimlanesOn']),
boardListsToUse() { 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() { mounted() {
...@@ -68,7 +71,7 @@ export default { ...@@ -68,7 +71,7 @@ export default {
<template v-else> <template v-else>
<epics-swimlanes <epics-swimlanes
ref="swimlanes" ref="swimlanes"
:lists="boardLists" :lists="boardListsToUse"
:can-admin-list="canAdminList" :can-admin-list="canAdminList"
:disabled="disabled" :disabled="disabled"
/> />
......
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
referencing a List Model class. Reactivity only applies to plain JS objects referencing a List Model class. Reactivity only applies to plain JS objects
*/ */
if (this.glFeatures.graphqlBoardLists) { 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); return boardsStore.state.lists.find(({ id }) => id === this.activeId);
}, },
......
...@@ -6,8 +6,14 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -6,8 +6,14 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash'; import { deprecatedCreateFlash as flash } from '~/flash';
import CreateLabelDropdown from '../../create_label'; import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import { fullLabelId } from '../boards_util';
import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
function shouldCreateListGraphQL(label) {
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
}
$(document) $(document)
.off('created.label') .off('created.label')
.on('created.label', (e, label, addNewList) => { .on('created.label', (e, label, addNewList) => {
...@@ -15,16 +21,20 @@ $(document) ...@@ -15,16 +21,20 @@ $(document)
return; return;
} }
boardsStore.new({ if (shouldCreateListGraphQL(label)) {
title: label.title, store.dispatch('createList', { labelId: fullLabelId(label) });
position: boardsStore.state.lists.length - 2, } else {
list_type: 'label', boardsStore.new({
label: {
id: label.id,
title: label.title, 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() { export default function initNewListDropdown() {
...@@ -74,7 +84,9 @@ export default function initNewListDropdown() { ...@@ -74,7 +84,9 @@ export default function initNewListDropdown() {
const label = options.selectedObj; const label = options.selectedObj;
e.preventDefault(); e.preventDefault();
if (!boardsStore.findListByLabelId(label.id)) { if (shouldCreateListGraphQL(label)) {
store.dispatch('createList', { labelId: fullLabelId(label) });
} else if (!boardsStore.findListByLabelId(label.id)) {
boardsStore.new({ boardsStore.new({
title: label.title, title: label.title,
position: boardsStore.state.lists.length - 2, 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) { mutation CreateBoardList(
boardListCreate(input: { boardId: $boardId, backlog: $backlog }) { $boardId: BoardID!
$backlog: Boolean
$labelId: LabelID
$milestoneId: MilestoneID
$assigneeId: UserID
) {
boardListCreate(
input: {
boardId: $boardId
backlog: $backlog
labelId: $labelId
milestoneId: $milestoneId
assigneeId: $assigneeId
}
) {
list { list {
...BoardListFragment ...BoardListFragment
} }
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { sortBy, pick } from 'lodash'; import { pick } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { BoardType, ListType, inactiveId } from '~/boards/constants'; import { BoardType, ListType, inactiveId } from '~/boards/constants';
import * as types from './mutation_types'; 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 boardStore from '~/boards/stores/boards_store';
import listsIssuesQuery from '../queries/lists_issues.query.graphql'; import listsIssuesQuery from '../queries/lists_issues.query.graphql';
...@@ -71,38 +75,29 @@ export default { ...@@ -71,38 +75,29 @@ export default {
variables, variables,
}) })
.then(({ data }) => { .then(({ data }) => {
let { lists } = data[boardType]?.board; const { lists } = data[boardType]?.board;
// Temporarily using positioning logic from boardStore commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists));
lists = lists.nodes.map(list =>
boardStore.updateListPosition({
...list,
doNotFetchIssues: true,
}),
);
commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position'));
// Backlog list needs to be created if it doesn't exist // 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('createList', { backlog: true });
} }
dispatch('showWelcomeList'); dispatch('showWelcomeList');
}) })
.catch(() => { .catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
createFlash(
__('An error occurred while fetching the board lists. Please reload the page.'),
);
});
}, },
// This action only supports backlog list creation at this stage createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
// Future iterations will add the ability to create other list types
createList: ({ state, commit, dispatch }, { backlog = false }) => {
const { boardId } = state.endpoints; const { boardId } = state.endpoints;
gqlClient gqlClient
.mutate({ .mutate({
mutation: createBoardListMutation, mutation: createBoardListMutation,
variables: { variables: {
boardId: fullBoardId(boardId), boardId: fullBoardId(boardId),
backlog, backlog,
labelId,
milestoneId,
assigneeId,
}, },
}) })
.then(({ data }) => { .then(({ data }) => {
...@@ -113,16 +108,15 @@ export default { ...@@ -113,16 +108,15 @@ export default {
dispatch('addList', list); dispatch('addList', list);
} }
}) })
.catch(() => { .catch(() => commit(types.CREATE_LIST_FAILURE));
commit(types.CREATE_LIST_FAILURE);
});
}, },
addList: ({ state, commit }, list) => { addList: ({ commit }, list) => {
const lists = state.boardLists;
// Temporarily using positioning logic from boardStore // Temporarily using positioning logic from boardStore
lists.push(boardStore.updateListPosition({ ...list, doNotFetchIssues: true })); commit(
commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position')); types.RECEIVE_ADD_LIST_SUCCESS,
boardStore.updateListPosition({ ...list, doNotFetchIssues: true }),
);
}, },
showWelcomeList: ({ state, dispatch }) => { showWelcomeList: ({ state, dispatch }) => {
...@@ -130,7 +124,9 @@ export default { ...@@ -130,7 +124,9 @@ export default {
return; return;
} }
if ( 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; return;
} }
...@@ -152,13 +148,16 @@ export default { ...@@ -152,13 +148,16 @@ export default {
notImplemented(); notImplemented();
}, },
moveList: ({ state, commit, dispatch }, { listId, newIndex, adjustmentValue }) => { moveList: (
{ state, commit, dispatch },
{ listId, replacedListId, newIndex, adjustmentValue },
) => {
const { boardLists } = state; const { boardLists } = state;
const backupList = [...boardLists]; const backupList = { ...boardLists };
const movedList = boardLists.find(({ id }) => id === listId); const movedList = boardLists[listId];
const newPosition = newIndex - 1; const newPosition = newIndex - 1;
const listAtNewIndex = boardLists[newIndex]; const listAtNewIndex = boardLists[replacedListId];
movedList.position = newPosition; movedList.position = newPosition;
listAtNewIndex.position += adjustmentValue; listAtNewIndex.position += adjustmentValue;
......
import { find } from 'lodash';
import { inactiveId } from '../constants'; import { inactiveId } from '../constants';
export default { export default {
...@@ -22,4 +23,16 @@ export default { ...@@ -22,4 +23,16 @@ export default {
getActiveIssue: state => { getActiveIssue: state => {
return state.issues[state.activeId] || {}; 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'; ...@@ -3,6 +3,7 @@ export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS'; export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE'; export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; 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 SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST'; export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
......
import Vue from 'vue'; import Vue from 'vue';
import { sortBy, pull, union } from 'lodash'; import { pull, union } from 'lodash';
import { formatIssue, moveIssueListHelper } from '../boards_util'; import { formatIssue, moveIssueListHelper } from '../boards_util';
import * as mutationTypes from './mutation_types'; import * as mutationTypes from './mutation_types';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -10,16 +10,10 @@ const notImplemented = () => { ...@@ -10,16 +10,10 @@ const notImplemented = () => {
throw new Error('Not implemented!'); 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 }) => { export const removeIssueFromList = ({ state, listId, issueId }) => {
Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId)); Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
const { listIndex, list } = getListById({ state, listId }); const list = state.boardLists[listId];
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize - 1 }); Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize - 1 });
}; };
export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => { export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
...@@ -32,8 +26,8 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter ...@@ -32,8 +26,8 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
} }
listIssues.splice(newIndex, 0, issueId); listIssues.splice(newIndex, 0, issueId);
Vue.set(state.issuesByListId, listId, listIssues); Vue.set(state.issuesByListId, listId, listIssues);
const { listIndex, list } = getListById({ state, listId }); const list = state.boardLists[listId];
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize + 1 }); Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize + 1 });
}; };
export default { export default {
...@@ -49,6 +43,12 @@ export default { ...@@ -49,6 +43,12 @@ export default {
state.boardLists = lists; 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 }) { [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id; state.activeId = id;
state.sidebarType = sidebarType; state.sidebarType = sidebarType;
...@@ -66,8 +66,8 @@ export default { ...@@ -66,8 +66,8 @@ export default {
notImplemented(); notImplemented();
}, },
[mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: () => { [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: (state, list) => {
notImplemented(); Vue.set(state.boardLists, list.id, list);
}, },
[mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => { [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => {
...@@ -76,10 +76,8 @@ export default { ...@@ -76,10 +76,8 @@ export default {
[mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => { [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => {
const { boardLists } = state; const { boardLists } = state;
const movedListIndex = state.boardLists.findIndex(l => l.id === movedList.id); Vue.set(boardLists, movedList.id, movedList);
Vue.set(boardLists, movedListIndex, movedList); Vue.set(boardLists, listAtNewIndex.id, listAtNewIndex);
Vue.set(boardLists, movedListIndex.position + 1, listAtNewIndex);
Vue.set(state, 'boardLists', sortBy(boardLists, 'position'));
}, },
[mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => { [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => {
...@@ -156,8 +154,8 @@ export default { ...@@ -156,8 +154,8 @@ export default {
state, state,
{ originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId },
) => { ) => {
const fromList = state.boardLists.find(l => l.id === fromListId); const fromList = state.boardLists[fromListId];
const toList = state.boardLists.find(l => l.id === toListId); const toList = state.boardLists[toListId];
const issue = moveIssueListHelper(originalIssue, fromList, toList); const issue = moveIssueListHelper(originalIssue, fromList, toList);
Vue.set(state.issues, issue.id, issue); Vue.set(state.issues, issue.id, issue);
......
...@@ -8,7 +8,7 @@ export default () => ({ ...@@ -8,7 +8,7 @@ export default () => ({
isShowingLabels: true, isShowingLabels: true,
activeId: inactiveId, activeId: inactiveId,
sidebarType: '', sidebarType: '',
boardLists: [], boardLists: {},
listsFlags: {}, listsFlags: {},
issuesByListId: {}, issuesByListId: {},
pageInfoByListId: {}, pageInfoByListId: {},
......
import $ from 'jquery'; import $ from 'jquery';
import { hide } from '~/tooltips';
export const addTooltipToEl = el => { export const addTooltipToEl = el => {
const textEl = el.querySelector('.js-breadcrumb-item-text'); const textEl = el.querySelector('.js-breadcrumb-item-text');
...@@ -23,9 +24,11 @@ export default () => { ...@@ -23,9 +24,11 @@ export default () => {
topLevelLinks.forEach(el => addTooltipToEl(el)); topLevelLinks.forEach(el => addTooltipToEl(el));
$expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => { $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => {
$('.js-breadcrumbs-collapsed-expander', e.currentTarget) const $el = $('.js-breadcrumbs-collapsed-expander', e.currentTarget);
.toggleClass('open')
.tooltip('hide'); $el.toggleClass('open');
hide($el);
}); });
} }
}; };
...@@ -360,7 +360,7 @@ export default { ...@@ -360,7 +360,7 @@ export default {
> >
<template #link="{ content }"> <template #link="{ content }">
<gl-link <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" target="_blank"
>{{ content }}</gl-link >{{ content }}</gl-link
> >
......
...@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
import UploadButton from '../components/upload/button.vue'; import UploadButton from '../components/upload/button.vue';
import DeleteButton from '../components/delete_button.vue'; import DeleteButton from '../components/delete_button.vue';
import Design from '../components/list/item.vue'; import Design from '../components/list/item.vue';
...@@ -31,7 +32,7 @@ import { ...@@ -31,7 +32,7 @@ import {
isValidDesignFile, isValidDesignFile,
moveDesignOptimisticResponse, moveDesignOptimisticResponse,
} from '../utils/design_management_utils'; } 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'; import { DESIGNS_ROUTE_NAME } from '../router/constants';
const MAXIMUM_FILE_UPLOAD_LIMIT = 10; const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
...@@ -186,6 +187,7 @@ export default { ...@@ -186,6 +187,7 @@ export default {
updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody); updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody);
}, },
onUploadDesignDone(res) { onUploadDesignDone(res) {
// display any warnings, if necessary
const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || []; const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || [];
const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles); const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles);
if (skippedWarningMessage) { if (skippedWarningMessage) {
...@@ -196,7 +198,19 @@ export default { ...@@ -196,7 +198,19 @@ export default {
if (!this.isLatestVersion) { if (!this.isLatestVersion) {
this.$router.push({ name: DESIGNS_ROUTE_NAME }); this.$router.push({ name: DESIGNS_ROUTE_NAME });
} }
// reset state
this.resetFilesToBeSaved(); 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() { onUploadDesignError() {
this.resetFilesToBeSaved(); this.resetFilesToBeSaved();
......
import Tracking from '~/tracking'; import Tracking from '~/tracking';
// Tracking Constants // Tracking Constants
const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0'; const DESIGN_TRACKING_CONTEXT_SCHEMAS = {
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; VIEW_DESIGN_SCHEMA: 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0',
const DESIGN_TRACKING_EVENT_NAME = 'view_design'; };
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( export function trackDesignDetailView(
referer = '', referer = '',
...@@ -11,10 +18,11 @@ export function trackDesignDetailView( ...@@ -11,10 +18,11 @@ export function trackDesignDetailView(
designVersion = 1, designVersion = 1,
latestVersion = false, latestVersion = false,
) { ) {
Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, { const eventName = DESIGN_TRACKING_EVENTS.VIEW_DESIGN;
label: DESIGN_TRACKING_EVENT_NAME, Tracking.event(DESIGN_TRACKING_PAGE_NAME, eventName, {
label: eventName,
context: { context: {
schema: DESIGN_TRACKING_CONTEXT_SCHEMA, schema: DESIGN_TRACKING_CONTEXT_SCHEMAS.VIEW_DESIGN_SCHEMA,
data: { data: {
'design-version-number': designVersion, 'design-version-number': designVersion,
'design-is-current-version': latestVersion, 'design-is-current-version': latestVersion,
...@@ -24,3 +32,11 @@ export function trackDesignDetailView( ...@@ -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'; ...@@ -5,6 +5,14 @@ import getUnicodeSupportMap from './unicode_support_map';
let browserUnicodeSupportMap; let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) { 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(); browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion); return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
} }
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash'; 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 { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
import FeatureFlagsTab from './feature_flags_tab.vue'; import FeatureFlagsTab from './feature_flags_tab.vue';
import FeatureFlagsTable from './feature_flags_table.vue'; import FeatureFlagsTable from './feature_flags_table.vue';
...@@ -9,9 +10,9 @@ import UserListsTable from './user_lists_table.vue'; ...@@ -9,9 +10,9 @@ import UserListsTable from './user_lists_table.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { import {
buildUrlWithCurrentLocation,
getParameterByName, getParameterByName,
historyPushState, historyPushState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue'; import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue';
...@@ -20,13 +21,15 @@ const SCOPES = { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE }; ...@@ -20,13 +21,15 @@ const SCOPES = { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE };
export default { export default {
components: { components: {
ConfigureFeatureFlagsModal,
FeatureFlagsTab,
FeatureFlagsTable, FeatureFlagsTable,
UserListsTable, GlAlert,
TablePagination,
GlButton, GlButton,
GlSprintf,
GlTabs, GlTabs,
FeatureFlagsTab, TablePagination,
ConfigureFeatureFlagsModal, UserListsTable,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -44,6 +47,20 @@ export default { ...@@ -44,6 +47,20 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
featureFlagsLimit: {
type: String,
required: true,
},
featureFlagsLimitExceeded: {
type: Boolean,
required: false,
default: false,
},
rotateInstanceIdPath: {
type: String,
required: false,
default: '',
},
unleashApiUrl: { unleashApiUrl: {
type: String, type: String,
required: true, required: true,
...@@ -69,6 +86,7 @@ export default { ...@@ -69,6 +86,7 @@ export default {
scope, scope,
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
isUserListAlertDismissed: false, isUserListAlertDismissed: false,
shouldShowFeatureFlagsLimitWarning: this.featureFlagsLimitExceeded,
selectedTab: Object.values(SCOPES).indexOf(scope), selectedTab: Object.values(SCOPES).indexOf(scope),
}; };
}, },
...@@ -184,11 +202,36 @@ export default { ...@@ -184,11 +202,36 @@ export default {
dataForScope(scope) { dataForScope(scope) {
return this[scope]; return this[scope];
}, },
onDismissFeatureFlagsLimitWarning() {
this.shouldShowFeatureFlagsLimitWarning = false;
},
onNewFeatureFlagCLick() {
if (this.featureFlagsLimitExceeded) {
this.shouldShowFeatureFlagsLimitWarning = true;
}
},
}, },
}; };
</script> </script>
<template> <template>
<div> <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 <configure-feature-flags-modal
v-if="canUserConfigure" v-if="canUserConfigure"
:help-client-libraries-path="featureFlagsClientLibrariesHelpPagePath" :help-client-libraries-path="featureFlagsClientLibrariesHelpPagePath"
...@@ -228,9 +271,10 @@ export default { ...@@ -228,9 +271,10 @@ export default {
<gl-button <gl-button
v-if="hasNewPath" v-if="hasNewPath"
:href="newFeatureFlagPath" :href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath"
variant="success" variant="success"
data-testid="ff-new-button" data-testid="ff-new-button"
@click="onNewFeatureFlagCLick"
> >
{{ s__('FeatureFlags|New feature flag') }} {{ s__('FeatureFlags|New feature flag') }}
</gl-button> </gl-button>
...@@ -306,9 +350,10 @@ export default { ...@@ -306,9 +350,10 @@ export default {
<gl-button <gl-button
v-if="hasNewPath" v-if="hasNewPath"
:href="newFeatureFlagPath" :href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath"
variant="success" variant="success"
data-testid="ff-new-button" data-testid="ff-new-button"
@click="onNewFeatureFlagCLick"
> >
{{ s__('FeatureFlags|New feature flag') }} {{ s__('FeatureFlags|New feature flag') }}
</gl-button> </gl-button>
......
...@@ -36,6 +36,8 @@ export default () => { ...@@ -36,6 +36,8 @@ export default () => {
el.dataset.featureFlagsClientLibrariesHelpPagePath, el.dataset.featureFlagsClientLibrariesHelpPagePath,
featureFlagsClientExampleHelpPagePath: el.dataset.featureFlagsClientExampleHelpPagePath, featureFlagsClientExampleHelpPagePath: el.dataset.featureFlagsClientExampleHelpPagePath,
unleashApiUrl: el.dataset.unleashApiUrl, unleashApiUrl: el.dataset.unleashApiUrl,
featureFlagsLimitExceeded: el.dataset.featureFlagsLimitExceeded,
featureFlagsLimit: el.dataset.featureFlagsLimit,
csrfToken: csrf.token, csrfToken: csrf.token,
canUserConfigure: el.dataset.canUserAdminFeatureFlag, canUserConfigure: el.dataset.canUserAdminFeatureFlag,
newFeatureFlagPath: el.dataset.newFeatureFlagPath, newFeatureFlagPath: el.dataset.newFeatureFlagPath,
......
...@@ -16,53 +16,63 @@ const frequentItemDropdowns = [ ...@@ -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() { export default function initFrequentItemDropdowns() {
frequentItemDropdowns.forEach(dropdown => { frequentItemDropdowns.forEach(dropdown => {
const { namespace, key } = dropdown; const { namespace, key } = dropdown;
const el = document.getElementById(`js-${namespace}-dropdown`);
const navEl = document.getElementById(`nav-${namespace}-dropdown`); const navEl = document.getElementById(`nav-${namespace}-dropdown`);
// Don't do anything if element doesn't exist (No groups dropdown) // Don't do anything if element doesn't exist (No groups dropdown)
// This is for when the user accesses GitLab without logging in // This is for when the user accesses GitLab without logging in
if (!el || !navEl) { if (!navEl) {
return; 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', () => { $(navEl).on('shown.bs.dropdown', () => {
initFrequentItemList(namespace, key);
eventHub.$emit(`${namespace}-dropdownOpen`); eventHub.$emit(`${namespace}-dropdownOpen`);
}); });
}); });
......
...@@ -5,6 +5,7 @@ import { formatDate } from '~/lib/utils/datetime_utility'; ...@@ -5,6 +5,7 @@ import { formatDate } from '~/lib/utils/datetime_utility';
export default { export default {
components: { components: {
GlLink, GlLink,
IncidentSla: () => import('ee_component/issue_show/components/incidents/incident_sla.vue'),
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -12,36 +13,51 @@ export default { ...@@ -12,36 +13,51 @@ export default {
props: { props: {
alert: { alert: {
type: Object, type: Object,
required: true, required: false,
default: null,
}, },
}, },
data() {
return { childHasData: false };
},
computed: { computed: {
startTime() { startTime() {
return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z'); return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z');
}, },
showHighlightBar() {
return this.alert || this.childHasData;
},
},
methods: {
update(hasData) {
this.childHasData = hasData;
},
}, },
}; };
</script> </script>
<template> <template>
<div <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" 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> <span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl"> <gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
#{{ alert.iid }} #{{ alert.iid }}
</gl-link> </gl-link>
</div> </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> <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
{{ startTime }} {{ startTime }}
</div> </div>
<div> <div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span> <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
<span>{{ alert.eventCount }}</span> <span>{{ alert.eventCount }}</span>
</div> </div>
<incident-sla @update="update" />
</div> </div>
</template> </template>
...@@ -53,7 +53,7 @@ export default { ...@@ -53,7 +53,7 @@ export default {
<div> <div>
<gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs"> <gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs">
<gl-tab :title="s__('Incident|Summary')"> <gl-tab :title="s__('Incident|Summary')">
<highlight-bar v-if="alert" :alert="alert" /> <highlight-bar :alert="alert" />
<description-component v-bind="$attrs" /> <description-component v-bind="$attrs" />
</gl-tab> </gl-tab>
<gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')"> <gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')">
......
...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; ...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue'; import incidentTabs from './components/incidents/incident_tabs.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -11,7 +12,7 @@ export default function initIssuableApp(issuableData = {}) { ...@@ -11,7 +12,7 @@ export default function initIssuableApp(issuableData = {}) {
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
}); });
const { projectNamespace, projectPath, iid } = issuableData; const { iid, projectNamespace, projectPath, slaFeatureAvailable } = issuableData;
return new Vue({ return new Vue({
el: document.getElementById('js-issuable-app'), el: document.getElementById('js-issuable-app'),
...@@ -22,6 +23,7 @@ export default function initIssuableApp(issuableData = {}) { ...@@ -22,6 +23,7 @@ export default function initIssuableApp(issuableData = {}) {
provide: { provide: {
fullPath: `${projectNamespace}/${projectPath}`, fullPath: `${projectNamespace}/${projectPath}`,
iid, iid,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
}, },
render(createElement) { render(createElement) {
return createElement('issuable-app', { return createElement('issuable-app', {
......
...@@ -13,6 +13,7 @@ import ModalStore from './boards/stores/modal_store'; ...@@ -13,6 +13,7 @@ import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store'; import boardsStore from './boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { fixTitle } from '~/tooltips';
export default class LabelsSelect { export default class LabelsSelect {
constructor(els, options = {}) { constructor(els, options = {}) {
...@@ -57,7 +58,6 @@ export default class LabelsSelect { ...@@ -57,7 +58,6 @@ export default class LabelsSelect {
.get(); .get();
const scopedLabels = $dropdown.data('scopedLabels'); const scopedLabels = $dropdown.data('scopedLabels');
const { handleClick } = options; const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new CreateLabelDropdown( new CreateLabelDropdown(
...@@ -166,7 +166,8 @@ export default class LabelsSelect { ...@@ -166,7 +166,8 @@ export default class LabelsSelect {
labelTooltipTitle = __('Labels'); labelTooltipTitle = __('Labels');
} }
$sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle'); $sidebarLabelTooltip.attr('title', labelTooltipTitle);
fixTitle($sidebarLabelTooltip);
$('.has-tooltip', $value).tooltip({ $('.has-tooltip', $value).tooltip({
container: 'body', container: 'body',
......
import Project from './project'; import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => { new Project(); // eslint-disable-line no-new
new Project(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
});
...@@ -3,7 +3,7 @@ import { ...@@ -3,7 +3,7 @@ import {
GlFilteredSearchToken, GlFilteredSearchToken,
GlAvatar, GlAvatar,
GlFilteredSearchSuggestion, GlFilteredSearchSuggestion,
GlDeprecatedDropdownDivider, GlDropdownDivider,
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
...@@ -21,7 +21,7 @@ export default { ...@@ -21,7 +21,7 @@ export default {
GlFilteredSearchToken, GlFilteredSearchToken,
GlAvatar, GlAvatar,
GlFilteredSearchSuggestion, GlFilteredSearchSuggestion,
GlDeprecatedDropdownDivider, GlDropdownDivider,
GlLoadingIcon, GlLoadingIcon,
}, },
props: { props: {
...@@ -94,7 +94,7 @@ export default { ...@@ -94,7 +94,7 @@ export default {
<gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{ <gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{
$options.anyTriggerAuthor $options.anyTriggerAuthor
}}</gl-filtered-search-suggestion> }}</gl-filtered-search-suggestion>
<gl-deprecated-dropdown-divider /> <gl-dropdown-divider />
<gl-loading-icon v-if="loading" /> <gl-loading-icon v-if="loading" />
<template v-else> <template v-else>
......
<script> <script>
import { GlProgressBar } from '@gitlab/ui'; import { GlProgressBar, GlTooltipDirective } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
export default { export default {
...@@ -10,7 +9,7 @@ export default { ...@@ -10,7 +9,7 @@ export default {
GlProgressBar, GlProgressBar,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
timeSpent: { timeSpent: {
...@@ -73,7 +72,7 @@ export default { ...@@ -73,7 +72,7 @@ export default {
<template> <template>
<div class="time-tracking-comparison-pane"> <div class="time-tracking-comparison-pane">
<div <div
v-tooltip v-gl-tooltip
:title="timeRemainingTooltip" :title="timeRemainingTooltip"
:class="timeRemainingStatusClass" :class="timeRemainingStatusClass"
class="compare-meter" class="compare-meter"
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
@import '@gitlab/at.js/dist/css/jquery.atwho'; @import '@gitlab/at.js/dist/css/jquery.atwho';
@import 'dropzone/dist/basic'; @import 'dropzone/dist/basic';
@import 'select2'; @import 'select2';
@import 'cropper/dist/cropper';
// GitLab UI framework // GitLab UI framework
@import 'framework'; @import 'framework';
......
...@@ -215,7 +215,7 @@ ...@@ -215,7 +215,7 @@
} }
&.build-trace-rounded { &.build-trace-rounded {
border-radius: $border-radius-base; border-radius: $gl-border-radius-base;
} }
} }
......
...@@ -8,24 +8,24 @@ ...@@ -8,24 +8,24 @@
.external-url, .external-url,
.dropdown-new { .dropdown-new {
color: $gl-text-color-secondary; color: var(--gray-500, $gray-500);
} }
.build-link, .build-link,
.ref-name { .ref-name {
color: $gl-text-color; color: var(--gray-900, $gray-900);
} }
.folder-icon { .folder-icon {
margin-right: 3px; margin-right: 3px;
color: $gl-text-color-secondary; color: var(--gray-500, $gray-500);
display: inline-block; display: inline-block;
vertical-align: text-top; vertical-align: text-top;
} }
.folder-name { .folder-name {
cursor: pointer; cursor: pointer;
color: $gl-text-color-secondary; color: var(--gray-500, $gray-500);
display: inline-block; display: inline-block;
} }
...@@ -74,17 +74,17 @@ ...@@ -74,17 +74,17 @@
.x-axis path, .x-axis path,
.y-axis path { .y-axis path {
stroke: $gray-300; stroke: var(--gray-300, $gray-300);
} }
.label-x-axis-line, .label-x-axis-line,
.label-y-axis-line { .label-y-axis-line {
stroke: $border-color; stroke: var(--gray-100, $gray-100);
} }
.y-axis { .y-axis {
line { line {
stroke: $gray-300; stroke: var(--gray-300, $gray-300);
stroke-width: 1; stroke-width: 1;
} }
} }
...@@ -94,13 +94,13 @@ ...@@ -94,13 +94,13 @@
} }
.rect-text-metric { .rect-text-metric {
fill: $white; fill: var(--white, $white);
stroke-width: 1; stroke-width: 1;
stroke: $gray-darkest; stroke: var(--gray-600, $gray-600);
} }
.rect-axis-text { .rect-axis-text {
fill: $white; fill: var(--white, $white);
} }
.text-metric { .text-metric {
...@@ -108,18 +108,18 @@ ...@@ -108,18 +108,18 @@
} }
.selected-metric-line { .selected-metric-line {
stroke: $gray-900; stroke: var(--gray-900, $gray-900);
stroke-width: 1; stroke-width: 1;
} }
.deployment-line { .deployment-line {
stroke: $black; stroke: var(--white, $white);
stroke-width: 1; stroke-width: 1;
} }
.divider-line { .divider-line {
stroke-width: 1; stroke-width: 1;
stroke: $gray-darkest; stroke: var(--gray-600, $gray-600);
} }
.environments-actions { .environments-actions {
......
...@@ -476,3 +476,9 @@ ...@@ -476,3 +476,9 @@
height: auto !important; height: auto !important;
} }
} }
.test-reports-table {
.build-trace {
@include build-trace();
}
}
...@@ -28,7 +28,10 @@ ...@@ -28,7 +28,10 @@
height: 14px; height: 14px;
width: 14px; width: 14px;
vertical-align: middle; vertical-align: middle;
fill: $gl-text-color-secondary;
&:not(.text-warning) {
fill: $gl-text-color-secondary;
}
} }
.sprite { .sprite {
...@@ -130,12 +133,6 @@ ...@@ -130,12 +133,6 @@
float: none; float: none;
} }
.test-reports-table {
.build-trace {
@include build-trace();
}
}
.progress-bar.bg-primary { .progress-bar.bg-primary {
background-color: $blue-500 !important; background-color: $blue-500 !important;
} }
......
...@@ -72,6 +72,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -72,6 +72,7 @@ class Admin::UsersController < Admin::ApplicationController
def deactivate 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: _("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: _("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? 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 user.deactivate
......
...@@ -11,8 +11,8 @@ class Dashboard::LabelsController < Dashboard::ApplicationController ...@@ -11,8 +11,8 @@ class Dashboard::LabelsController < Dashboard::ApplicationController
def labels def labels
finder_params = { project_ids: projects.select(:id) } 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
end end
...@@ -25,7 +25,7 @@ module Ci ...@@ -25,7 +25,7 @@ module Ci
attr_reader :current_user, :pipeline, :project, :params, :type attr_reader :current_user, :pipeline, :project, :params, :type
def init_collection 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 pipeline_jobs || project_jobs || all_jobs
else else
project ? project_builds : all_jobs project ? project_builds : all_jobs
...@@ -59,7 +59,7 @@ module Ci ...@@ -59,7 +59,7 @@ module Ci
end end
def filter_by_scope(builds) 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) return filter_by_statuses!(params[:scope], builds) if params[:scope].is_a?(Array)
end end
......
...@@ -11,4 +11,11 @@ module TimeFrameFilter ...@@ -11,4 +11,11 @@ module TimeFrameFilter
rescue ArgumentError rescue ArgumentError
items items
end end
def containing_date(items)
return items unless params[:containing_date]
date = params[:containing_date].to_date
items.within_timeframe(date, date)
end
end end
...@@ -102,7 +102,7 @@ class IssuableFinder ...@@ -102,7 +102,7 @@ class IssuableFinder
items = filter_items(items) items = filter_items(items)
# Let's see if we have to negate anything # 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 # 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 # for counts by passing the force_cte param and passing the
...@@ -134,13 +134,15 @@ class IssuableFinder ...@@ -134,13 +134,15 @@ class IssuableFinder
by_my_reaction_emoji(items) by_my_reaction_emoji(items)
end end
# Negates all params found in `negatable_params` def should_filter_negated_args?
def filter_negated_items(items) return false unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true)
return items 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 # 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_author(items)
items = by_negated_assignee(items) items = by_negated_assignee(items)
items = by_negated_label(items) items = by_negated_label(items)
......
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
# order - Orders by field default due date asc. # order - Orders by field default due date asc.
# title - filter by title. # title - filter by title.
# state - filters by state. # state - filters by state.
# start_date & end_date - filters by timeframe (see TimeFrameFilter)
# containing_date - filters by point in time (see TimeFrameFilter)
class MilestonesFinder class MilestonesFinder
include FinderMethods include FinderMethods
...@@ -28,6 +30,7 @@ class MilestonesFinder ...@@ -28,6 +30,7 @@ class MilestonesFinder
items = by_search_title(items) items = by_search_title(items)
items = by_state(items) items = by_state(items)
items = by_timeframe(items) items = by_timeframe(items)
items = containing_date(items)
order(items) order(items)
end 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 ...@@ -5,49 +5,26 @@ module Mutations
class Update < Base class Update < Base
graphql_name 'UpdateIssue' graphql_name 'UpdateIssue'
argument :title, include CommonMutationArguments
GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :title)
argument :description, argument :title, GraphQL::STRING_TYPE,
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,
required: false, required: false,
description: copy_field_description(Types::IssueType, :discussion_locked) description: copy_field_description(Types::IssueType, :title)
argument :add_label_ids, argument :milestone_id, GraphQL::ID_TYPE,
[GraphQL::ID_TYPE],
required: false, 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, argument :add_label_ids, [GraphQL::ID_TYPE],
[GraphQL::ID_TYPE],
required: false, 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, argument :remove_label_ids, [GraphQL::ID_TYPE],
GraphQL::ID_TYPE,
required: false, 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, argument :state_event, Types::IssueStateEventEnum,
description: 'Close or reopen an issue.', description: 'Close or reopen an issue',
required: false required: false
def resolve(project_path:, iid:, **args) def resolve(project_path:, iid:, **args)
......
...@@ -17,7 +17,7 @@ module Mutations ...@@ -17,7 +17,7 @@ module Mutations
discussion_id = nil discussion_id = nil
if args[:discussion_id] 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) authorize_discussion!(discussion)
discussion_id = discussion.id discussion_id = discussion.id
......
...@@ -6,6 +6,8 @@ module Resolvers ...@@ -6,6 +6,8 @@ module Resolvers
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::GlobalIDCompatibility include ::Gitlab::Graphql::GlobalIDCompatibility
argument_class ::Types::BaseArgument
def self.single def self.single
@single ||= Class.new(self) do @single ||= Class.new(self) do
def ready?(**args) def ready?(**args)
......
...@@ -3,21 +3,33 @@ ...@@ -3,21 +3,33 @@
module TimeFrameArguments module TimeFrameArguments
extend ActiveSupport::Concern 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 included do
argument :start_date, Types::TimeType, argument :start_date, Types::TimeType,
required: false, 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, argument :end_date, Types::TimeType,
required: false, 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 end
# TODO: remove when the start_date and end_date arguments are removed
def validate_timeframe_params!(args) 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 = 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." "Both startDate and endDate must be present."
elsif args[:start_date] > args[:end_date] elsif args[:start_date] > args[:end_date]
"startDate is after endDate" "startDate is after endDate"
......
...@@ -13,6 +13,18 @@ module Resolvers ...@@ -13,6 +13,18 @@ module Resolvers
required: false, required: false,
description: 'Filter milestones by state' 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 type Types::MilestoneType, null: true
def resolve(**args) def resolve(**args)
...@@ -29,9 +41,18 @@ module Resolvers ...@@ -29,9 +41,18 @@ module Resolvers
{ {
ids: parse_gids(args[:ids]), ids: parse_gids(args[:ids]),
state: args[:state] || 'all', state: args[:state] || 'all',
start_date: args[:start_date], title: args[:title],
end_date: args[:end_date] search_title: args[:search_title],
}.merge(parent_id_parameters(args)) 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 end
def parent 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 ...@@ -5,6 +5,8 @@ module Types
prepend Gitlab::Graphql::Authorize prepend Gitlab::Graphql::Authorize
include GitlabStyleDeprecations include GitlabStyleDeprecations
argument_class ::Types::BaseArgument
DEFAULT_COMPLEXITY = 1 DEFAULT_COMPLEXITY = 1
def initialize(*args, **kwargs, &block) def initialize(*args, **kwargs, &block)
......
...@@ -6,22 +6,22 @@ module Types ...@@ -6,22 +6,22 @@ module Types
class DetailedStatusType < BaseObject class DetailedStatusType < BaseObject
graphql_name 'DetailedStatus' graphql_name 'DetailedStatus'
field :group, GraphQL::STRING_TYPE, null: false, field :group, GraphQL::STRING_TYPE, null: true,
description: 'Group of the status' description: 'Group of the status'
field :icon, GraphQL::STRING_TYPE, null: false, field :icon, GraphQL::STRING_TYPE, null: true,
description: 'Icon of the status' description: 'Icon of the status'
field :favicon, GraphQL::STRING_TYPE, null: false, field :favicon, GraphQL::STRING_TYPE, null: true,
description: 'Favicon of the status' description: 'Favicon of the status'
field :details_path, GraphQL::STRING_TYPE, null: true, field :details_path, GraphQL::STRING_TYPE, null: true,
description: 'Path of the details for the status' 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', description: 'Indicates if the status has further details',
method: :has_details? method: :has_details?
field :label, GraphQL::STRING_TYPE, null: false, field :label, GraphQL::STRING_TYPE, null: true,
description: 'Label of the status' description: 'Label of the status'
field :text, GraphQL::STRING_TYPE, null: false, field :text, GraphQL::STRING_TYPE, null: true,
description: 'Text of the status' 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', description: 'Tooltip associated with the status',
method: :status_tooltip method: :status_tooltip
field :action, Types::Ci::StatusActionType, null: true, field :action, Types::Ci::StatusActionType, null: true,
......
...@@ -13,6 +13,8 @@ module Types ...@@ -13,6 +13,8 @@ module Types
field :detailed_status, Types::Ci::DetailedStatusType, null: true, field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job', description: 'Detailed status of the job',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } 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 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 ...@@ -30,7 +30,7 @@ module Types
# most recent `Version` for an issue # most recent `Version` for an issue
Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'stateful_version', object.issue_id, version_gid]) do Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'stateful_version', object.issue_id, version_gid]) do
if version_gid if version_gid
GitlabSchema.object_from_id(version_gid)&.sync GitlabSchema.object_from_id(version_gid, expected_type: ::DesignManagement::Version)&.sync
else else
object.issue.design_versions.most_recent object.issue.design_versions.most_recent
end end
......
...@@ -23,6 +23,7 @@ module Types ...@@ -23,6 +23,7 @@ module Types
mount_mutation Mutations::Branches::Create, calls_gitaly: true mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Commits::Create, calls_gitaly: true mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::Discussions::ToggleResolve mount_mutation Mutations::Discussions::ToggleResolve
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked 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 ...@@ -73,6 +73,32 @@ module Timebox
end end
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 scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL') where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date) .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 ...@@ -46,6 +46,10 @@ class Milestone < ApplicationRecord
state :active state :active
end end
def self.min_chars_for_partial_matching
2
end
def self.reference_prefix def self.reference_prefix
'%' '%'
end end
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
class Packages::Event < ApplicationRecord class Packages::Event < ApplicationRecord
belongs_to :package, optional: true 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 EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze
enum event_scope: EVENT_SCOPES enum event_scope: EVENT_SCOPES
......
...@@ -1711,7 +1711,7 @@ class User < ApplicationRecord ...@@ -1711,7 +1711,7 @@ class User < ApplicationRecord
end end
def can_be_deactivated? def can_be_deactivated?
active? && no_recent_activity? active? && no_recent_activity? && !internal?
end end
def last_active_at def last_active_at
......
...@@ -11,3 +11,5 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated ...@@ -11,3 +11,5 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
issue.subscribed?(current_user, issue.project) issue.subscribed?(current_user, issue.project)
end end
end end
IssuePresenter.prepend_if_ee('EE::IssuePresenter')
...@@ -133,7 +133,7 @@ class BuildDetailsEntity < JobEntity ...@@ -133,7 +133,7 @@ class BuildDetailsEntity < JobEntity
def callout_message def callout_message
return super unless build.failure_reason.to_sym == :missing_dependency_failure 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, failure_message.html_safe,
......
# frozen_string_literal: true # frozen_string_literal: true
class LabelEntity < Grape::Entity class LabelEntity < Grape::Entity
expose :id, if: ->(label, _) { !label.is_a?(GlobalLabel) } expose :id
expose :title expose :title
expose :color expose :color
......
...@@ -4,6 +4,6 @@ class LabelSerializer < BaseSerializer ...@@ -4,6 +4,6 @@ class LabelSerializer < BaseSerializer
entity LabelEntity entity LabelEntity
def represent_appearance(resource) def represent_appearance(resource)
represent(resource, { only: [:id, :title, :color, :text_color] }) represent(resource, { only: [:id, :title, :color, :text_color, :project_id] })
end end
end end
...@@ -34,6 +34,18 @@ module Issues ...@@ -34,6 +34,18 @@ module Issues
private 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) def create_assignee_note(issue, old_assignees)
SystemNoteService.change_issuable_assignees( SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees) issue, issue.project, current_user, old_assignees)
......
...@@ -52,7 +52,8 @@ module Issues ...@@ -52,7 +52,8 @@ module Issues
iid: nil, iid: nil,
project: target_project, project: target_project,
author: original_entity.author, 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) new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
module PersonalAccessTokens module PersonalAccessTokens
class RevokeService 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 @current_user = current_user
@token = params[:token] @token = params[:token]
@group = params[:group]
end end
def execute def execute
...@@ -34,3 +35,5 @@ module PersonalAccessTokens ...@@ -34,3 +35,5 @@ module PersonalAccessTokens
end end
end end
end end
PersonalAccessTokens::RevokeService.prepend_if_ee('EE::PersonalAccessTokens::RevokeService')
...@@ -150,26 +150,27 @@ ...@@ -150,26 +150,27 @@
= render 'admin/users/user_detail_note' = render 'admin/users/user_detail_note'
- if @user.deactivated? - unless @user.internal?
.card.border-info - if @user.deactivated?
.card-header.bg-info.text-white .card.border-info
Reactivate this user .card-header.bg-info.text-white
.card-body Reactivate this user
= render partial: 'admin/users/user_activation_effects' .card-body
%br = render partial: 'admin/users/user_activation_effects'
= link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' } %br
- elsif @user.can_be_deactivated? = link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
.card.border-warning - elsif @user.can_be_deactivated?
.card-header.bg-warning.text-white .card.border-warning
Deactivate this user .card-header.bg-warning.text-white
.card-body Deactivate this user
= render partial: 'admin/users/user_deactivation_effects' .card-body
%br = render partial: 'admin/users/user_deactivation_effects'
%button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'deactivate', %br
content: 'You can always re-activate their account, their data will remain intact.', %button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'deactivate',
url: deactivate_admin_user_path(@user), content: 'You can always re-activate their account, their data will remain intact.',
username: sanitize_name(@user.name) } } url: deactivate_admin_user_path(@user),
= s_('AdminUsers|Deactivate user') username: sanitize_name(@user.name) } }
= s_('AdminUsers|Deactivate user')
- if @user.blocked? - if @user.blocked?
.card.border-info .card.border-info
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
= hidden_field_tag 'new_parent_group_id' = hidden_field_tag 'new_parent_group_id'
%ul %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>' } - 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= warning_text.html_safe
%li= s_('GroupSettings|You can only transfer the group to a group you manage.') %li= s_('GroupSettings|You can only transfer the group to a group you manage.')
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
- if ref - if ref
- if job.ref - if job.ref
.icon-container.gl-display-inline-block .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" = link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else - else
.light= _('none') .light= _('none')
...@@ -33,10 +33,12 @@ ...@@ -33,10 +33,12 @@
= link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha mr-0" = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha mr-0"
- if job.stuck? - 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 - 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 .label-container
- if job.tags.any? - if job.tags.any?
...@@ -87,7 +89,7 @@ ...@@ -87,7 +89,7 @@
- if job.finished_at - if job.finished_at
%p.finished-at %p.finished-at
= icon("calendar") = sprite_icon("calendar")
%span= time_ago_with_tooltip(job.finished_at) %span= time_ago_with_tooltip(job.finished_at)
%td.coverage %td.coverage
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.files-changed-inner .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? } - 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' = link_to _('Expand all'), url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle - if show_whitespace_toggle
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
"feature-flags-help-page-path" => help_page_path("operations/feature_flags"), "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-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-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-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)), "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), "can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project),
......
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1] - 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") - breadcrumb_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
......
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
- else - else
- selected_labels = issuable_sidebar[:labels] - selected_labels = issuable_sidebar[:labels]
.block.labels{ data: { qa_selector: 'labels_block' } } .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') = sprite_icon('labels')
%span %span
= selected_labels.size = 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 ...@@ -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 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/245183
group: group::continuous integration group: group::continuous integration
type: development type: development
default_enabled: false default_enabled: true
...@@ -574,6 +574,9 @@ Gitlab.ee do ...@@ -574,6 +574,9 @@ Gitlab.ee do
Settings.cron_jobs['historical_data_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['historical_data_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['historical_data_worker']['cron'] ||= '0 12 * * *' Settings.cron_jobs['historical_data_worker']['cron'] ||= '0 12 * * *'
Settings.cron_jobs['historical_data_worker']['job_class'] = 'HistoricalDataWorker' 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'] ||= Settingslogic.new({})
Settings.cron_jobs['import_software_licenses_worker']['cron'] ||= '0 3 * * 0' Settings.cron_jobs['import_software_licenses_worker']['cron'] ||= '0 3 * * 0'
Settings.cron_jobs['import_software_licenses_worker']['job_class'] = 'ImportSoftwareLicensesWorker' Settings.cron_jobs['import_software_licenses_worker']['job_class'] = 'ImportSoftwareLicensesWorker'
......
...@@ -142,6 +142,8 @@ ...@@ -142,6 +142,8 @@
- 2 - 2
- - incident_management - - incident_management
- 2 - 2
- - incident_management_apply_incident_sla_exceeded_label
- 1
- - invalid_gpg_signature_update - - invalid_gpg_signature_update
- 2 - 2
- - irker - - irker
......
...@@ -26,18 +26,18 @@ PATTERNS = %w[ ...@@ -26,18 +26,18 @@ PATTERNS = %w[
gl-deprecated-dropdown-divider gl-deprecated-dropdown-divider
gl-deprecated-dropdown-header gl-deprecated-dropdown-header
gl-deprecated-dropdown-item gl-deprecated-dropdown-item
graphql_pagination
has-tooltip has-tooltip
has_tooltip has_tooltip
initDeprecatedJQueryDropdown initDeprecatedJQueryDropdown
loading-button loading-button
pagination-button
v-popover v-popover
v-tooltip v-tooltip
with_tooltip with_tooltip
].freeze ].freeze
BLOCKING_PATTERNS = %w[ BLOCKING_PATTERNS = %w[
pagination-button
graphql_pagination
].freeze ].freeze
def get_added_lines(files) 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