summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-03 15:24:33 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-03 15:24:33 +0000
commit2a7fd3827b0838a900399b0c3440942cdaa09c75 (patch)
treeadb9315f24704a322c0c3e05da2f2b8e835fbf60
parent6e228f38c37c4c7b6d6be648ae2664ebfb5c3c80 (diff)
downloadgitlab-ce-2a7fd3827b0838a900399b0c3440942cdaa09c75.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.checksum8
-rw-r--r--Gemfile.lock18
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue29
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue7
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue28
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue157
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue65
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue12
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/stylesheets/framework/mixins.scss8
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss23
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss14
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss4
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss4
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss5
-rw-r--r--app/graphql/types/permission_types/work_item.rb2
-rw-r--r--app/models/ci/processable.rb2
-rw-r--r--config/feature_flags/development/rollup_timebox_chart.yml8
-rw-r--r--data/removals/15_9/15-9-omniauth-authentiq.yml11
-rw-r--r--data/removals/15_9/15-9-omniauth-shibboleth.yml11
-rw-r--r--doc/administration/environment_variables.md1
-rw-r--r--doc/api/dora/metrics.md2
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/update/removals.md16
-rw-r--r--doc/user/group/saml_sso/index.md5
-rw-r--r--doc/user/product_analytics/index.md4
-rw-r--r--lib/feature.rb6
-rw-r--r--lib/feature_groups/gitlab_team_members.rb31
-rw-r--r--lib/gitlab/ci/config.rb2
-rw-r--r--package.json4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb10
-rw-r--r--spec/features/projects/work_items/work_item_spec.rb13
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js43
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js17
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js84
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js105
-rw-r--r--spec/frontend/work_items/mock_data.js13
-rw-r--r--spec/graphql/types/permission_types/work_item_spec.rb2
-rw-r--r--spec/lib/feature_groups/gitlab_team_members_spec.rb65
-rw-r--r--spec/lib/feature_spec.rb27
-rw-r--r--spec/lib/gitlab/jwt_authenticatable_spec.rb8
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb4
-rw-r--r--spec/lib/json_web_token/hmac_token_spec.rb4
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb22
-rw-r--r--spec/requests/api/projects_spec.rb1
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb34
-rw-r--r--yarn.lock18
52 files changed, 604 insertions, 379 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5d233f92ccb..de0f925f962 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-07810d3f4a30529cc56b991c2c086600876d9f67
+d7baefaebfcd0beaee08f12d14080f64d4a7aae7
diff --git a/Gemfile b/Gemfile
index e44a4525544..f7962ba17d0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -45,8 +45,8 @@ gem 'declarative_policy', '~> 1.1.0'
gem 'devise', '~> 4.8.1'
gem 'devise-pbkdf2-encryptable', '~> 0.0.0', path: 'vendor/gems/devise-pbkdf2-encryptable'
gem 'bcrypt', '~> 3.1', '>= 3.1.14'
-gem 'doorkeeper', '~> 5.5'
-gem 'doorkeeper-openid_connect', '~> 1.8'
+gem 'doorkeeper', '~> 5.6', '>= 5.6.6'
+gem 'doorkeeper-openid_connect', '~> 1.8', '>= 1.8.5'
gem 'rexml', '~> 3.2.5'
gem 'ruby-saml', '~> 1.13.0'
gem 'omniauth', '~> 2.1.0'
@@ -71,7 +71,7 @@ gem 'openid_connect', '= 1.3.0'
gem 'omniauth-salesforce', '~> 1.0.5', path: 'vendor/gems/omniauth-salesforce' # See gem README.md
gem 'omniauth-atlassian-oauth2', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.21.3'
-gem 'jwt', '~> 2.1.0'
+gem 'jwt', '~> 2.5'
# Kerberos authentication. EE-only
gem 'gssapi', '~> 1.3.1', group: :kerberos
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 4218d69bc52..0fccab1a66d 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -29,7 +29,7 @@
{"name":"asciidoctor-kroki","version":"0.8.0","platform":"ruby","checksum":"e53b3f349167cebde990b0098863e8fe98fd235e35263a78c88cc4e0268b1a36"},
{"name":"asciidoctor-plantuml","version":"0.0.16","platform":"ruby","checksum":"407e47cd1186ded5ccc75f0c812e5524c26c571d542247c5132abb8f47bd1793"},
{"name":"ast","version":"2.4.2","platform":"ruby","checksum":"1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12"},
-{"name":"atlassian-jwt","version":"0.2.0","platform":"ruby","checksum":"52e653e9d6062d7a740c3675b0e79fa08367927c6fc17f5476d1b6b3798c6eb2"},
+{"name":"atlassian-jwt","version":"0.2.1","platform":"ruby","checksum":"2fd2d87418773f2e140c038cb22e049069708aff2bd0a423a7e1740574e97823"},
{"name":"attr_required","version":"1.0.1","platform":"ruby","checksum":"024e10393bd30901e1adf6769bd756b873a5ef7da60f86f8f11066116b5742bc"},
{"name":"autoprefixer-rails","version":"10.2.5.1","platform":"ruby","checksum":"3711d67f1112361c7628847ac192d8aa6f3b8abe47527aee8a69dc8985e798ee"},
{"name":"awesome_print","version":"1.9.2","platform":"ruby","checksum":"e99b32b704acff16d768b3468680793ced40bfdc4537eb07e06a4be11133786e"},
@@ -119,8 +119,8 @@
{"name":"discordrb-webhooks","version":"3.4.2","platform":"ruby","checksum":"cfdba8a4b28236b6ab34e37389f881a59c241aeb5be0a4447249efd4e4383c6e"},
{"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"},
{"name":"domain_name","version":"0.5.20190701","platform":"ruby","checksum":"000a600454cb4a344769b2f10b531765ea7bd3a304fe47ed12e5ca1eab969851"},
-{"name":"doorkeeper","version":"5.5.4","platform":"ruby","checksum":"7fe233a96f93bf0d5496e2284abf431f38ab465fd65d1972b90cbec7c45b1ea1"},
-{"name":"doorkeeper-openid_connect","version":"1.8.3","platform":"ruby","checksum":"0df2e714508f1f43fdb4669e97b38b90d365a072908427416da943a1a8e00b6e"},
+{"name":"doorkeeper","version":"5.6.6","platform":"ruby","checksum":"2344e86c77770526efcda893b5217aa13d1c7eb1b40de840b58b19eb1ff757e0"},
+{"name":"doorkeeper-openid_connect","version":"1.8.5","platform":"ruby","checksum":"d4ee57687945402843c948cee399c758cdddf04468c42b1fb02a8800dd0627f6"},
{"name":"dotenv","version":"2.7.6","platform":"ruby","checksum":"2451ed5e8e43776d7a787e51d6f8903b98e446146c7ad143d5678cc2c409d547"},
{"name":"dry-configurable","version":"0.12.0","platform":"ruby","checksum":"87a9579a04dfbae73e401d694282800d64bbdb8631cb3e987bfb79b673df7c67"},
{"name":"dry-container","version":"0.7.2","platform":"ruby","checksum":"a071824ba3451048b23500210f96a2b9facd6e46ac687f65e49c75d18786f6da"},
@@ -316,7 +316,7 @@
{"name":"json-jwt","version":"1.15.3","platform":"ruby","checksum":"66db4f14e538a774c15502a5b5b26b1f3e7585481bbb96df490aa74b5c2d6110"},
{"name":"json_schemer","version":"0.2.18","platform":"ruby","checksum":"3362c21efbefdd12ce994e541a1e7fdb86fd267a6541dd8715e8a580fe3b6be6"},
{"name":"jsonpath","version":"1.1.2","platform":"ruby","checksum":"6804124c244d04418218acb85b15c7caa79c592d7d6970195300428458946d3a"},
-{"name":"jwt","version":"2.1.0","platform":"ruby","checksum":"7e7e7ffc1a5ebce628ac7da428341c50615a3a10ac47bb74c22c1cba325613f0"},
+{"name":"jwt","version":"2.5.0","platform":"ruby","checksum":"b835fe55287572e1f65128d6c12d3ff7402bb4652c4565bf3ecdcb974db7954d"},
{"name":"kaminari","version":"1.2.2","platform":"ruby","checksum":"c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e"},
{"name":"kaminari-actionview","version":"1.2.2","platform":"ruby","checksum":"1330f6fc8b59a4a4ef6a549ff8a224797289ebf7a3a503e8c1652535287cc909"},
{"name":"kaminari-activerecord","version":"1.2.2","platform":"ruby","checksum":"0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 7d8c2675b78..6e3b013e787 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -199,8 +199,8 @@ GEM
asciidoctor-plantuml (0.0.16)
asciidoctor (>= 2.0.17, < 3.0.0)
ast (2.4.2)
- atlassian-jwt (0.2.0)
- jwt (~> 2.1.0)
+ atlassian-jwt (0.2.1)
+ jwt (~> 2.1)
attr_required (1.0.1)
autoprefixer-rails (10.2.5.1)
execjs (> 0)
@@ -397,11 +397,11 @@ GEM
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (5.5.4)
+ doorkeeper (5.6.6)
railties (>= 5)
- doorkeeper-openid_connect (1.8.3)
+ doorkeeper-openid_connect (1.8.5)
doorkeeper (>= 5.5, < 5.7)
- json-jwt (>= 1.15.0)
+ jwt (>= 2.5)
dotenv (2.7.6)
dry-configurable (0.12.0)
concurrent-ruby (~> 1.0)
@@ -853,7 +853,7 @@ GEM
uri_template (~> 0.7)
jsonpath (1.1.2)
multi_json
- jwt (2.1.0)
+ jwt (2.5.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -1703,8 +1703,8 @@ DEPENDENCIES
diff_match_patch (~> 0.1.0)
diffy (~> 3.4)
discordrb-webhooks (~> 3.4)
- doorkeeper (~> 5.5)
- doorkeeper-openid_connect (~> 1.8)
+ doorkeeper (~> 5.6, >= 5.6.6)
+ doorkeeper-openid_connect (~> 1.8, >= 1.8.5)
duo_api (~> 1.3)
ed25519 (~> 1.3.0)
elasticsearch-api (= 7.13.3)
@@ -1790,7 +1790,7 @@ DEPENDENCIES
js_regex (~> 3.8)
json (~> 2.6.3)
json_schemer (~> 0.2.18)
- jwt (~> 2.1.0)
+ jwt (~> 2.5)
kaminari (~> 1.2.2)
kas-grpc (~> 0.0.2)
knapsack (~> 1.21.1)
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index d1be7967569..2675099a2f5 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -128,7 +128,7 @@ export default {
>
<div class="gl-pb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
- <gl-icon name="search" class="position-absolute tree-list-icon" />
+ <gl-icon name="search" class="gl-absolute gl-top-5 tree-list-icon" />
<label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label>
<input
id="diff-tree-search"
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index 00fb813728e..b7a9583cae9 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -2,7 +2,6 @@
import { GlButton, GlCollapse } from '@gitlab/ui';
import { __ } from '~/locale';
import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
-import { SUPER_SIDEBAR_PEEK_DELAY } from '../constants';
import UserBar from './user_bar.vue';
import SidebarPortalTarget from './sidebar_portal_target.vue';
import ContextSwitcherToggle from './context_switcher_toggle.vue';
@@ -33,8 +32,7 @@ export default {
data() {
return {
contextSwitcherOpen: false,
- isInert: isCollapsed(),
- isPeek: false,
+ isCollapased: isCollapsed(),
};
},
computed: {
@@ -49,18 +47,6 @@ export default {
onContextSwitcherShown() {
this.$refs['context-switcher'].focusInput();
},
- onMouseOver() {
- setTimeout(() => {
- this.isPeek = true;
- this.isInert = false;
- }, SUPER_SIDEBAR_PEEK_DELAY);
- },
- onMouseLeave() {
- setTimeout(() => {
- this.isPeek = false;
- this.isInert = true;
- }, SUPER_SIDEBAR_PEEK_DELAY);
- },
},
};
</script>
@@ -68,21 +54,14 @@ export default {
<template>
<div>
<div class="super-sidebar-overlay" @click="collapseSidebar"></div>
- <div
- v-if="!isPeek"
- class="super-sidebar-hover-area gl-fixed gl-left-0 gl-top-0 gl-bottom-0 gl-w-3"
- data-testid="super-sidebar-hover-area"
- @mouseover="onMouseOver"
- ></div>
<aside
id="super-sidebar"
class="super-sidebar"
- :class="{ 'gl-visibility-hidden': isInert, 'super-sidebar-peek': isPeek }"
+ :class="{ 'gl-visibility-hidden': isCollapased }"
data-testid="super-sidebar"
data-qa-selector="navbar"
- :inert="isInert"
+ :inert="isCollapased"
tabindex="-1"
- @mouseleave="onMouseLeave"
>
<gl-button
class="super-sidebar-skip-to gl-sr-only-focusable gl-absolute gl-left-3 gl-right-3 gl-top-3"
@@ -91,7 +70,7 @@ export default {
>
{{ $options.i18n.skipToMainContent }}
</gl-button>
- <user-bar :has-collapse-button="!isPeek" :sidebar-data="sidebarData" />
+ <user-bar :sidebar-data="sidebarData" />
<div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden">
<div class="gl-flex-grow-1 gl-overflow-auto">
<context-switcher-toggle
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 34af935434c..62161f2846a 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -50,11 +50,6 @@ export default {
},
inject: ['rootPath'],
props: {
- hasCollapseButton: {
- default: true,
- type: Boolean,
- required: false,
- },
sidebarData: {
type: Object,
required: true,
@@ -94,12 +89,10 @@ export default {
>
<div class="gl-flex-grow-1"></div>
<gl-button
- v-if="hasCollapseButton"
v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.collapseSidebar"
aria-controls="super-sidebar"
aria-expanded="true"
:aria-label="$options.i18n.navigationSidebar"
- data-testid="super-sidebar-collapse-button"
icon="sidebar"
category="tertiary"
@click="collapseSidebar"
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index 23233ab7792..ad9d4bc43f2 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -13,7 +13,5 @@ export const portalState = Vue.observable({
export const MAX_FREQUENT_PROJECTS_COUNT = 5;
export const MAX_FREQUENT_GROUPS_COUNT = 3;
-export const SUPER_SIDEBAR_PEEK_DELAY = 150;
-
export const TRACKING_UNKNOWN_ID = 'item_without_id';
export const CLICK_MENU_ITEM_ACTION = 'click_menu_item';
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index 7fa99958b9d..6cf15ba50ec 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -63,6 +63,16 @@ export default {
required: false,
default: () => ({}),
},
+ assignees: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canSetWorkItemMetadata: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -153,6 +163,12 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
:class="{ 'gl-mb-4': hasReplies }"
+ :assignees="assignees"
+ :can-set-work-item-metadata="canSetWorkItemMetadata"
+ :work-item-id="workItemId"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', note)"
@error="$emit('error', $event)"
@@ -179,6 +195,12 @@ export default {
:class="{ 'gl-mb-4': hasReplies }"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ :assignees="assignees"
+ :work-item-id="workItemId"
+ :can-set-work-item-metadata="canSetWorkItemMetadata"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', note)"
@error="$emit('error', $event)"
@@ -200,6 +222,12 @@ export default {
:is-modal="isModal"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ :assignees="assignees"
+ :work-item-id="workItemId"
+ :can-set-work-item-metadata="canSetWorkItemMetadata"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', reply)"
@error="$emit('error', $event)"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 50eb95e20f1..8b25d305398 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -1,27 +1,26 @@
<script>
-import { GlAvatarLink, GlAvatar, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
+import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_ASSIGNEES } from '~/work_items/constants';
+import Tracking from '~/tracking';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import { renderMarkdown } from '~/notes/utils';
import { getLocationHash } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { getWorkItemQuery } from '~/work_items/utils';
import EditedAt from '~/issues/show/components/edited.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql';
import WorkItemCommentForm from './work_item_comment_form.vue';
export default {
name: 'WorkItemNoteThread',
- i18n: {
- moreActionsText: __('More actions'),
- deleteNoteText: __('Delete comment'),
- copyLinkText: __('Copy link'),
- },
components: {
TimelineEntryItem,
NoteBody,
@@ -29,15 +28,28 @@ export default {
NoteActions,
GlAvatar,
GlAvatarLink,
- GlDropdown,
- GlDropdownItem,
WorkItemCommentForm,
EditedAt,
},
- directives: {
- GlTooltip: GlTooltipDirective,
- },
+ mixins: [Tracking.mixin()],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
note: {
type: Object,
required: true,
@@ -70,6 +82,16 @@ export default {
required: false,
default: () => ({}),
},
+ assignees: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canSetWorkItemMetadata: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -78,6 +100,13 @@ export default {
};
},
computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_note_actions',
+ property: `type_${this.workItemType}`,
+ };
+ },
author() {
return this.note.author;
},
@@ -121,6 +150,28 @@ export default {
hasAwardEmojiPermission() {
return this.note.userPermissions.awardEmoji;
},
+ isAuthorAnAssignee() {
+ return Boolean(this.assignees.filter((assignee) => assignee.id === this.author.id).length);
+ },
+ },
+ apollo: {
+ workItem: {
+ query() {
+ return getWorkItemQuery(this.fetchByIid);
+ },
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ },
+ skip() {
+ return !this.queryVariables.id && !this.queryVariables.iid;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
},
methods: {
showReplyForm() {
@@ -171,12 +222,68 @@ export default {
this.isSubmitting = false;
}
},
+ getNewAssigneesAndWidget() {
+ let newAssignees = [];
+ if (this.isAuthorAnAssignee) {
+ newAssignees = this.assignees.filter(({ id }) => id !== this.author.id);
+ } else {
+ newAssignees = [...this.assignees, this.author];
+ }
+
+ const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
+
+ const assigneesWidgetIndex = this.workItem.widgets.findIndex(isAssigneesWidget);
+
+ const editedWorkItemWidgets = [...this.workItem.widgets];
+
+ editedWorkItemWidgets[assigneesWidgetIndex] = {
+ ...editedWorkItemWidgets[assigneesWidgetIndex],
+ assignees: {
+ nodes: newAssignees,
+ },
+ };
+
+ return {
+ newAssignees,
+ editedWorkItemWidgets,
+ };
+ },
notifyCopyDone() {
if (this.isModal) {
navigator.clipboard.writeText(this.noteUrl);
}
toast(__('Link copied to clipboard.'));
},
+ async assignUserAction() {
+ const { newAssignees, editedWorkItemWidgets } = this.getNewAssigneesAndWidget();
+
+ try {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ assigneesWidget: {
+ assigneeIds: newAssignees.map(({ id }) => id),
+ },
+ },
+ },
+ optimisticResponse: {
+ workItemUpdate: {
+ errors: [],
+ workItem: {
+ ...this.workItem,
+ widgets: editedWorkItemWidgets,
+ },
+ },
+ },
+ });
+ this.track(`${this.isAuthorAnAssignee ? 'unassigned_user' : 'assigned_user'}`);
+ } catch (error) {
+ this.$emit('error', i18n.updateError);
+ Sentry.captureException(error);
+ }
+ },
},
};
</script>
@@ -200,7 +307,6 @@ export default {
:aria-label="__('Edit comment')"
:autosave-key="autosaveKey"
:initial-value="note.body"
- :is-submitting="isSubmitting"
:comment-button-text="__('Save comment')"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
@@ -224,32 +330,15 @@ export default {
:show-reply="showReply"
:show-edit="hasAdminPermission"
:note-id="note.id"
+ :is-author-an-assignee="isAuthorAnAssignee"
+ :show-assign-unassign="canSetWorkItemMetadata"
@startReplying="showReplyForm"
@startEditing="startEditing"
@error="($event) => $emit('error', $event)"
+ @notifyCopyDone="notifyCopyDone"
+ @deleteNote="$emit('deleteNote')"
+ @assignUser="assignUserAction"
/>
- <gl-dropdown
- v-gl-tooltip
- icon="ellipsis_v"
- text-sr-only
- right
- :text="$options.i18n.moreActionsText"
- :title="$options.i18n.moreActionsText"
- category="tertiary"
- no-caret
- >
- <gl-dropdown-item :data-clipboard-text="noteUrl" @click="notifyCopyDone">
- <span>{{ $options.i18n.copyLinkText }}</span>
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="hasAdminPermission"
- variant="danger"
- data-testid="delete-note-action"
- @click="$emit('deleteNote')"
- >
- {{ $options.i18n.deleteNoteText }}
- </gl-dropdown-item>
- </gl-dropdown>
</div>
</div>
<div class="timeline-discussion-body">
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index 6bea7953698..624a532c2aa 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlTooltipDirective, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { __, s__ } from '~/locale';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
@@ -10,11 +10,18 @@ export default {
name: 'WorkItemNoteActions',
i18n: {
editButtonText: __('Edit comment'),
+ moreActionsText: __('More actions'),
+ deleteNoteText: __('Delete comment'),
+ copyLinkText: __('Copy link'),
+ assignUserText: __('Assign to commenting user'),
+ unassignUserText: __('Unassign from commenting user'),
},
components: {
GlButton,
GlIcon,
ReplyButton,
+ GlDropdown,
+ GlDropdownItem,
EmojiPicker: () => import('~/emoji/components/picker.vue'),
},
directives: {
@@ -39,6 +46,28 @@ export default {
required: false,
default: false,
},
+ noteUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isAuthorAnAssignee: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showAssignUnassign: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ assignUserActionText() {
+ return this.isAuthorAnAssignee
+ ? this.$options.i18n.unassignUserText
+ : this.$options.i18n.assignUserText;
+ },
},
methods: {
async setAwardEmoji(name) {
@@ -100,5 +129,39 @@ export default {
:aria-label="$options.i18n.editButtonText"
@click="$emit('startEditing')"
/>
+ <gl-dropdown
+ v-gl-tooltip
+ data-testid="work-item-note-actions"
+ icon="ellipsis_v"
+ text-sr-only
+ right
+ :text="$options.i18n.moreActionsText"
+ :title="$options.i18n.moreActionsText"
+ category="tertiary"
+ no-caret
+ >
+ <gl-dropdown-item
+ data-testid="copy-link-action"
+ :data-clipboard-text="noteUrl"
+ @click="$emit('notifyCopyDone')"
+ >
+ <span>{{ $options.i18n.copyLinkText }}</span>
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="showAssignUnassign"
+ data-testid="assign-note-action"
+ @click="$emit('assignUser')"
+ >
+ {{ assignUserActionText }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="showEdit"
+ variant="danger"
+ data-testid="delete-note-action"
+ @click="$emit('deleteNote')"
+ >
+ {{ $options.i18n.deleteNoteText }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index f568e23a30a..aed2187a3e6 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -224,15 +224,18 @@ export default {
canDelete() {
return this.workItem?.userPermissions?.deleteWorkItem;
},
+ canSetWorkItemMetadata() {
+ return this.workItem?.userPermissions?.setWorkItemMetadata;
+ },
+ canAssignUnassignUser() {
+ return this.workItemAssignees && this.canSetWorkItemMetadata;
+ },
confidentialTooltip() {
return sprintfWorkItem(this.$options.i18n.confidentialTooltip, this.workItemType);
},
fullPath() {
return this.workItem?.project.fullPath;
},
- workItemsMvcEnabled() {
- return this.glFeatures.workItemsMvc;
- },
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
@@ -711,8 +714,11 @@ export default {
:fetch-by-iid="fetchByIid"
:work-item-type="workItemType"
:is-modal="isModal"
+ :assignees="workItemAssignees && workItemAssignees.assignees.nodes"
+ :can-set-work-item-metadata="canAssignUnassignUser"
class="gl-pt-5"
@error="updateError = $event"
+ @has-notes="updateHasNotes"
/>
<gl-empty-state
v-if="error"
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 2a45cc968d8..a1f1eda8bc5 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -78,6 +78,16 @@ export default {
required: false,
default: false,
},
+ assignees: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canSetWorkItemMetadata: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -375,6 +385,8 @@ export default {
:is-modal="isModal"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ :assignees="assignees"
+ :can-set-work-item-metadata="canSetWorkItemMetadata"
@deleteNote="showDeleteNoteModal($event, discussion)"
@error="$emit('error', $event)"
/>
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index ada9f737e6e..3651cad48f6 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -27,6 +27,7 @@ fragment WorkItem on WorkItem {
userPermissions {
deleteWorkItem
updateWorkItem
+ setWorkItemMetadata
}
widgets {
...WorkItemWidgets
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index b20ec1dc50a..c452c401cfb 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -268,14 +268,8 @@
@mixin build-log-top-bar($height) {
@include build-log-bar($height);
-
- position: -webkit-sticky;
position: sticky;
- top: $header-height;
-
- .with-performance-bar & {
- top: calc(#{$header-height} + #{$performance-bar-height});
- }
+ top: $calc-application-header-height;
}
/*
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 52561e5deb1..48c87682897 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -49,9 +49,7 @@
}
&:not(.super-sidebar-loading) {
- @media (prefers-reduced-motion: no-preference) {
- transition: transform $gl-transition-duration-medium ease-out;
- }
+ transition: transform $gl-transition-duration-medium;
}
.user-bar {
@@ -156,26 +154,9 @@
display: none;
}
-.super-sidebar-peek {
- @include gl-shadow;
- border-right: 0;
- transform: translate3d(0, 0, 0) !important;
-
- @media (prefers-reduced-motion: no-preference) {
- transition: transform 100ms ease-out !important;
- }
-}
-
-.super-sidebar-hover-area {
- z-index: $super-sidebar-z-index;
-}
-
.page-with-super-sidebar {
padding-left: 0;
-
- @media (prefers-reduced-motion: no-preference) {
- transition: padding-left $gl-transition-duration-medium ease-out;
- }
+ transition: padding-left $gl-transition-duration-medium;
&:not(.page-with-super-sidebar-collapsed) {
.super-sidebar-overlay {
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index d40c03b7fd1..5114f484e53 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -6,30 +6,22 @@
}
.archived-job {
- top: $header-height;
+ top: $calc-application-header-height;
border-radius: 2px 2px 0 0;
color: var(--orange-600, $orange-600);
background-color: var(--orange-50, $orange-50);
border: 1px solid var(--border-color, $border-color);
-
- .with-performance-bar & {
- top: calc(#{$header-height} + #{$performance-bar-height});
- }
}
.top-bar {
@include build-log-top-bar(50px);
&.has-archived-block {
- top: calc(#{$header-height} + 28px);
-
- .with-performance-bar & {
- top: calc(#{$header-height} + #{$performance-bar-height} + 28px);
- }
+ top: calc(#{$calc-application-header-height} + 28px);
}
&.affix {
- top: $header-height;
+ top: $calc-application-header-height;
// with sidebar
&.sidebar-expanded {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index aeaa46c2b86..65cae126795 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -1472,13 +1472,9 @@ kbd {
transform: translate3d(0, 0, 0);
}
}
-@media (prefers-reduced-motion: no-preference) {
-}
.page-with-super-sidebar {
padding-left: 0;
}
-@media (prefers-reduced-motion: no-preference) {
-}
@media (min-width: 1200px) {
.page-with-super-sidebar {
padding-left: 256px;
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index e6589cb73a0..c5d9721d93c 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -1472,13 +1472,9 @@ kbd {
transform: translate3d(0, 0, 0);
}
}
-@media (prefers-reduced-motion: no-preference) {
-}
.page-with-super-sidebar {
padding-left: 0;
}
-@media (prefers-reduced-motion: no-preference) {
-}
@media (min-width: 1200px) {
.page-with-super-sidebar {
padding-left: 256px;
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 7b6e3a5c521..d366649916d 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -430,8 +430,12 @@ input.btn-block[type="button"] {
cursor: not-allowed;
color: #89888d;
}
+.gl-form-checkbox.custom-control {
+ padding-left: 1rem;
+}
.gl-form-checkbox.custom-control .custom-control-input ~ .custom-control-label {
cursor: pointer;
+ padding-left: 0.5rem;
}
.gl-form-checkbox.custom-control
.custom-control-input
@@ -440,6 +444,7 @@ input.btn-block[type="button"] {
.custom-control-input
~ .custom-control-label::after {
top: 0;
+ left: -1rem;
}
.gl-form-checkbox.custom-control
.custom-control-input
diff --git a/app/graphql/types/permission_types/work_item.rb b/app/graphql/types/permission_types/work_item.rb
index f35f42001e0..25d6b3e924d 100644
--- a/app/graphql/types/permission_types/work_item.rb
+++ b/app/graphql/types/permission_types/work_item.rb
@@ -6,7 +6,7 @@ module Types
graphql_name 'WorkItemPermissions'
description 'Check permissions for the current user on a work item'
- abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item
+ abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item, :set_work_item_metadata
end
end
end
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 37c82c125aa..4c421f066f9 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Ci
+ # This class is a collection of common features between Ci::Build and Ci::Bridge.
+ # In https://gitlab.com/groups/gitlab-org/-/epics/9991, we aim to clarify class naming conventions.
class Processable < ::CommitStatus
include Gitlab::Utils::StrongMemoize
include FromUnion
diff --git a/config/feature_flags/development/rollup_timebox_chart.yml b/config/feature_flags/development/rollup_timebox_chart.yml
new file mode 100644
index 00000000000..00952adfe8c
--- /dev/null
+++ b/config/feature_flags/development/rollup_timebox_chart.yml
@@ -0,0 +1,8 @@
+---
+name: rollup_timebox_chart
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115674
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/399186
+milestone: '15.11'
+type: development
+group: group::project management
+default_enabled: false
diff --git a/data/removals/15_9/15-9-omniauth-authentiq.yml b/data/removals/15_9/15-9-omniauth-authentiq.yml
new file mode 100644
index 00000000000..2a2e2601704
--- /dev/null
+++ b/data/removals/15_9/15-9-omniauth-authentiq.yml
@@ -0,0 +1,11 @@
+- title: "`omniauth-authentiq` gem no longer available" # (required) Clearly explain the change. For example, "The `confidential` field for a `Note` is removed" or "CI/CD job names are limited to 250 characters."
+ announcement_milestone: "15.9" # (required) The milestone when this feature was deprecated.
+ announcement_date: "2023-02-22" # (required) The date of the milestone release when this feature was deprecated. This should almost always be the 22nd of a month (YYYY-MM-DD), unless you did an out of band blog post.
+ removal_milestone: "15.9" # (required) The milestone when this feature is being removed.
+ removal_date: "2023-02-22" # (required) This should almost always be the 22nd of a month (YYYY-MM-DD), the date of the milestone release when this feature will be removed.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: adil.farrukh # (required) GitLab username of the person reporting the removal
+ stage: manage # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389452 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ `omniauth-authentiq` is an OmniAuth strategy gem that was part of GitLab. The company providing authentication services, Authentiq, has shut down. Therefore the gem is being removed.
diff --git a/data/removals/15_9/15-9-omniauth-shibboleth.yml b/data/removals/15_9/15-9-omniauth-shibboleth.yml
new file mode 100644
index 00000000000..a7641bb893b
--- /dev/null
+++ b/data/removals/15_9/15-9-omniauth-shibboleth.yml
@@ -0,0 +1,11 @@
+- title: "`omniauth-shibboleth` gem no longer available" # (required) Clearly explain the change. For example, "The `confidential` field for a `Note` is removed" or "CI/CD job names are limited to 250 characters."
+ announcement_milestone: "10.0" # (required) The milestone when this feature was deprecated.
+ announcement_date: "2017-09-22" # (required) The date of the milestone release when this feature was deprecated. This should almost always be the 22nd of a month (YYYY-MM-DD), unless you did an out of band blog post.
+ removal_milestone: "15.9" # (required) The milestone when this feature is being removed.
+ removal_date: "2023-02-22" # (required) This should almost always be the 22nd of a month (YYYY-MM-DD), the date of the milestone release when this feature will be removed.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: adil.farrukh # (required) GitLab username of the person reporting the removal
+ stage: manage # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388959 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ `omniauth-shibboleth` is an OmniAuth strategy gem that was part of GitLab. The gem has not received security updates and does not meet GitLab quality guidance criteria. This gem was originally scheduled for removal by 14.1, but it was not removed at that time. The gem is being removed now.
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 3faeea1b5c4..a453d703a18 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -36,6 +36,7 @@ You can use the following environment variables to override certain values:
| `GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for runners. |
| `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging`, or `test`. |
| `GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS` | integer | The default TTL used for entries stored in the Rails-cache. Default is `28800`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95042) in 15.3. |
+| `GITLAB_CI_CONFIG_FETCH_TIMEOUT_SECONDS` | integer | Timeout for resolving remote includes in CI config in seconds. Must be between `0` and `60`. Default is `30`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116383) in 15.11. |
## Adding more variables
diff --git a/doc/api/dora/metrics.md b/doc/api/dora/metrics.md
index a3865213714..a1515413bed 100644
--- a/doc/api/dora/metrics.md
+++ b/doc/api/dora/metrics.md
@@ -11,6 +11,8 @@ type: reference, api
> - The legacy key/value pair `{ "<date>" => "<value>" }` was removed from the payload in GitLab 14.0.
> `time_to_restore_service` metric was introduced in GitLab 14.9.
+You can also retrieve [DORA metrics](../../user/analytics/dora_metrics.md) with the [GraphQL API](../../api/graphql/reference/index.md).
+
All methods require at least the Reporter role.
## Get project-level DORA metrics
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9a4df4cab04..511d6b81066 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -22319,6 +22319,7 @@ Check permissions for the current user on a work item.
| <a id="workitempermissionsadminworkitem"></a>`adminWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_work_item` on this resource. |
| <a id="workitempermissionsdeleteworkitem"></a>`deleteWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `delete_work_item` on this resource. |
| <a id="workitempermissionsreadworkitem"></a>`readWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `read_work_item` on this resource. |
+| <a id="workitempermissionssetworkitemmetadata"></a>`setWorkItemMetadata` | [`Boolean!`](#boolean) | Indicates the user can perform `set_work_item_metadata` on this resource. |
| <a id="workitempermissionsupdateworkitem"></a>`updateWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `update_work_item` on this resource. |
### `WorkItemType`
diff --git a/doc/update/removals.md b/doc/update/removals.md
index 2dd92093d87..6874ab9fe2e 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -55,6 +55,22 @@ Review the details carefully before upgrading.
The Live Preview feature of the Web IDE was intended to provide a client-side preview of static web applications. However, complex configuration steps and a narrow set of supported project types have limited its utility. With the introduction of the Web IDE Beta in GitLab 15.7, you can now connect to a full server-side runtime environment. With upcoming support for installing extensions in the Web IDE, we’ll also support more advanced workflows than those available with Live Preview. As of GitLab 15.9, Live Preview is no longer available in the Web IDE.
+### `omniauth-authentiq` gem no longer available
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+`omniauth-authentiq` is an OmniAuth strategy gem that was part of GitLab. The company providing authentication services, Authentiq, has shut down. Therefore the gem is being removed.
+
+### `omniauth-shibboleth` gem no longer available
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+`omniauth-shibboleth` is an OmniAuth strategy gem that was part of GitLab. The gem has not received security updates and does not meet GitLab quality guidance criteria. This gem was originally scheduled for removal by 14.1, but it was not removed at that time. The gem is being removed now.
+
## Removed in 15.8
### CiliumNetworkPolicy within the auto deploy Helm chart is removed
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 37f1f4cc65e..a7f53dae8b6 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -480,8 +480,9 @@ An [issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/297389) to add a
When the **Enforce SSO-only authentication for web activity for this group** option is enabled:
-- All users must access GitLab by using their GitLab group's single sign-on URL
- to access group resources, regardless of whether they have an existing SAML
+- In alignment with the table above, all members must access GitLab by using
+ their GitLab group’s single sign-on URL to
+ access group resources, regardless of whether they have an existing SAML
identity.
- SSO is enforced when users access groups and projects in the organization's
group hierarchy. Users can view other groups and projects without SSO sign in.
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index fde104200a7..6676a28dfdb 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -120,9 +120,9 @@ To view a list of product analytics dashboards for a project:
To define a dashboard:
1. In `.gitlab/product_analytics/dashboards/`, create a directory named like the dashboard. Each dashboard should have its own directory.
-1. In the new directory, create a `.yaml` file with the same name as the directory. This file contains the dashboard definition, and must conform to the JSON schema defined in `ee/app/validators/json_schemas/product_analytics_dashboard.json`.
+1. In the new directory, create a `.yaml` file with the same name as the directory. This file contains the dashboard definition, and must conform to the JSON schema defined in `ee/app/validators/json_schemas/analytics_dashboard.json`.
1. In the `.gitlab/product_analytics/dashboards/visualizations/` directory, create a `yaml` file. This file defines the visualization type for the dashboard, and must conform to the schema in
- `ee/app/validators/json_schemas/product_analytics_visualization.json`.
+ `ee/app/validators/json_schemas/analytics_visualization.json`.
The example below includes three dashboards and one visualization that applies to all dashboards.
diff --git a/lib/feature.rb b/lib/feature.rb
index 17c26796ea1..eb2997a3551 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -205,9 +205,9 @@ module Feature
# This method is called from config/initializers/flipper.rb and can be used
# to register Flipper groups.
# See https://docs.gitlab.com/ee/development/feature_flags/index.html
- def register_feature_groups
- Flipper.register(:gitlab_team_members) { |actor| FeatureGroups::GitlabTeamMembers.enabled?(actor.thing) }
- end
+ #
+ # EE feature groups should go inside the ee/lib/ee/feature.rb version of this method.
+ def register_feature_groups; end
def register_definitions
Feature::Definition.reload!
diff --git a/lib/feature_groups/gitlab_team_members.rb b/lib/feature_groups/gitlab_team_members.rb
deleted file mode 100644
index 7f4c597fddd..00000000000
--- a/lib/feature_groups/gitlab_team_members.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module FeatureGroups
- class GitlabTeamMembers
- GITLAB_COM_GROUP_ID = 6543
-
- class << self
- def enabled?(thing)
- return false unless Gitlab.com?
-
- team_member?(thing)
- end
-
- private
-
- def team_member?(thing)
- thing.is_a?(::User) && gitlab_com_member_ids.include?(thing.id)
- end
-
- def gitlab_com
- @gitlab_com ||= ::Group.find(GITLAB_COM_GROUP_ID)
- end
-
- def gitlab_com_member_ids
- Rails.cache.fetch("gitlab_team_members", expires_in: 1.hour) do
- gitlab_com.members.pluck_user_ids.to_set
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 534b84afc23..0c293c3f0ef 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -9,7 +9,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
ConfigError = Class.new(StandardError)
- TIMEOUT_SECONDS = 30.seconds
+ TIMEOUT_SECONDS = ENV.fetch('GITLAB_CI_CONFIG_FETCH_TIMEOUT_SECONDS', 30).to_i.clamp(0, 60).seconds
TIMEOUT_MESSAGE = 'Request timed out when fetching configuration files.'
RESCUE_ERRORS = [
diff --git a/package.json b/package.json
index 42c95e7afa4..f57fcfe6ab0 100644
--- a/package.json
+++ b/package.json
@@ -55,8 +55,8 @@
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
- "@gitlab/svgs": "3.31.0",
- "@gitlab/ui": "58.6.0",
+ "@gitlab/svgs": "3.36.0",
+ "@gitlab/ui": "58.8.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230323132525",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb
index 679f273d0f4..afa9be034eb 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb
@@ -30,14 +30,8 @@ module QA
Page::Project::Show.perform do |show|
show.switch_to_branch(branch_name)
- # It takes a few seconds for console errors to appear
- sleep 3
-
- errors = page.driver.browser.logs.get(:browser)
- .select { |e| e.level == "SEVERE" }
- .to_a
-
- raise("Console error(s):\n#{errors.join("\n\n")}") if errors.present?
+ # To prevent false positives: https://gitlab.com/gitlab-org/gitlab/-/issues/383863
+ expect(show).to have_no_content('An error occurred')
show.click_file('test-folder')
diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb
index e88d514e967..f1604a61890 100644
--- a/spec/features/projects/work_items/work_item_spec.rb
+++ b/spec/features/projects/work_items/work_item_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
let_it_be(:work_item) { create(:work_item, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:milestones) { create_list(:milestone, 25, project: project) }
+ let_it_be(:note) { create(:note, noteable: work_item, project: work_item.project) }
let(:work_items_path) { project_work_items_path(project, work_items_path: work_item.iid, iid_path: true) }
context 'for signed in user' do
@@ -45,4 +46,16 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
it_behaves_like 'work items invite members'
end
+
+ context 'for guest users' do
+ before do
+ project.add_guest(user)
+
+ sign_in(user)
+
+ visit work_items_path
+ end
+
+ it_behaves_like 'work items comment actions for guest users'
+ end
end
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index b16371dae78..6eae650a2a1 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,8 +1,6 @@
-import { nextTick } from 'vue';
import { GlCollapse } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
-import { SUPER_SIDEBAR_PEEK_DELAY } from '~/super_sidebar/constants';
import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
@@ -19,19 +17,13 @@ const focusInputMock = jest.fn();
describe('SuperSidebar component', () => {
let wrapper;
- const findSidebar = () => wrapper.findByTestId('super-sidebar');
- const findHoverArea = () => wrapper.findByTestId('super-sidebar-hover-area');
+ const findSidebar = () => wrapper.find('.super-sidebar');
const findUserBar = () => wrapper.findComponent(UserBar);
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
- const createWrapper = ({ props = {}, isPeek = false } = {}) => {
+ const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
- data() {
- return {
- isPeek,
- };
- },
propsData: {
sidebarData,
...props,
@@ -59,36 +51,6 @@ describe('SuperSidebar component', () => {
expect(findSidebar().attributes('inert')).toBe(undefined);
});
- it('updates inert attribute and `gl-visibility-hidden` class when peeking on hover', async () => {
- const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
- isCollapsed.mockReturnValue(true);
- createWrapper();
-
- findHoverArea().trigger('mouseover');
- expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
- expect(setTimeoutSpy).toHaveBeenLastCalledWith(
- expect.any(Function),
- SUPER_SIDEBAR_PEEK_DELAY,
- );
- jest.runAllTimers();
- await nextTick();
-
- expect(findSidebar().classes()).not.toContain('gl-visibility-hidden');
- expect(findSidebar().attributes('inert')).toBe(undefined);
-
- findSidebar().trigger('mouseleave');
- expect(setTimeoutSpy).toHaveBeenCalledTimes(2);
- expect(setTimeoutSpy).toHaveBeenLastCalledWith(
- expect.any(Function),
- SUPER_SIDEBAR_PEEK_DELAY,
- );
- jest.runAllTimers();
- await nextTick();
-
- expect(findSidebar().classes()).toContain('gl-visibility-hidden');
- expect(findSidebar().attributes('inert')).toBe('inert');
- });
-
it('renders UserBar with sidebarData', () => {
createWrapper();
expect(findUserBar().props('sidebarData')).toBe(sidebarData);
@@ -105,7 +67,6 @@ describe('SuperSidebar component', () => {
});
it("does not call the context switcher's focusInput method initially", () => {
- createWrapper();
expect(focusInputMock).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index a8bd8e341d6..48e62c3564d 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -19,7 +19,6 @@ describe('UserBar component', () => {
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
- const findCollapseButton = () => wrapper.findByTestId('super-sidebar-collapse-button');
const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo');
const findSearchButton = () => wrapper.findByTestId('super-sidebar-search-button');
const findSearchModal = () => wrapper.findComponent(SearchModal);
@@ -31,10 +30,9 @@ describe('UserBar component', () => {
searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
},
});
- const createWrapper = ({ hasCollapseButton = true, extraSidebarData = {} } = {}) => {
+ const createWrapper = (extraSidebarData = {}) => {
wrapper = shallowMountExtended(UserBar, {
propsData: {
- hasCollapseButton,
sidebarData: { ...sidebarData, ...extraSidebarData },
},
provide: {
@@ -82,21 +80,12 @@ describe('UserBar component', () => {
expect(findBrandLogo().exists()).toBe(true);
expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url);
});
-
- it('renders collapse button when hasCollapseButton is true', () => {
- expect(findCollapseButton().exists()).toBe(true);
- });
-
- it('does not render collapse button when hasCollapseButton is false', () => {
- createWrapper({ hasCollapseButton: false });
- expect(findCollapseButton().exists()).toBe(false);
- });
});
describe('GitLab Next badge', () => {
describe('when on canary', () => {
it('should render a badge to switch off GitLab Next', () => {
- createWrapper({ extraSidebarData: { gitlab_com_and_canary: true } });
+ createWrapper({ gitlab_com_and_canary: true });
const badge = wrapper.findComponent(GlBadge);
expect(badge.text()).toBe('Next');
expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url);
@@ -105,7 +94,7 @@ describe('UserBar component', () => {
describe('when not on canary', () => {
it('should not render the GitLab Next badge', () => {
- createWrapper({ extraSidebarData: { gitlab_com_and_canary: false } });
+ createWrapper({ gitlab_com_and_canary: false });
const badge = wrapper.findComponent(GlBadge);
expect(badge.exists()).toBe(false);
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index b293127b6af..b406c9d843a 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,3 +1,4 @@
+import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -17,6 +18,10 @@ describe('Work Item Note Actions', () => {
const findReplyButton = () => wrapper.findComponent(ReplyButton);
const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
+ const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]');
+ const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]');
const addEmojiMutationResolver = jest.fn().mockResolvedValue({
data: {
@@ -29,13 +34,19 @@ describe('Work Item Note Actions', () => {
template: '<div></div>',
};
- const createComponent = ({ showReply = true, showEdit = true, showAwardEmoji = true } = {}) => {
+ const createComponent = ({
+ showReply = true,
+ showEdit = true,
+ showAwardEmoji = true,
+ showAssignUnassign = false,
+ } = {}) => {
wrapper = shallowMount(WorkItemNoteActions, {
propsData: {
showReply,
showEdit,
noteId,
showAwardEmoji,
+ showAssignUnassign,
},
provide: {
glFeatures: {
@@ -113,4 +124,75 @@ describe('Work Item Note Actions', () => {
});
});
});
+
+ describe('delete note', () => {
+ it('should display the `Delete comment` dropdown item if user has a permission to delete a note', () => {
+ createComponent({
+ showEdit: true,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(true);
+ });
+
+ it('should not display the `Delete comment` dropdown item if user has no permission to delete a note', () => {
+ createComponent({
+ showEdit: false,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(false);
+ });
+
+ it('should emit `deleteNote` event when delete note action is clicked', () => {
+ createComponent({
+ showEdit: true,
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ });
+ });
+
+ describe('copy link', () => {
+ beforeEach(() => {
+ createComponent({});
+ });
+ it('should display Copy link always', () => {
+ expect(findCopyLinkButton().exists()).toBe(true);
+ });
+
+ it('should emit `notifyCopyDone` event when copy link note action is clicked', () => {
+ findCopyLinkButton().vm.$emit('click');
+
+ expect(wrapper.emitted('notifyCopyDone')).toEqual([[]]);
+ });
+ });
+
+ describe('assign/unassign to commenting user', () => {
+ it('should not display assign/unassign by default', () => {
+ createComponent();
+
+ expect(findAssignUnassignButton().exists()).toBe(false);
+ });
+
+ it('should display assign/unassign when the props is true', () => {
+ createComponent({
+ showAssignUnassign: true,
+ });
+
+ expect(findAssignUnassignButton().exists()).toBe(true);
+ });
+
+ it('should emit `assignUser` event when assign note action is clicked', () => {
+ createComponent({
+ showAssignUnassign: true,
+ });
+
+ findAssignUnassignButton().vm.$emit('click');
+
+ expect(wrapper.emitted('assignUser')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index f7e7b21d322..17bbdf78458 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -1,4 +1,4 @@
-import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
+import { GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -12,8 +12,17 @@ import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
-import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import {
+ mockAssignees,
+ mockWorkItemCommentNote,
+ updateWorkItemMutationResponse,
+ workItemQueryResponse,
+} from 'jest/work_items/mock_data';
+import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { mockTracking } from 'helpers/tracking_helper';
Vue.use(VueApollo);
jest.mock('~/lib/utils/autosave');
@@ -22,6 +31,7 @@ describe('Work Item Note', () => {
let wrapper;
const updatedNoteText = '# Some title';
const updatedNoteBody = '<h1 data-sourcepos="1:1-1:12" dir="auto">Some title</h1>';
+ const mockWorkItemId = workItemQueryResponse.data.workItem.id;
const successHandler = jest.fn().mockResolvedValue({
data: {
@@ -35,6 +45,13 @@ describe('Work Item Note', () => {
},
},
});
+
+ const workItemResponseHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+
+ const updateWorkItemMutationSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+
const errorHandler = jest.fn().mockRejectedValue('Oops');
const findAuthorAvatarLink = () => wrapper.findComponent(GlAvatarLink);
@@ -42,27 +59,38 @@ describe('Work Item Note', () => {
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
const findNoteActions = () => wrapper.findComponent(NoteActions);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findEditedAt = () => wrapper.findComponent(EditedAt);
-
- const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
const findNoteWrapper = () => wrapper.find('[data-testid="note-wrapper"]');
const createComponent = ({
note = mockWorkItemCommentNote,
isFirstNote = false,
updateNoteMutationHandler = successHandler,
+ workItemId = mockWorkItemId,
+ updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
+ assignees = mockAssignees,
+ queryVariables = { id: mockWorkItemId },
+ fetchByIid = false,
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
propsData: {
note,
isFirstNote,
workItemType: 'Task',
+ workItemId,
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
autocompleteDataSources: {},
+ assignees,
+ queryVariables,
+ fetchByIid,
+ fullPath: 'test-project-path',
},
- apolloProvider: mockApollo([[updateWorkItemNoteMutation, updateNoteMutationHandler]]),
+ apolloProvider: mockApollo([
+ [workItemQuery, workItemResponseHandler],
+ [updateWorkItemNoteMutation, updateNoteMutationHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
+ ]),
});
};
@@ -226,36 +254,57 @@ describe('Work Item Note', () => {
});
});
- it('should display the `Delete comment` dropdown item if user has a permission to delete a note', () => {
- createComponent({
- note: {
- ...mockWorkItemCommentNote,
- userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
- },
+ describe('assign/unassign to commenting user', () => {
+ it('calls a mutation with correct variables', async () => {
+ createComponent({ assignees: mockAssignees });
+ await waitForPromises();
+ findNoteActions().vm.$emit('assignUser');
+
+ await waitForPromises();
+
+ expect(updateWorkItemMutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockWorkItemId,
+ assigneesWidget: {
+ assigneeIds: [mockAssignees[1].id],
+ },
+ },
+ });
});
- expect(findDropdown().exists()).toBe(true);
- expect(findDeleteNoteButton().exists()).toBe(true);
- });
+ it('emits an error and resets assignees if mutation was rejected', async () => {
+ createComponent({
+ updateWorkItemMutationHandler: errorHandler,
+ assignees: [mockAssignees[0]],
+ });
- it('should not display the `Delete comment` dropdown item if user has no permission to delete a note', () => {
- createComponent();
+ await waitForPromises();
- expect(findDropdown().exists()).toBe(true);
- expect(findDeleteNoteButton().exists()).toBe(false);
- });
+ expect(findNoteActions().props('isAuthorAnAssignee')).toEqual(true);
- it('should emit `deleteNote` event when delete note action is clicked', () => {
- createComponent({
- note: {
- ...mockWorkItemCommentNote,
- userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
- },
+ findNoteActions().vm.$emit('assignUser');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(findNoteActions().props('isAuthorAnAssignee')).toEqual(true);
});
- findDeleteNoteButton().vm.$emit('click');
+ it('tracks the event', async () => {
+ createComponent();
+ await waitForPromises();
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findNoteActions().vm.$emit('assignUser');
- expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'unassigned_user', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_note_actions',
+ property: 'type_Task',
+ });
+ });
});
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index d106d54bafa..50d7aff3365 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -82,6 +82,7 @@ export const workItemQueryResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -183,6 +184,7 @@ export const updateWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -332,6 +334,7 @@ export const workItemResponseFactory = ({
userPermissions: {
deleteWorkItem: canDelete,
updateWorkItem: canUpdate,
+ setWorkItemMetadata: canUpdate,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -542,6 +545,7 @@ export const createWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [],
@@ -591,6 +595,7 @@ export const createWorkItemFromTaskMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -632,6 +637,7 @@ export const createWorkItemFromTaskMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [],
@@ -857,6 +863,7 @@ export const workItemHierarchyEmptyResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -898,6 +905,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
project: {
@@ -1039,6 +1047,7 @@ export const workItemHierarchyResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1128,6 +1137,7 @@ export const workItemObjectiveWithChild = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1195,6 +1205,7 @@ export const workItemHierarchyTreeResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -1272,6 +1283,7 @@ export const changeIndirectWorkItemParentMutationResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
description: null,
@@ -1333,6 +1345,7 @@ export const changeWorkItemParentMutationResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
description: null,
diff --git a/spec/graphql/types/permission_types/work_item_spec.rb b/spec/graphql/types/permission_types/work_item_spec.rb
index db6d78b1538..c710f7d169e 100644
--- a/spec/graphql/types/permission_types/work_item_spec.rb
+++ b/spec/graphql/types/permission_types/work_item_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Types::PermissionTypes::WorkItem do
it do
expected_permissions = [
- :read_work_item, :update_work_item, :delete_work_item, :admin_work_item
+ :read_work_item, :update_work_item, :delete_work_item, :admin_work_item, :set_work_item_metadata
]
expected_permissions.each do |permission|
diff --git a/spec/lib/feature_groups/gitlab_team_members_spec.rb b/spec/lib/feature_groups/gitlab_team_members_spec.rb
deleted file mode 100644
index f4db02e6c58..00000000000
--- a/spec/lib/feature_groups/gitlab_team_members_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe FeatureGroups::GitlabTeamMembers, feature_category: :shared do
- let_it_be(:gitlab_com) { create(:group) }
- let_it_be_with_reload(:member) { create(:user).tap { |user| gitlab_com.add_developer(user) } }
- let_it_be_with_reload(:non_member) { create(:user) }
-
- before do
- stub_const("#{described_class.name}::GITLAB_COM_GROUP_ID", gitlab_com.id)
- end
-
- describe '#enabled?' do
- context 'when not on gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(false)
- end
-
- it 'returns false' do
- expect(described_class.enabled?(member)).to eq(false)
- end
- end
-
- context 'when on gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- it 'returns true for gitlab-com group members' do
- expect(described_class.enabled?(member)).to eq(true)
- end
-
- it 'returns false for users not in gitlab-com' do
- expect(described_class.enabled?(non_member)).to eq(false)
- end
-
- it 'returns false when actor is not a user' do
- expect(described_class.enabled?(gitlab_com)).to eq(false)
- end
-
- it 'reloads members after 1 hour' do
- expect(described_class.enabled?(non_member)).to eq(false)
-
- gitlab_com.add_developer(non_member)
-
- travel_to(2.hours.from_now) do
- expect(described_class.enabled?(non_member)).to eq(true)
- end
- end
-
- it 'does not make queries on subsequent calls', :use_clean_rails_memory_store_caching do
- described_class.enabled?(member)
- non_member
-
- queries = ActiveRecord::QueryRecorder.new do
- described_class.enabled?(member)
- described_class.enabled?(non_member)
- end
-
- expect(queries.count).to eq(0)
- end
- end
- end
-end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index c86bc36057a..51f21e7f46e 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -154,17 +154,6 @@ RSpec.describe Feature, stub_feature_flags: false, feature_category: :shared do
end
end
- describe '.register_feature_groups' do
- before do
- Flipper.unregister_groups
- described_class.register_feature_groups
- end
-
- it 'registers expected groups' do
- expect(Flipper.groups).to include(an_object_having_attributes(name: :gitlab_team_members))
- end
- end
-
describe '.enabled?' do
before do
allow(Feature).to receive(:log_feature_flag_states?).and_return(false)
@@ -361,22 +350,6 @@ RSpec.describe Feature, stub_feature_flags: false, feature_category: :shared do
end
end
- context 'with gitlab_team_members feature group' do
- let(:actor) { build_stubbed(:user) }
-
- before do
- Flipper.unregister_groups
- described_class.register_feature_groups
- described_class.enable(:enabled_feature_flag, :gitlab_team_members)
- end
-
- it 'delegates check to FeatureGroups::GitlabTeamMembers' do
- expect(FeatureGroups::GitlabTeamMembers).to receive(:enabled?).with(actor)
-
- described_class.enabled?(:enabled_feature_flag, actor)
- end
- end
-
context 'with an individual actor' do
let(:actor) { stub_feature_flag_gate('CustomActor:5') }
let(:another_actor) { stub_feature_flag_gate('CustomActor:10') }
diff --git a/spec/lib/gitlab/jwt_authenticatable_spec.rb b/spec/lib/gitlab/jwt_authenticatable_spec.rb
index 92d5feceb75..9a06f9b91df 100644
--- a/spec/lib/gitlab/jwt_authenticatable_spec.rb
+++ b/spec/lib/gitlab/jwt_authenticatable_spec.rb
@@ -172,11 +172,17 @@ RSpec.describe Gitlab::JwtAuthenticatable do
end
it 'raises an error if iat is invalid' do
- encoded_message = JWT.encode(payload.merge(iat: 'wrong'), test_class.secret, 'HS256')
+ encoded_message = JWT.encode(payload.merge(iat: Time.current.to_i + 1), test_class.secret, 'HS256')
expect { test_class.decode_jwt(encoded_message, iat_after: true) }.to raise_error(JWT::DecodeError)
end
+ it 'raises InvalidPayload exception if iat is a string' do
+ expect do
+ JWT.encode(payload.merge(iat: 'wrong'), test_class.secret, 'HS256')
+ end.to raise_error(JWT::InvalidPayload)
+ end
+
it 'raises an error if iat is absent' do
encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index 294a5ee82ed..509a4bb921b 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -175,7 +175,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
end
it 'raises an error' do
- expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised')
+ expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
@@ -191,7 +191,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
end
it 'raises an error' do
- expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised')
+ expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
end
diff --git a/spec/lib/json_web_token/hmac_token_spec.rb b/spec/lib/json_web_token/hmac_token_spec.rb
index 016084eaf69..7c486b2fe1b 100644
--- a/spec/lib/json_web_token/hmac_token_spec.rb
+++ b/spec/lib/json_web_token/hmac_token_spec.rb
@@ -50,8 +50,8 @@ RSpec.describe JSONWebToken::HMACToken do
context 'that was generated using a different secret' do
let(:encoded_token) { described_class.new('some other secret').encoded }
- it "raises exception saying 'Signature verification raised" do
- expect { decoded_token }.to raise_error(JWT::VerificationError, 'Signature verification raised')
+ it "raises exception saying 'Signature verification failed" do
+ expect { decoded_token }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index fe6f75548a5..8dfa577bc35 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -59,7 +59,8 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
'readWorkItem' => true,
'updateWorkItem' => true,
'deleteWorkItem' => false,
- 'adminWorkItem' => true
+ 'adminWorkItem' => true,
+ 'setWorkItemMetadata' => true
},
'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path)
)
@@ -497,6 +498,25 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
end
+ context 'when the user cannot set work item metadata' do
+ let(:current_user) { guest }
+
+ before do
+ project.add_guest(guest)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns correct user permission' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'userPermissions' =>
+ hash_including(
+ 'setWorkItemMetadata' => false
+ )
+ )
+ end
+ end
+
context 'when the user can not read the work item' do
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 51b1d0160f4..4af5f7f574d 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3040,6 +3040,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
create(:project_group_link, project: project)
+ create(:project_group_link, project: project)
expect do
get api("/projects/#{project.id}", user)
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 7e9e87f96f5..d2270a124b7 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -55,6 +55,25 @@ RSpec.shared_examples 'work items comments' do |type|
end
end
+ context 'for work item note actions signed in user with developer role' do
+ it 'shows work item note actions' do
+ set_comment
+
+ click_button "Comment"
+
+ wait_for_requests
+
+ page.within(".main-notes-list") do
+ expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+
+ find('[data-testid="work-item-note-actions"]', match: :first).click
+
+ expect(page).to have_selector('[data-testid="copy-link-action"]')
+ expect(page).to have_selector('[data-testid="assign-note-action"]')
+ end
+ end
+ end
+
it 'successfully posts comments using shortcut and checks if textarea is blank when reinitiated' do
set_comment
@@ -257,3 +276,18 @@ RSpec.shared_examples 'work items milestone' do
expect(page.find(milestone_dropdown_selector)).to have_text('Add to milestone')
end
end
+
+RSpec.shared_examples 'work items comment actions for guest users' do
+ context 'for guest user' do
+ it 'hides other actions other than copy link' do
+ page.within(".main-notes-list") do
+ expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+
+ find('[data-testid="work-item-note-actions"]', match: :first).click
+
+ expect(page).to have_selector('[data-testid="copy-link-action"]')
+ expect(page).not_to have_selector('[data-testid="assign-note-action"]')
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 2eba40aa31d..305c1988cfd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1122,15 +1122,15 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
-"@gitlab/svgs@3.31.0":
- version "3.31.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.31.0.tgz#37e0a189def22400758267a84b61840a74aaf937"
- integrity sha512-VzbMlj7TSroWvHDBMvCF4EDOnozFah5wPSyI+YJ+eefQoX0Fzu6RIZ9h8+lhnRzffygcValdVNdnuzMbXB+Q/g==
-
-"@gitlab/ui@58.6.0":
- version "58.6.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-58.6.0.tgz#4f49ca6374fa376a53e5bad866155620bdaac45b"
- integrity sha512-OGkk5nxECUZ1vZEvar+49xz/PGdJoKzy9ZOIDF4cXTkRGtxXJApqglFH0Uy39l3mzjBhHMHZuLd0122wWj0XJA==
+"@gitlab/svgs@3.36.0":
+ version "3.36.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.36.0.tgz#2fc363008a060d41d0d0d2dfdfdeff892bc8842e"
+ integrity sha512-jlokfIwWwaFydzOYBe9W+8Iw4z7mmFqwkdg584sithgMKqYKUkofDGcgW5EHLnU9CESHVkqedpp3vVU4toebgA==
+
+"@gitlab/ui@58.8.0":
+ version "58.8.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-58.8.0.tgz#47662d2266fd6fea3c9dd594614cb594514dc519"
+ integrity sha512-pEvZc95FU3kKumd4ipXfSqeyuScgpB8bBPY5hq89sY7XlHNDc6rBpYTVgqvSkGahZ1LKGJWYDgebzMNWPYAswA==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.23.1"