Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-15 15:07:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-15 15:07:44 +0300
commit6a9ab27963fc1479fe7c78581b942c8dcce322e5 (patch)
tree8d32f4f66efde1b426658a74d0276e5250091ab7 /app
parent389d5aa505a916b0506b7b73dcc3be342d724976 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/google_cloud/components/home.vue8
-rw-r--r--app/assets/javascripts/google_cloud/components/revoke_oauth.vue38
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql24
-rw-r--r--app/assets/javascripts/header_search/store/getters.js12
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue22
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue28
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/constants.js25
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/escalation_status.vue61
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue135
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/utils.js5
-rw-r--r--app/assets/javascripts/sidebar/constants.js19
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js49
-rw-r--r--app/assets/javascripts/sidebar/queries/escalation_status.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql13
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue126
-rw-r--r--app/assets/stylesheets/pages/issuable.scss7
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb8
-rw-r--r--app/controllers/projects/google_cloud/gcp_regions_controller.rb7
-rw-r--r--app/controllers/projects/google_cloud/revoke_oauth_controller.rb23
-rw-r--r--app/controllers/projects/google_cloud/service_accounts_controller.rb7
-rw-r--r--app/controllers/projects/google_cloud_controller.rb16
-rw-r--r--app/controllers/projects/incidents_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/helpers/listbox_helper.rb13
-rw-r--r--app/models/project_team.rb32
-rw-r--r--app/models/wiki.rb13
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/ci/runners/register_runner_service.rb2
-rw-r--r--app/services/issuable_links/create_service.rb41
-rw-r--r--app/services/issue_links/create_service.rb27
-rw-r--r--app/services/merge_requests/approval_service.rb5
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/services/system_notes/issuables_service.rb5
-rw-r--r--app/views/admin/application_settings/ci_cd.html.haml2
-rw-r--r--app/views/clusters/clusters/_cluster_list.html.haml10
-rw-r--r--app/views/clusters/clusters/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml6
-rw-r--r--app/views/projects/settings/_general.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
49 files changed, 670 insertions, 209 deletions
diff --git a/app/assets/javascripts/google_cloud/components/home.vue b/app/assets/javascripts/google_cloud/components/home.vue
index 5f98eaf4a05..e41337e2679 100644
--- a/app/assets/javascripts/google_cloud/components/home.vue
+++ b/app/assets/javascripts/google_cloud/components/home.vue
@@ -1,6 +1,7 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import DeploymentsServiceTable from './deployments_service_table.vue';
+import RevokeOauth from './revoke_oauth.vue';
import ServiceAccountsList from './service_accounts_list.vue';
import GcpRegionsList from './gcp_regions_list.vue';
@@ -9,6 +10,7 @@ export default {
GlTabs,
GlTab,
DeploymentsServiceTable,
+ RevokeOauth,
ServiceAccountsList,
GcpRegionsList,
},
@@ -41,6 +43,10 @@ export default {
type: Array,
required: true,
},
+ revokeOauthUrl: {
+ type: String,
+ required: true,
+ },
},
};
</script>
@@ -61,6 +67,8 @@ export default {
:create-url="configureGcpRegionsUrl"
:list="gcpRegions"
/>
+ <hr v-if="revokeOauthUrl" />
+ <revoke-oauth v-if="revokeOauthUrl" :url="revokeOauthUrl" />
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployments-service-table
diff --git a/app/assets/javascripts/google_cloud/components/revoke_oauth.vue b/app/assets/javascripts/google_cloud/components/revoke_oauth.vue
new file mode 100644
index 00000000000..07d966894f6
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/revoke_oauth.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlButton, GlForm } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { s__ } from '~/locale';
+
+export const GOOGLE_CLOUD_REVOKE_TITLE = s__('GoogleCloud|Revoke authorizations');
+export const GOOGLE_CLOUD_REVOKE_DESCRIPTION = s__(
+ 'GoogleCloud|Revoke authorizations granted to GitLab. This does not invalidate service accounts.',
+);
+
+export default {
+ components: { GlButton, GlForm },
+ csrf,
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ title: GOOGLE_CLOUD_REVOKE_TITLE,
+ description: GOOGLE_CLOUD_REVOKE_DESCRIPTION,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mx-4">
+ <h2 class="gl-font-size-h2">{{ $options.i18n.title }}</h2>
+ <p>{{ $options.i18n.description }}</p>
+ <gl-form :action="url" method="post">
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <gl-button category="secondary" variant="warning" type="submit">
+ {{ $options.i18n.title }}
+ </gl-button>
+ </gl-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql b/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
new file mode 100644
index 00000000000..2bd016feb19
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
@@ -0,0 +1,24 @@
+#import "../fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query projectUsersSearchWithMRPermissions(
+ $search: String!
+ $fullPath: ID!
+ $mergeRequestId: MergeRequestID!
+) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ users: projectMembers(search: $search, relations: [DIRECT, INHERITED, INVITED_GROUPS]) {
+ nodes {
+ id
+ mergeRequestInteraction(id: $mergeRequestId) {
+ canMerge
+ }
+ user {
+ ...User
+ ...UserAvailability
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/header_search/store/getters.js b/app/assets/javascripts/header_search/store/getters.js
index adaacc6ecf0..87dec95153f 100644
--- a/app/assets/javascripts/header_search/store/getters.js
+++ b/app/assets/javascripts/header_search/store/getters.js
@@ -21,6 +21,8 @@ export const searchQuery = (state) => {
group_id: state.searchContext?.group?.id,
scope: state.searchContext?.scope,
snippets: state.searchContext?.for_snippets ? true : null,
+ search_code: state.searchContext?.code_search ? true : null,
+ repository_ref: state.searchContext?.ref,
},
isNil,
);
@@ -98,6 +100,8 @@ export const projectUrl = (state) => {
group_id: state.searchContext?.group?.id,
scope: state.searchContext?.scope,
snippets: state.searchContext?.for_snippets ? true : null,
+ search_code: state.searchContext?.code_search ? true : null,
+ repository_ref: state.searchContext?.ref,
},
isNil,
);
@@ -113,6 +117,8 @@ export const groupUrl = (state) => {
group_id: state.searchContext?.group?.id,
scope: state.searchContext?.scope,
snippets: state.searchContext?.for_snippets ? true : null,
+ search_code: state.searchContext?.code_search ? true : null,
+ repository_ref: state.searchContext?.ref,
},
isNil,
);
@@ -127,6 +133,8 @@ export const allUrl = (state) => {
nav_source: 'navbar',
scope: state.searchContext?.scope,
snippets: state.searchContext?.for_snippets ? true : null,
+ search_code: state.searchContext?.code_search ? true : null,
+ repository_ref: state.searchContext?.ref,
},
isNil,
);
@@ -140,7 +148,7 @@ export const scopedSearchOptions = (state, getters) => {
if (state.searchContext?.project) {
options.push({
html_id: 'scoped-in-project',
- scope: state.searchContext?.project.name,
+ scope: state.searchContext.project?.name || '',
description: MSG_IN_PROJECT,
url: getters.projectUrl,
});
@@ -149,7 +157,7 @@ export const scopedSearchOptions = (state, getters) => {
if (state.searchContext?.group) {
options.push({
html_id: 'scoped-in-group',
- scope: state.searchContext?.group.name,
+ scope: state.searchContext.group?.name || '',
description: MSG_IN_GROUP,
url: getters.groupUrl,
});
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index 7bc096ce2c8..9cb070a5517 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
@@ -2,7 +2,6 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { experiment } from '~/experimentation/utils';
import { DRAWER_EXPANDED_KEY } from '../../constants';
import FirstPipelineCard from './cards/first_pipeline_card.vue';
import GettingStartedCard from './cards/getting_started_card.vue';
@@ -50,29 +49,8 @@ export default {
},
mounted() {
this.setTopPosition();
- this.setInitialExpandState();
},
methods: {
- setInitialExpandState() {
- let isExpanded;
-
- experiment('pipeline_editor_walkthrough', {
- control: () => {
- isExpanded = true;
- },
- candidate: () => {
- isExpanded = false;
- },
- });
-
- // We check in the local storage and if no value is defined, we want the default
- // to be true. We want to explicitly set it to true here so that the drawer
- // animates to open on load.
- const localValue = localStorage.getItem(this.$options.localDrawerKey);
- if (localValue === null) {
- this.isExpanded = isExpanded;
- }
- },
setTopPosition() {
const navbarEl = document.querySelector('.js-navbar');
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index c75b1d4bb11..5cff93c884f 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -4,7 +4,6 @@ import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
@@ -66,7 +65,6 @@ export default {
GlTabs,
PipelineGraph,
TextEditor,
- GitlabExperiment,
WalkthroughPopover,
},
mixins: [glFeatureFlagsMixin()],
@@ -158,11 +156,7 @@ export default {
data-testid="editor-tab"
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
- <gitlab-experiment name="pipeline_editor_walkthrough">
- <template #candidate>
- <walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
- </template>
- </gitlab-experiment>
+ <walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
<ci-editor-header />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
index da9ff407faf..240e12ee597 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
@@ -1,5 +1,6 @@
<script>
import { GlIcon } from '@gitlab/ui';
+import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
export default {
@@ -31,10 +32,11 @@ export default {
);
},
isMergeRequest() {
- return this.issuableType === 'merge_request';
+ return this.issuableType === IssuableType.MergeRequest;
},
hasMergeIcon() {
- return this.isMergeRequest && !this.user.can_merge;
+ const canMerge = this.user.mergeRequestInteraction?.canMerge || this.user.can_merge;
+ return this.isMergeRequest && !canMerge;
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index 2a237e7ace0..578c344da02 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { IssuableType } from '~/issues/constants';
import { __ } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import AssigneeAvatar from './assignee_avatar.vue';
@@ -71,7 +72,8 @@ export default {
},
computed: {
cannotMerge() {
- return this.issuableType === 'merge_request' && !this.user.can_merge;
+ const canMerge = this.user.mergeRequestInteraction?.canMerge || this.user.can_merge;
+ return this.issuableType === IssuableType.MergeRequest && !canMerge;
},
tooltipTitle() {
const { name = '', availability = '' } = this.user;
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 6a74ab83c22..856687c00ae 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -58,7 +58,7 @@ export default {
return this.users.length > 2;
},
allAssigneesCanMerge() {
- return this.users.every((user) => user.can_merge);
+ return this.users.every((user) => user.can_merge || user.mergeRequestInteraction?.canMerge);
},
sidebarAvatarCounter() {
if (this.users.length > DEFAULT_MAX_COUNTER) {
@@ -77,7 +77,9 @@ export default {
return '';
}
- const mergeLength = this.users.filter((u) => u.can_merge).length;
+ const mergeLength = this.users.filter(
+ (u) => u.can_merge || u.mergeRequestInteraction?.canMerge,
+ ).length;
if (mergeLength === this.users.length) {
return '';
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index a3379784bc1..59a4eb54bbe 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -44,7 +44,7 @@ export default {
<div class="gl-display-flex gl-flex-direction-column issuable-assignees">
<div
v-if="emptyUsers"
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-2 hide-collapsed"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 hide-collapsed"
data-testid="none"
>
<span> {{ __('None') }}</span>
@@ -65,7 +65,7 @@ export default {
v-else
:users="users"
:issuable-type="issuableType"
- class="gl-text-gray-800 gl-mt-2 hide-collapsed"
+ class="gl-text-gray-800 hide-collapsed"
@toggle-attention-requested="toggleAttentionRequested"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 9c031ae64f8..7743004a293 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -1,6 +1,5 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
-import { cloneDeep } from 'lodash';
import Vue from 'vue';
import createFlash from '~/flash';
import { IssuableType } from '~/issues/constants';
@@ -101,7 +100,10 @@ export default {
}
const issuable = data.workspace?.issuable;
if (issuable) {
- this.selected = cloneDeep(issuable.assignees.nodes);
+ this.selected = issuable.assignees.nodes.map((node) => ({
+ ...node,
+ canMerge: node.mergeRequestInteraction?.canMerge || false,
+ }));
}
},
error() {
@@ -141,6 +143,7 @@ export default {
username: gon?.current_username,
name: gon?.current_user_fullname,
avatarUrl: gon?.current_user_avatar_url,
+ canMerge: this.issuable?.userPermissions?.canMerge || false,
};
},
signedIn() {
@@ -206,8 +209,8 @@ export default {
expandWidget() {
this.$refs.toggle.expand();
},
- focusSearch() {
- this.$refs.userSelect.focusSearch();
+ showDropdown() {
+ this.$refs.userSelect.showDropdown();
},
showError() {
createFlash({ message: __('An error occurred while fetching participants.') });
@@ -236,11 +239,11 @@ export default {
:initial-loading="isAssigneesLoading"
:title="assigneeText"
:is-dirty="isDirty"
- @open="focusSearch"
+ @open="showDropdown"
@close="saveAssignees"
>
<template #collapsed>
- <slot name="collapsed" :users="assignees" :on-click="expandWidget"></slot>
+ <slot name="collapsed" :users="assignees"></slot>
<issuable-assignees
:users="assignees"
:issuable-type="issuableType"
@@ -256,12 +259,13 @@ export default {
:text="$options.i18n.assignees"
:header-text="$options.i18n.assignTo"
:iid="iid"
+ :issuable-id="issuableId"
:full-path="fullPath"
:allow-multiple-assignees="allowMultipleAssignees"
:current-user="currentUser"
:issuable-type="issuableType"
:is-editing="edit"
- class="gl-w-full dropdown-menu-user"
+ class="gl-w-full dropdown-menu-user gl-mt-n3"
@toggle="collapseWidget"
@error="showError"
@input="setDirtyState"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
index 8ef65ef7308..28bc5afc1a4 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
@@ -30,6 +30,6 @@ export default {
:event="$options.dataTrackEvent"
:label="$options.dataTrackLabel"
:trigger-source="triggerSource"
- classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
+ classes="gl-display-block gl-pl-0 gl-hover-text-decoration-none gl-hover-text-blue-800!"
/>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
index e2a38a100b9..19f588b28be 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
@@ -1,17 +1,24 @@
<script>
-import { GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui';
+import { IssuableType } from '~/issues/constants';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlAvatarLabeled,
GlAvatarLink,
+ GlIcon,
},
props: {
user: {
type: Object,
required: true,
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: IssuableType.Issue,
+ },
},
computed: {
userLabel() {
@@ -22,6 +29,9 @@ export default {
author: this.user.name,
});
},
+ hasCannotMergeIcon() {
+ return this.issuableType === IssuableType.MergeRequest && !this.user.canMerge;
+ },
},
};
</script>
@@ -31,9 +41,19 @@ export default {
<gl-avatar-labeled
:size="32"
:label="userLabel"
- :sub-label="user.username"
+ :sub-label="`@${user.username}`"
:src="user.avatarUrl || user.avatar || user.avatar_url"
- class="gl-align-items-center"
- />
+ class="gl-align-items-center gl-relative"
+ >
+ <template #meta>
+ <gl-icon
+ v-if="hasCannotMergeIcon"
+ name="warning-solid"
+ aria-hidden="true"
+ class="merge-icon"
+ :size="12"
+ />
+ </template>
+ </gl-avatar-labeled>
</gl-avatar-link>
</template>
diff --git a/app/assets/javascripts/sidebar/components/incidents/constants.js b/app/assets/javascripts/sidebar/components/incidents/constants.js
new file mode 100644
index 00000000000..cd05a6099fd
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/incidents/constants.js
@@ -0,0 +1,25 @@
+import { s__ } from '~/locale';
+
+export const STATUS_TRIGGERED = 'TRIGGERED';
+export const STATUS_ACKNOWLEDGED = 'ACKNOWLEDGED';
+export const STATUS_RESOLVED = 'RESOLVED';
+
+export const STATUS_TRIGGERED_LABEL = s__('IncidentManagement|Triggered');
+export const STATUS_ACKNOWLEDGED_LABEL = s__('IncidentManagement|Acknowledged');
+export const STATUS_RESOLVED_LABEL = s__('IncidentManagement|Resolved');
+
+export const STATUS_LABELS = {
+ [STATUS_TRIGGERED]: STATUS_TRIGGERED_LABEL,
+ [STATUS_ACKNOWLEDGED]: STATUS_ACKNOWLEDGED_LABEL,
+ [STATUS_RESOLVED]: STATUS_RESOLVED_LABEL,
+};
+
+export const i18n = {
+ fetchError: s__(
+ 'IncidentManagement|An error occurred while fetching the incident status. Please reload the page.',
+ ),
+ title: s__('IncidentManagement|Status'),
+ updateError: s__(
+ 'IncidentManagement|An error occurred while updating the incident status. Please reload the page and try again.',
+ ),
+};
diff --git a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
new file mode 100644
index 00000000000..2c32cf89387
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { i18n, STATUS_ACKNOWLEDGED, STATUS_TRIGGERED, STATUS_RESOLVED } from './constants';
+import { getStatusLabel } from './utils';
+
+const STATUS_LIST = [STATUS_TRIGGERED, STATUS_ACKNOWLEDGED, STATUS_RESOLVED];
+
+export default {
+ i18n,
+ STATUS_LIST,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: null,
+ validator(value) {
+ return [...STATUS_LIST, null].includes(value);
+ },
+ },
+ },
+ computed: {
+ currentStatusLabel() {
+ return this.getStatusLabel(this.value);
+ },
+ },
+ methods: {
+ show() {
+ this.$refs.dropdown.show();
+ },
+ hide() {
+ this.$refs.dropdown.hide();
+ },
+ getStatusLabel,
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="dropdown"
+ block
+ :text="currentStatusLabel"
+ toggle-class="dropdown-menu-toggle gl-mb-2"
+ >
+ <slot name="header"> </slot>
+ <gl-dropdown-item
+ v-for="status in $options.STATUS_LIST"
+ :key="status"
+ data-testid="status-dropdown-item"
+ :is-check-item="true"
+ :is-checked="status === value"
+ @click="$emit('input', status)"
+ >
+ {{ getStatusLabel(status) }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
new file mode 100644
index 00000000000..67ae1e6fcab
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
@@ -0,0 +1,135 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { escalationStatusQuery, escalationStatusMutation } from '~/sidebar/constants';
+import { createAlert } from '~/flash';
+import { logError } from '~/lib/logger';
+import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
+import SidebarEditableItem from '../sidebar_editable_item.vue';
+import { i18n } from './constants';
+import { getStatusLabel } from './utils';
+
+export default {
+ i18n,
+ components: {
+ EscalationStatus,
+ SidebarEditableItem,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ iid: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ status: null,
+ isUpdating: false,
+ };
+ },
+ apollo: {
+ status: {
+ query() {
+ return escalationStatusQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.iid,
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.escalationStatus;
+ },
+ error(error) {
+ const message = this.$options.i18n.fetchError;
+ createAlert({ message });
+ logError(message, error);
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.status.loading;
+ },
+ currentStatusLabel() {
+ return getStatusLabel(this.status);
+ },
+ tooltipText() {
+ return `${this.$options.i18n.title}: ${this.currentStatusLabel}`;
+ },
+ },
+ methods: {
+ updateStatus(status) {
+ this.isUpdating = true;
+ this.closeSidebar();
+ return this.$apollo
+ .mutate({
+ mutation: escalationStatusMutation,
+ variables: {
+ status,
+ iid: this.iid,
+ projectPath: this.projectPath,
+ },
+ })
+ .then(({ data: { issueSetEscalationStatus } }) => {
+ this.status = issueSetEscalationStatus.issue.escalationStatus;
+ })
+ .catch((error) => {
+ const message = this.$options.i18n.updateError;
+ createAlert({ message });
+ logError(message, error);
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ closeSidebar() {
+ this.close();
+ this.$refs.editable.collapse();
+ },
+ open() {
+ this.$refs.escalationStatus.show();
+ },
+ close() {
+ this.$refs.escalationStatus.hide();
+ },
+ },
+};
+</script>
+
+<template>
+ <sidebar-editable-item
+ ref="editable"
+ :title="$options.i18n.title"
+ :initial-loading="isLoading"
+ :loading="isUpdating"
+ @open="open"
+ @close="close"
+ >
+ <template #default>
+ <escalation-status ref="escalationStatus" :value="status" @input="updateStatus" />
+ </template>
+ <template #collapsed>
+ <div
+ v-gl-tooltip.viewport.left="tooltipText"
+ class="sidebar-collapsed-icon"
+ data-testid="status-icon"
+ >
+ <gl-icon name="status" :size="16" />
+ </div>
+ <span class="hide-collapsed text-secondary">{{ currentStatusLabel }}</span>
+ </template>
+ </sidebar-editable-item>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/incidents/utils.js b/app/assets/javascripts/sidebar/components/incidents/utils.js
new file mode 100644
index 00000000000..59bf1ea466c
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/incidents/utils.js
@@ -0,0 +1,5 @@
+import { s__ } from '~/locale';
+
+import { STATUS_LABELS } from './constants';
+
+export const getStatusLabel = (status) => STATUS_LABELS[status] ?? s__('IncidentManagement|None');
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 0238fb8e8d5..989dc574bc3 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,7 +1,8 @@
import { s__, sprintf } from '~/locale';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
+import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { IssuableType, WorkspaceType } from '~/issues/constants';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
@@ -49,12 +50,12 @@ import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries
import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
+import getEscalationStatusQuery from '~/sidebar/queries/escalation_status.query.graphql';
+import updateEscalationStatusMutation from '~/sidebar/queries/update_escalation_status.mutation.graphql';
import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql';
import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
import projectMilestonesQuery from './queries/project_milestones.query.graphql';
-export const ASSIGNEES_DEBOUNCE_DELAY = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
-
export const defaultEpicSort = 'TITLE_ASC';
export const epicIidPattern = /^&(?<iid>\d+)$/;
@@ -91,6 +92,15 @@ export const participantsQueries = {
},
};
+export const userSearchQueries = {
+ [IssuableType.Issue]: {
+ query: userSearchQuery,
+ },
+ [IssuableType.MergeRequest]: {
+ query: userSearchWithMRPermissionsQuery,
+ },
+};
+
export const confidentialityQueries = {
[IssuableType.Issue]: {
query: issueConfidentialQuery,
@@ -305,3 +315,6 @@ export function dropdowni18nText(issuableAttribute, issuableType) {
),
};
}
+
+export const escalationStatusQuery = getEscalationStatusQuery;
+export const escalationStatusMutation = updateEscalationStatusMutation;
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 0fb31ec92ca..2a7d967cb61 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -10,6 +10,7 @@ import {
isInIssuePage,
isInDesignPage,
isInIncidentPage,
+ isInMRPage,
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -31,6 +32,7 @@ import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests
import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
+import SidebarEscalationStatus from './components/incidents/sidebar_escalation_status.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
@@ -135,6 +137,8 @@ function mountAssigneesComponent() {
if (!el) return;
const { id, iid, fullPath, editable } = getSidebarOptions();
+ const isIssuablePage = isInIssuePage() || isInIncidentPage() || isInDesignPage();
+ const issuableType = isIssuablePage ? IssuableType.Issue : IssuableType.MergeRequest;
// eslint-disable-next-line no-new
new Vue({
el,
@@ -152,21 +156,16 @@ function mountAssigneesComponent() {
props: {
iid: String(iid),
fullPath,
- issuableType:
- isInIssuePage() || isInIncidentPage() || isInDesignPage()
- ? IssuableType.Issue
- : IssuableType.MergeRequest,
+ issuableType,
issuableId: id,
allowMultipleAssignees: !el.dataset.maxAssignees,
},
scopedSlots: {
- collapsed: ({ users, onClick }) =>
+ collapsed: ({ users }) =>
createElement(CollapsedAssigneeList, {
props: {
users,
- },
- nativeOn: {
- click: onClick,
+ issuableType,
},
}),
},
@@ -568,6 +567,36 @@ function mountSeverityComponent() {
});
}
+function mountEscalationStatusComponent() {
+ const statusContainerEl = document.querySelector('#js-escalation-status');
+
+ if (!statusContainerEl) {
+ return false;
+ }
+
+ const { issuableType } = getSidebarOptions();
+ const { canUpdate, issueIid, projectPath } = statusContainerEl.dataset;
+
+ return new Vue({
+ el: statusContainerEl,
+ apolloProvider,
+ components: {
+ SidebarEscalationStatus,
+ },
+ provide: {
+ canUpdate: parseBoolean(canUpdate),
+ },
+ render: (createElement) =>
+ createElement('sidebar-escalation-status', {
+ props: {
+ iid: issueIid,
+ issuableType,
+ projectPath,
+ },
+ }),
+ });
+}
+
function mountCopyEmailComponent() {
const el = document.getElementById('issuable-copy-email');
@@ -585,7 +614,7 @@ function mountCopyEmailComponent() {
}
const isAssigneesWidgetShown =
- (isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget;
+ (isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
export function mountSidebar(mediator, store) {
initInviteMembersModal();
@@ -619,6 +648,8 @@ export function mountSidebar(mediator, store) {
mountSeverityComponent();
+ mountEscalationStatusComponent();
+
if (window.gon?.features?.mrAttentionRequests) {
eventHub.$on('removeCurrentUserAttentionRequested', () => {
mediator.removeCurrentUserAttentionRequested();
diff --git a/app/assets/javascripts/sidebar/queries/escalation_status.query.graphql b/app/assets/javascripts/sidebar/queries/escalation_status.query.graphql
new file mode 100644
index 00000000000..cb7c5a0fbe7
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/escalation_status.query.graphql
@@ -0,0 +1,9 @@
+query escalationStatusQuery($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: issue(iid: $iid) {
+ id
+ escalationStatus
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql
new file mode 100644
index 00000000000..a4aff7968df
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql
@@ -0,0 +1,10 @@
+mutation updateEscalationStatus($projectPath: ID!, $status: IssueEscalationStatus!, $iid: String!) {
+ issueSetEscalationStatus(input: { projectPath: $projectPath, status: $status, iid: $iid }) {
+ errors
+ clientMutationId
+ issue {
+ id
+ escalationStatus
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
index 81e19e48d75..7127940bb05 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
@@ -10,8 +10,14 @@ query getMrAssignees($fullPath: ID!, $iid: String!) {
nodes {
...User
...UserAvailability
+ mergeRequestInteraction {
+ canMerge
+ }
}
}
+ userPermissions {
+ canMerge
+ }
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
index 77140ea36d8..5fec2ccbdfb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
@@ -2,21 +2,18 @@
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
- mergeRequestSetAssignees(
+ issuableSetAssignees: mergeRequestSetAssignees(
input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $fullPath }
) {
- mergeRequest {
+ issuable: mergeRequest {
id
assignees {
nodes {
...User
...UserAvailability
- }
- }
- participants {
- nodes {
- ...User
- ...UserAvailability
+ mergeRequestInteraction {
+ canMerge
+ }
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index b85cae0c64f..9df5254155e 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -1,4 +1,5 @@
<script>
+import { debounce } from 'lodash';
import {
GlDropdown,
GlDropdownForm,
@@ -6,11 +7,14 @@ import {
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
+ GlTooltipDirective,
} from '@gitlab/ui';
-import searchUsers from '~/graphql_shared/queries/users_search.query.graphql';
import { __ } from '~/locale';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
-import { ASSIGNEES_DEBOUNCE_DELAY, participantsQueries } from '~/sidebar/constants';
+import { IssuableType } from '~/issues/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { participantsQueries, userSearchQueries } from '~/sidebar/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
export default {
i18n: {
@@ -25,6 +29,9 @@ export default {
SidebarParticipant,
GlLoadingIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
headerText: {
type: String,
@@ -58,13 +65,18 @@ export default {
issuableType: {
type: String,
required: false,
- default: 'issue',
+ default: IssuableType.Issue,
},
isEditing: {
type: Boolean,
required: false,
default: true,
},
+ issuableId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -89,28 +101,35 @@ export default {
};
},
update(data) {
- return data.workspace?.issuable?.participants.nodes;
+ return data.workspace?.issuable?.participants.nodes.map((node) => ({
+ ...node,
+ canMerge: false,
+ }));
},
error() {
this.$emit('error');
},
},
searchUsers: {
- query: searchUsers,
+ query() {
+ return userSearchQueries[this.issuableType].query;
+ },
variables() {
- return {
- fullPath: this.fullPath,
- search: this.search,
- first: 20,
- };
+ return this.searchUsersVariables;
},
skip() {
return !this.isEditing;
},
update(data) {
- return data.workspace?.users?.nodes.filter((x) => x?.user).map(({ user }) => user) || [];
+ return (
+ data.workspace?.users?.nodes
+ .filter((x) => x?.user)
+ .map((node) => ({
+ ...node.user,
+ canMerge: node.mergeRequestInteraction?.canMerge || false,
+ })) || []
+ );
},
- debounce: ASSIGNEES_DEBOUNCE_DELAY,
error() {
this.$emit('error');
this.isSearching = false;
@@ -121,6 +140,23 @@ export default {
},
},
computed: {
+ isMergeRequest() {
+ return this.issuableType === IssuableType.MergeRequest;
+ },
+ searchUsersVariables() {
+ const variables = {
+ fullPath: this.fullPath,
+ search: this.search,
+ first: 20,
+ };
+ if (!this.isMergeRequest) {
+ return variables;
+ }
+ return {
+ ...variables,
+ mergeRequestId: convertToGraphQLId('MergeRequest', this.issuableId),
+ };
+ },
isLoading() {
return this.$apollo.queries.searchUsers.loading || this.$apollo.queries.participants.loading;
},
@@ -135,8 +171,8 @@ export default {
// TODO this de-duplication is temporary (BE fix required)
// https://gitlab.com/gitlab-org/gitlab/-/issues/327822
- const mergedSearchResults = filteredParticipants
- .concat(this.searchUsers)
+ const mergedSearchResults = this.searchUsers
+ .concat(filteredParticipants)
.reduce(
(acc, current) => (acc.some((user) => current.id === user.id) ? acc : [...acc, current]),
[],
@@ -179,6 +215,7 @@ export default {
return this.selectedFiltered.length === 0;
},
},
+
watch: {
// We need to add this watcher to track the moment when user is alredy typing
// but query is still not started due to debounce
@@ -188,15 +225,21 @@ export default {
}
},
},
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
methods: {
selectAssignee(user) {
let selected = [...this.value];
if (!this.allowMultipleAssignees) {
selected = [user];
+ this.$emit('input', selected);
+ this.$refs.dropdown.hide();
+ this.$emit('toggle');
} else {
selected.push(user);
+ this.$emit('input', selected);
}
- this.$emit('input', selected);
},
unselect(name) {
const selected = this.value.filter((user) => user.username !== name);
@@ -205,6 +248,9 @@ export default {
focusSearch() {
this.$refs.search.focusInput();
},
+ showDropdown() {
+ this.$refs.dropdown.show();
+ },
showDivider(list) {
return list.length > 0 && this.isSearchEmpty;
},
@@ -216,22 +262,37 @@ export default {
const currentUser = usersCopy.find((user) => user.username === this.currentUser.username);
if (currentUser) {
+ currentUser.canMerge = this.currentUser.canMerge;
const index = usersCopy.indexOf(currentUser);
usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]);
}
return usersCopy;
},
+ setSearchKey(value) {
+ this.search = value.trim();
+ },
+ tooltipText(user) {
+ if (!this.isMergeRequest) {
+ return '';
+ }
+ return user.canMerge ? '' : __('Cannot merge');
+ },
},
};
</script>
<template>
- <gl-dropdown class="show" :text="text" @toggle="$emit('toggle')">
+ <gl-dropdown ref="dropdown" :text="text" @toggle="$emit('toggle')" @shown="focusSearch">
<template #header>
<p class="gl-font-weight-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p>
<gl-dropdown-divider />
- <gl-search-box-by-type ref="search" v-model.trim="search" class="js-dropdown-input-field" />
+ <gl-search-box-by-type
+ ref="search"
+ :value="search"
+ class="js-dropdown-input-field"
+ @input="debouncedSearchKeyUpdate"
+ />
</template>
<gl-dropdown-form class="gl-relative gl-min-h-7">
<gl-loading-icon
@@ -247,7 +308,7 @@ export default {
:is-checked="selectedIsEmpty"
:is-check-centered="true"
data-testid="unassign"
- @click="$emit('input', [])"
+ @click.native.capture.stop="$emit('input', [])"
>
<span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">{{
$options.i18n.unassigned
@@ -258,27 +319,44 @@ export default {
<gl-dropdown-item
v-for="item in selectedFiltered"
:key="item.id"
+ v-gl-tooltip.left.viewport
+ :title="tooltipText(item)"
+ boundary="viewport"
is-checked
is-check-centered
data-testid="selected-participant"
- @click.stop="unselect(item.username)"
+ @click.native.capture.stop="unselect(item.username)"
>
- <sidebar-participant :user="item" />
+ <sidebar-participant :user="item" :issuable-type="issuableType" />
</gl-dropdown-item>
<template v-if="showCurrentUser">
<gl-dropdown-divider />
- <gl-dropdown-item data-testid="current-user" @click.stop="selectAssignee(currentUser)">
- <sidebar-participant :user="currentUser" class="gl-pl-6!" />
+ <gl-dropdown-item
+ data-testid="current-user"
+ @click.native.capture.stop="selectAssignee(currentUser)"
+ >
+ <sidebar-participant
+ :user="currentUser"
+ :issuable-type="issuableType"
+ class="gl-pl-6!"
+ />
</gl-dropdown-item>
</template>
<gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
<gl-dropdown-item
v-for="unselectedUser in unselectedFiltered"
:key="unselectedUser.id"
+ v-gl-tooltip.left.viewport
+ :title="tooltipText(unselectedUser)"
+ boundary="viewport"
data-testid="unselected-participant"
- @click="selectAssignee(unselectedUser)"
+ @click.native.capture.stop="selectAssignee(unselectedUser)"
>
- <sidebar-participant :user="unselectedUser" class="gl-pl-6!" />
+ <sidebar-participant
+ :user="unselectedUser"
+ :issuable-type="issuableType"
+ class="gl-pl-6!"
+ />
</gl-dropdown-item>
<gl-dropdown-item v-if="noUsersFound" data-testid="empty-results" class="gl-pl-6!">
{{ __('No matching results') }}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6a27a0770c0..c00af802c06 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -108,12 +108,15 @@
.merge-icon {
color: $orange-400;
position: absolute;
- bottom: 0;
- right: 0;
filter: drop-shadow(0 0 0.5px $white) drop-shadow(0 0 1px $white) drop-shadow(0 0 2px $white);
}
}
+.assignee .merge-icon {
+ top: calc(50% + 0.25rem);
+ left: 1.275rem;
+}
+
.reviewer .merge-icon {
bottom: -3px;
right: -3px;
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 6f12e3940dd..8c6e8f0e126 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -2,7 +2,6 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
- before_action :setup_walkthrough_experiment, only: :show
before_action do
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end
@@ -19,11 +18,4 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
def check_can_collaborate!
render_404 unless can_collaborate_with_project?(@project)
end
-
- def setup_walkthrough_experiment
- experiment(:pipeline_editor_walkthrough, namespace: @project.namespace, sticky_to: current_user) do |e|
- e.candidate {}
- e.publish_to_database
- end
- end
end
diff --git a/app/controllers/projects/google_cloud/gcp_regions_controller.rb b/app/controllers/projects/google_cloud/gcp_regions_controller.rb
index c0531e5d2f5..beeb91cfd80 100644
--- a/app/controllers/projects/google_cloud/gcp_regions_controller.rb
+++ b/app/controllers/projects/google_cloud/gcp_regions_controller.rb
@@ -12,13 +12,14 @@ class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseC
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
tags = TagsFinder.new(project.repository, params).execute(gitaly_pagination: true)
refs = (branches + tags).map(&:name)
- @js_data = {
+ js_data = {
screen: 'gcp_regions_form',
availableRegions: AVAILABLE_REGIONS,
refs: refs,
cancelPath: project_google_cloud_index_path(project)
- }.to_json
- track_event('gcp_regions#index', 'form_render', @js_data)
+ }
+ @js_data = js_data.to_json
+ track_event('gcp_regions#index', 'form_render', js_data)
end
def create
diff --git a/app/controllers/projects/google_cloud/revoke_oauth_controller.rb b/app/controllers/projects/google_cloud/revoke_oauth_controller.rb
new file mode 100644
index 00000000000..03d1474707b
--- /dev/null
+++ b/app/controllers/projects/google_cloud/revoke_oauth_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Projects::GoogleCloud::RevokeOauthController < Projects::GoogleCloud::BaseController
+ before_action :validate_gcp_token!
+
+ def create
+ google_api_client = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ response = google_api_client.revoke_authorizations
+
+ if response.success?
+ status = 'success'
+ redirect_message = { notice: s_('GoogleCloud|Google OAuth2 token revocation requested') }
+ else
+ status = 'failed'
+ redirect_message = { alert: s_('GoogleCloud|Google OAuth2 token revocation request failed') }
+ end
+
+ session.delete(GoogleApi::CloudPlatform::Client.session_key_for_token)
+ track_event('revoke_oauth#create', 'create', status)
+
+ redirect_to project_google_cloud_index_path(project), redirect_message
+ end
+end
diff --git a/app/controllers/projects/google_cloud/service_accounts_controller.rb b/app/controllers/projects/google_cloud/service_accounts_controller.rb
index 9c4dd35a6f5..5d8b2030d5c 100644
--- a/app/controllers/projects/google_cloud/service_accounts_controller.rb
+++ b/app/controllers/projects/google_cloud/service_accounts_controller.rb
@@ -17,14 +17,15 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
tags = TagsFinder.new(project.repository, params).execute(gitaly_pagination: true)
refs = (branches + tags).map(&:name)
- @js_data = {
+ js_data = {
screen: 'service_accounts_form',
gcpProjects: gcp_projects,
refs: refs,
cancelPath: project_google_cloud_index_path(project)
- }.to_json
+ }
+ @js_data = js_data.to_json
- track_event('service_accounts#index', 'form_success', @js_data)
+ track_event('service_accounts#index', 'form_success', js_data)
end
rescue Google::Apis::ClientError => error
handle_gcp_error('service_accounts#index', error)
diff --git a/app/controllers/projects/google_cloud_controller.rb b/app/controllers/projects/google_cloud_controller.rb
index e52e14a75bc..49bb4bec859 100644
--- a/app/controllers/projects/google_cloud_controller.rb
+++ b/app/controllers/projects/google_cloud_controller.rb
@@ -4,7 +4,7 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
GCP_REGION_CI_VAR_KEY = 'GCP_REGION'
def index
- @js_data = {
+ js_data = {
screen: 'home',
serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
@@ -12,9 +12,11 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
- gcpRegions: gcp_regions
- }.to_json
- track_event('google_cloud#index', 'index', @js_data)
+ gcpRegions: gcp_regions,
+ revokeOauthUrl: revoke_oauth_url
+ }
+ @js_data = js_data.to_json
+ track_event('google_cloud#index', 'index', js_data)
end
private
@@ -23,4 +25,10 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
list = ::Ci::VariablesFinder.new(project, { key: GCP_REGION_CI_VAR_KEY }).execute
list.map { |variable| { gcp_region: variable.value, environment: variable.environment_scope } }
end
+
+ def revoke_oauth_url
+ google_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ google_token_valid ? project_google_cloud_revoke_oauth_index_path(project) : nil
+ end
end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 6f67b07c6a0..293581a6744 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -48,3 +48,5 @@ class Projects::IncidentsController < Projects::ApplicationController
IssueSerializer.new(current_user: current_user, project: incident.project)
end
end
+
+Projects::IncidentsController.prepend_mod
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 04f311f58e9..5259bf90dd0 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -43,6 +43,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml)
push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml)
push_frontend_feature_flag(:secure_vulnerability_training, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml)
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
diff --git a/app/helpers/listbox_helper.rb b/app/helpers/listbox_helper.rb
index d24680bc0b0..16caf862c7b 100644
--- a/app/helpers/listbox_helper.rb
+++ b/app/helpers/listbox_helper.rb
@@ -16,8 +16,10 @@ module ListboxHelper
# the sort key), `text` is the user-facing string for the item, and `href` is
# the path to redirect to when that item is selected.
#
- # The `selected` parameter is the currently selected `value`, and must
- # correspond to one of the `items`, or be `nil`. When `selected.nil?`, the first item is selected.
+ # The `selected` parameter is the currently selected `value`, and should
+ # correspond to one of the `items`, or be `nil`. When `selected.nil?` or
+ # a value which does not correspond to one of the items, the first item is
+ # selected.
#
# The final parameter `html_options` applies arbitrary attributes to the
# returned tag. Some of these are passed to the underlying Vue component as
@@ -37,9 +39,12 @@ module ListboxHelper
webpack_bundle_tag 'redirect_listbox'
end
- selected ||= items.first[:value]
selected_option = items.find { |opt| opt[:value] == selected }
- raise ArgumentError, "cannot find #{selected} in #{items}" unless selected_option
+
+ unless selected_option
+ selected_option = items.first
+ selected = selected_option[:value]
+ end
button = button_tag(type: :button, class: DROPDOWN_BUTTON_CLASSES) do
content_tag(:span, selected_option[:text], class: DROPDOWN_INNER_CLASS) +
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d5e0d112aeb..4b89d95c1a3 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -198,31 +198,15 @@ class ProjectTeam
end
def contribution_check_for_user_ids(user_ids)
- user_ids = user_ids.uniq
- key = "contribution_check_for_users:#{project.id}"
-
- Gitlab::SafeRequestStore[key] ||= {}
- contributors = Gitlab::SafeRequestStore[key] || {}
-
- user_ids -= contributors.keys
-
- return contributors if user_ids.empty?
-
- resource_contributors = project.merge_requests
- .merged
- .where(author_id: user_ids, target_branch: project.default_branch.to_s)
- .pluck(:author_id)
- .product([true]).to_h
-
- contributors.merge!(resource_contributors)
-
- missing_resource_ids = user_ids - resource_contributors.keys
-
- missing_resource_ids.each do |resource_id|
- contributors[resource_id] = false
+ Gitlab::SafeRequestLoader.execute(resource_key: "contribution_check_for_users:#{project.id}",
+ resource_ids: user_ids,
+ default_value: false) do |user_ids|
+ project.merge_requests
+ .merged
+ .where(author_id: user_ids, target_branch: project.default_branch.to_s)
+ .pluck(:author_id)
+ .product([true]).to_h
end
-
- contributors
end
def contributor?(user_id)
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 307f31edfef..622070abd88 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -87,8 +87,7 @@ class Wiki
end
def create_wiki_repository
- repository.create_if_not_exists
- change_head_to_default_branch
+ repository.create_if_not_exists(default_branch)
raise CouldNotCreateWikiError unless repository_exists?
rescue StandardError => err
@@ -322,16 +321,6 @@ class Wiki
def default_message(action, title)
"#{user.username} #{action} page: #{title}"
end
-
- def change_head_to_default_branch
- # If the wiki has commits in the 'HEAD' branch means that the current
- # HEAD is pointing to the right branch. If not, it could mean that either
- # the repo has just been created or that 'HEAD' is pointing
- # to the wrong branch and we need to rewrite it
- return if repository.raw_repository.commit_count('HEAD') != 0
-
- repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}")
- end
end
Wiki.prepend_mod_with('Wiki')
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 1dcf4409048..7a49ad3d4aa 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -81,7 +81,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:crm_enabled, score: 0, scope: :subject) { Feature.enabled?(:customer_relations, @subject) && @subject.crm_enabled? }
condition(:group_runner_registration_allowed) do
- Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
+ Feature.disabled?(:runner_registration_control, default_enabled: :yaml) || Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
end
rule { can?(:read_group) & design_management_enabled }.policy do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index e7b63d5e17f..09085bef9f0 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -195,7 +195,7 @@ class ProjectPolicy < BasePolicy
end
condition(:project_runner_registration_allowed) do
- Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('project')
+ Feature.disabled?(:runner_registration_control, default_enabled: :yaml) || Gitlab::CurrentSettings.valid_runner_registrars.include?('project')
end
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
diff --git a/app/services/ci/runners/register_runner_service.rb b/app/services/ci/runners/register_runner_service.rb
index 196d2de1a65..7978d094d9b 100644
--- a/app/services/ci/runners/register_runner_service.rb
+++ b/app/services/ci/runners/register_runner_service.rb
@@ -47,7 +47,7 @@ module Ci
end
def runner_registrar_valid?(type)
- Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
+ Feature.disabled?(:runner_registration_control, default_enabled: :yaml) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
end
def token_scope
diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb
index 81685f81afa..64a64f50ded 100644
--- a/app/services/issuable_links/create_service.rb
+++ b/app/services/issuable_links/create_service.rb
@@ -36,6 +36,20 @@ module IssuableLinks
success
end
+ # rubocop: disable CodeReuse/ActiveRecord
+ def relate_issuables(referenced_issuable)
+ link = link_class.find_or_initialize_by(source: issuable, target: referenced_issuable)
+
+ set_link_type(link)
+
+ if link.changed? && link.save
+ create_notes(referenced_issuable)
+ end
+
+ link
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
private
def render_conflict_error?
@@ -96,6 +110,23 @@ module IssuableLinks
{}
end
+ def issuables_assigned_message
+ _('%{issuable}(s) already assigned' % { issuable: target_issuable_type.capitalize })
+ end
+
+ def issuables_not_found_message
+ _('No matching %{issuable} found. Make sure that you are adding a valid %{issuable} URL.' % { issuable: target_issuable_type })
+ end
+
+ def target_issuable_type
+ :issue
+ end
+
+ def create_notes(referenced_issuable)
+ SystemNoteService.relate_issuable(issuable, referenced_issuable, current_user)
+ SystemNoteService.relate_issuable(referenced_issuable, issuable, current_user)
+ end
+
def linkable_issuables(objects)
raise NotImplementedError
end
@@ -104,16 +135,12 @@ module IssuableLinks
raise NotImplementedError
end
- def relate_issuables(referenced_object)
+ def link_class
raise NotImplementedError
end
- def issuables_assigned_message
- _("Issue(s) already assigned")
- end
-
- def issuables_not_found_message
- _("No matching issue found. Make sure that you are adding a valid issue URL.")
+ def set_link_type(_link)
+ # no-op
end
end
end
diff --git a/app/services/issue_links/create_service.rb b/app/services/issue_links/create_service.rb
index a022d3e0bcf..1c6621ce0a1 100644
--- a/app/services/issue_links/create_service.rb
+++ b/app/services/issue_links/create_service.rb
@@ -2,44 +2,25 @@
module IssueLinks
class CreateService < IssuableLinks::CreateService
- # rubocop: disable CodeReuse/ActiveRecord
- def relate_issuables(referenced_issue)
- link = IssueLink.find_or_initialize_by(source: issuable, target: referenced_issue)
-
- set_link_type(link)
-
- if link.changed? && link.save
- create_notes(referenced_issue)
- end
-
- link
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def linkable_issuables(issues)
@linkable_issuables ||= begin
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
end
end
- def create_notes(referenced_issue)
- SystemNoteService.relate_issue(issuable, referenced_issue, current_user)
- SystemNoteService.relate_issue(referenced_issue, issuable, current_user)
- end
-
def previous_related_issuables
@related_issues ||= issuable.related_issues(current_user).to_a
end
private
- def set_link_type(_link)
- # EE only
- end
-
def track_event
track_incident_action(current_user, issuable, :incident_relate)
end
+
+ def link_class
+ IssueLink
+ end
end
end
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 527ed9d45b7..37c2676e51c 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -11,6 +11,7 @@ module MergeRequests
reset_approvals_cache(merge_request)
create_event(merge_request)
+ stream_audit_event(merge_request)
create_approval_note(merge_request)
mark_pending_todos_as_done(merge_request)
execute_approval_hooks(merge_request, current_user)
@@ -52,6 +53,10 @@ module MergeRequests
def create_event(merge_request)
event_service.approve_mr(merge_request, current_user)
end
+
+ def stream_audit_event(merge_request)
+ # Defined in EE
+ end
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 8d77f03c0d9..9db39a5e174 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -49,8 +49,8 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_contacts(added_count, removed_count)
end
- def relate_issue(noteable, noteable_ref, user)
- ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref)
+ def relate_issuable(noteable, noteable_ref, user)
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issuable(noteable_ref)
end
def unrelate_issuable(noteable, noteable_ref, user)
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index ce592f8d4bb..89212288a6b 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -10,8 +10,9 @@ module SystemNotes
# "marked this issue as related to gitlab-foss#9001"
#
# Returns the created Note object
- def relate_issue(noteable_ref)
- body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
+ def relate_issuable(noteable_ref)
+ issuable_type = noteable.to_ability_name.humanize(capitalize: false)
+ body = "marked this #{issuable_type} as related to #{noteable_ref.to_reference(noteable.resource_parent)}"
issue_activity_counter.track_issue_related_action(author: author) if noteable.is_a?(Issue)
diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml
index 18ec43407c3..762dba69e6a 100644
--- a/app/views/admin/application_settings/ci_cd.html.haml
+++ b/app/views/admin/application_settings/ci_cd.html.haml
@@ -39,7 +39,7 @@
.settings-content
= render 'registry'
-- if Feature.enabled?(:runner_registration_control)
+- if Feature.enabled?(:runner_registration_control, default_enabled: :yaml)
%section.settings.as-runner.no-animate#js-runner-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
diff --git a/app/views/clusters/clusters/_cluster_list.html.haml b/app/views/clusters/clusters/_cluster_list.html.haml
deleted file mode 100644
index 8d92dc30a76..00000000000
--- a/app/views/clusters/clusters/_cluster_list.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- if !clusters.empty?
- .top-area.adjust
- .gl-display-block.gl-text-right.gl-my-4.gl-w-full
- - if clusterable.can_add_cluster?
- = link_to s_('ClusterIntegration|Connect cluster with certificate'), clusterable.new_path, class: 'btn gl-button btn-confirm js-add-cluster gl-py-2', data: { qa_selector: 'integrate_kubernetes_cluster_button' }
- - else
- %span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
- = s_("ClusterIntegration|Connect cluster with certificate")
-
-.js-clusters-main-view{ data: js_clusters_list_data(clusterable) }
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index 457e34b306a..eec44945a77 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -9,4 +9,4 @@
.js-clusters-main-view{ data: js_clusters_data(clusterable) }
- else
- = render 'cluster_list', clusters: @clusters
+ .js-clusters-main-view{ data: js_clusters_list_data(clusterable) }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index ea778517374..e2ac8ef5abc 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -29,8 +29,7 @@
= dropdown_content
= dropdown_loading
.card-footer
- .text-center
- .js-source-loading.mt-1.gl-spinner
+ = gl_loading_icon(css_class: 'js-source-loading gl-my-3')
%ul.list-unstyled.mr_source_commit
.col-lg-6
@@ -58,8 +57,7 @@
= dropdown_content
= dropdown_loading
.card-footer
- .text-center
- .js-target-loading.mt-1.gl-spinner
+ = gl_loading_icon(css_class: 'js-target-loading gl-my-3')
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index 960b1d67610..3a62c6f41cc 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -23,7 +23,7 @@
.row
.form-group.col-md-9
= f.label :description, _('Project description (optional)'), class: 'label-bold'
- = f.text_area :description, class: 'form-control gl-form-input', rows: 3, maxlength: 250
+ = f.text_area :description, class: 'form-control gl-form-input', rows: 3
.row= render_if_exists 'projects/classification_policy_settings', f: f
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 72fa979392e..37d31515307 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -27,7 +27,7 @@
- if issuable_sidebar[:supports_escalation]
.block.escalation-status{ data: { testid: 'escalation_status_container' } }
- #js-escalation-status{ data: { can_edit: issuable_sidebar.dig(:current_user, :can_update_escalation_status).to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
+ #js-escalation-status{ data: { can_update: issuable_sidebar.dig(:current_user, :can_update_escalation_status).to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
= render_if_exists 'shared/issuable/sidebar_escalation_policy', issuable_sidebar: issuable_sidebar
- if @project.group.present?