Commit 4219e0fc authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents b52b4279 f532e9b6
......@@ -83,6 +83,9 @@ const Api = {
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
notificationSettingsPath: '/api/:version/notification_settings',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
......@@ -906,6 +909,34 @@ const Api = {
return { data, headers };
});
},
async updateNotificationSettings(projectId, groupId, data = {}) {
let url = Api.buildUrl(this.notificationSettingsPath);
if (projectId) {
url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
} else if (groupId) {
url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
}
const result = await axios.put(url, data);
return result;
},
async getNotificationSettings(projectId, groupId) {
let url = Api.buildUrl(this.notificationSettingsPath);
if (projectId) {
url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
} else if (groupId) {
url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
}
const result = await axios.get(url);
return result;
},
};
export default Api;
<script>
import {
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
import { sprintf } from '~/locale';
import Api from '~/api';
import NotificationsDropdownItem from './notifications_dropdown_item.vue';
import { CUSTOM_LEVEL, i18n } from '../constants';
export default {
name: 'NotificationsDropdown',
components: {
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownDivider,
NotificationsDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
containerClass: {
default: '',
},
disabled: {
default: false,
},
dropdownItems: {
default: [],
},
buttonSize: {
default: 'medium',
},
initialNotificationLevel: {
default: '',
},
projectId: {
default: null,
},
groupId: {
default: null,
},
},
data() {
return {
selectedNotificationLevel: this.initialNotificationLevel,
isLoading: false,
};
},
computed: {
notificationLevels() {
return this.dropdownItems.map((level) => ({
level,
title: this.$options.i18n.notificationTitles[level] || '',
description: this.$options.i18n.notificationDescriptions[level] || '',
}));
},
isCustomNotification() {
return this.selectedNotificationLevel === CUSTOM_LEVEL;
},
buttonIcon() {
if (this.isLoading) {
return null;
}
return this.selectedNotificationLevel === 'disabled' ? 'notifications-off' : 'notifications';
},
buttonTooltip() {
const notificationTitle =
this.$options.i18n.notificationTitles[this.selectedNotificationLevel] ||
this.selectedNotificationLevel;
return this.disabled
? this.$options.i18n.notificationDescriptions.owner_disabled
: sprintf(this.$options.i18n.notificationTooltipTitle, {
notification_title: notificationTitle,
});
},
},
methods: {
selectItem(level) {
if (level !== this.selectedNotificationLevel) {
this.updateNotificationLevel(level);
}
},
async updateNotificationLevel(level) {
this.isLoading = true;
try {
await Api.updateNotificationSettings(this.projectId, this.groupId, { level });
this.selectedNotificationLevel = level;
} catch (error) {
this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
} finally {
this.isLoading = false;
}
},
},
customLevel: CUSTOM_LEVEL,
i18n,
};
</script>
<template>
<div :class="containerClass">
<gl-button-group
v-if="isCustomNotification"
v-gl-tooltip="{ title: buttonTooltip }"
data-testid="notificationButton"
:size="buttonSize"
>
<gl-button :size="buttonSize" :icon="buttonIcon" :loading="isLoading" :disabled="disabled" />
<gl-dropdown :size="buttonSize" :disabled="disabled">
<notifications-dropdown-item
v-for="item in notificationLevels"
:key="item.level"
:level="item.level"
:title="item.title"
:description="item.description"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
<gl-dropdown-divider />
<notifications-dropdown-item
:key="$options.customLevel"
:level="$options.customLevel"
:title="$options.i18n.notificationTitles.custom"
:description="$options.i18n.notificationDescriptions.custom"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
</gl-dropdown>
</gl-button-group>
<gl-dropdown
v-else
v-gl-tooltip="{ title: buttonTooltip }"
data-testid="notificationButton"
:icon="buttonIcon"
:loading="isLoading"
:size="buttonSize"
:disabled="disabled"
>
<notifications-dropdown-item
v-for="item in notificationLevels"
:key="item.level"
:level="item.level"
:title="item.title"
:description="item.description"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
<gl-dropdown-divider />
<notifications-dropdown-item
:key="$options.customLevel"
:level="$options.customLevel"
:title="$options.i18n.notificationTitles.custom"
:description="$options.i18n.notificationDescriptions.custom"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
</gl-dropdown>
</div>
</template>
<script>
import { GlDropdownItem } from '@gitlab/ui';
export default {
name: 'NotificationsDropdownItem',
components: {
GlDropdownItem,
},
props: {
level: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
notificationLevel: {
type: String,
required: true,
},
},
computed: {
isActive() {
return this.notificationLevel === this.level;
},
},
};
</script>
<template>
<gl-dropdown-item is-check-item :is-checked="isActive" @click="$emit('item-selected', level)">
<div class="gl-display-flex gl-flex-direction-column">
<span class="gl-font-weight-bold">{{ title }}</span>
<span class="gl-text-gray-500">{{ description }}</span>
</div>
</gl-dropdown-item>
</template>
import { __, s__ } from '~/locale';
export const CUSTOM_LEVEL = 'custom';
export const i18n = {
notificationTitles: {
participating: s__('NotificationLevel|Participate'),
mention: s__('NotificationLevel|On mention'),
watch: s__('NotificationLevel|Watch'),
global: s__('NotificationLevel|Global'),
disabled: s__('NotificationLevel|Disabled'),
custom: s__('NotificationLevel|Custom'),
},
notificationTooltipTitle: __('Notification setting - %{notification_title}'),
notificationDescriptions: {
participating: __('You will only receive notifications for threads you have participated in'),
mention: __('You will receive notifications only for comments in which you were @mentioned'),
watch: __('You will receive notifications for any activity'),
disabled: __('You will not get any notifications via email'),
global: __('Use your global notification setting'),
custom: __('You will only receive notifications for the events you choose'),
owner_disabled: __('Notifications have been disabled by the project or group owner'),
},
updateNotificationLevelErrorMessage: __(
'An error occured while updating the notification settings. Please try again.',
),
};
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import NotificationsDropdown from './components/notifications_dropdown.vue';
Vue.use(GlToast);
export default () => {
const el = document.querySelector('.js-vue-notification-dropdown');
if (!el) return false;
const {
containerClass,
buttonSize,
disabled,
dropdownItems,
notificationLevel,
projectId,
groupId,
} = el.dataset;
return new Vue({
el,
provide: {
containerClass,
buttonSize,
disabled: parseBoolean(disabled),
dropdownItems: JSON.parse(dropdownItems),
initialNotificationLevel: notificationLevel,
projectId,
groupId,
},
render(h) {
return h(NotificationsDropdown);
},
});
};
......@@ -12,6 +12,7 @@ import notificationsDropdown from '../../../notifications_dropdown';
import { showLearnGitLabProjectPopover } from '~/onboarding_issues';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initVueNotificationsDropdown from '~/notifications';
initReadMore();
new Star(); // eslint-disable-line no-new
......@@ -42,7 +43,14 @@ leaveByUrl('project');
showLearnGitLabProjectPopover();
notificationsDropdown();
if (gon.features?.vueNotificationDropdown) {
initVueNotificationsDropdown();
} else {
notificationsDropdown();
}
initVueNotificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
initInviteMembersTrigger();
......
......@@ -31,6 +31,10 @@ class ProjectsController < Projects::ApplicationController
# Project Export Rate Limit
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
before_action do
push_frontend_feature_flag(:vue_notification_dropdown, @project, default_enabled: :yaml)
end
before_action only: [:edit] do
push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:allow_editing_commit_messages, @project)
......
......@@ -125,4 +125,13 @@ module NotificationsHelper
def can_read_project?(project)
can?(current_user, :read_project, project)
end
def notification_dropdown_items(notification_setting)
NotificationSetting.levels.each_key.map do |level|
next if level == "custom"
next if level == "global" && notification_setting.source.nil?
level
end.compact
end
end
......@@ -46,7 +46,11 @@
.project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.d-inline-flex
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', dropdown_container_class: 'gl-mr-3', emails_disabled: emails_disabled
- if Feature.enabled?(:vue_notification_dropdown, @project, default_enabled: :yaml)
- if @notification_setting
.js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, container_class: 'gl-mr-3 gl-mt-5 gl-vertical-align-top' } }
- else
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', dropdown_container_class: 'gl-mr-3', emails_disabled: emails_disabled
.count-buttons.d-inline-flex
= render 'projects/buttons/star'
......
---
name: vue_notification_dropdown
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52068
rollout_issue_url:
milestone: '13.8'
type: development
group: group::optimize
default_enabled: false
......@@ -60,6 +60,10 @@ boolean
booleans
Bootsnap
browsable
bugfix
bugfixed
bugfixes
bugfixing
Bugzilla
Buildkite
buildpack
......@@ -668,6 +672,7 @@ unverify
unverifying
uploader
uploaders
upstreams
upvoted
upvotes
URIs
......
......@@ -15,9 +15,9 @@ relevant compliance standards.
|Feature |GitLab tier |GitLab.com | Product level |
| ---------| :--------: | :-------: | :-----------: |
|**[Restrict SSH Keys](../security/ssh_keys_restrictions.md)**<br>Control the technology and key length of SSH keys used to access GitLab|Core+||Instance|
|**[Granular user roles and flexible permissions](../user/permissions.md)**<br>Manage access and permissions with five different user roles and settings for external users. Set permissions according to people's role, rather than either read or write access to a repository. Don't share the source code with people that only need access to the issue tracker.|Core+|✓|Instance, Group, Project|
|**[Enforce TOS acceptance](../user/admin_area/settings/terms.md)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Core+||Instance|
|**[Restrict SSH Keys](../security/ssh_keys_restrictions.md)**<br>Control the technology and key length of SSH keys used to access GitLab|Free+||Instance|
|**[Granular user roles and flexible permissions](../user/permissions.md)**<br>Manage access and permissions with five different user roles and settings for external users. Set permissions according to people's role, rather than either read or write access to a repository. Don't share the source code with people that only need access to the issue tracker.|Free+|✓|Instance, Group, Project|
|**[Enforce TOS acceptance](../user/admin_area/settings/terms.md)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Free+||Instance|
|**[Email all users of a project, group, or entire server](../tools/email.md)**<br>An admin can email groups of users based on project or group membership, or email everyone using the GitLab instance. This is great for scheduled maintenance or upgrades.|Starter+|||Instance
|**[Omnibus package supports log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-forwarding)**<br>Forward your logs to a central system.|Starter+||Instance|
|**[Lock project membership to group](../user/group/index.md#member-lock)**<br>Group owners can prevent new members from being added to projects within a group.|Starter+|✓|Group|
......
......@@ -17,7 +17,7 @@ GitLab has two product distributions available through [different subscriptions]
You can [install either GitLab CE or GitLab EE](https://about.gitlab.com/install/ce-or-ee/).
However, the features you have access to depend on your chosen [subscription](https://about.gitlab.com/pricing/).
GitLab Community Edition installations have access only to Core features.
GitLab Community Edition installations have access only to Free features.
Non-administrator users can't access GitLab administration tools and settings.
......
......@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Instance Review
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Core](https://about.gitlab.com/pricing/) 11.3.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Free](https://about.gitlab.com/pricing/) 11.3.
If you run a medium-sized self-managed instance (50+ users) of a free version of GitLab,
[either Community Edition or unlicensed Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/),
......
......@@ -91,7 +91,7 @@ _The artifacts are stored by default in
> [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
> - Since version 9.5, artifacts are [browsable](../ci/pipelines/job_artifacts.md#browsing-artifacts),
> when object storage is enabled. 9.4 lacks this feature.
> - Since version 10.6, available in [GitLab Core](https://about.gitlab.com/pricing/)
> - Since version 10.6, available in [GitLab Free](https://about.gitlab.com/pricing/).
> - Since version 11.0, we support `direct_upload` to S3.
If you don't want to use the local disk where GitLab is installed to store the
......
......@@ -27,7 +27,7 @@ can be started.
## Start multiple processes
> - [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4006) in GitLab 12.10, starting multiple processes with Sidekiq cluster.
> - [Sidekiq cluster moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10.
> - [Sidekiq cluster moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Free](https://about.gitlab.com/pricing/) in GitLab 12.10.
> - [Sidekiq cluster became default](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4140) in GitLab 13.0.
To start multiple processes:
......@@ -113,7 +113,7 @@ you list:
## Queue selector
> - [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10.
> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Free](https://about.gitlab.com/pricing/) in GitLab 12.10.
> - [Renamed from `experimental_queue_selector` to `queue_selector`](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/147) in GitLab 13.6.
In addition to selecting queues by name, as above, the `queue_selector`
......
......@@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# GitLab Dependency Proxy administration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Free](https://about.gitlab.com/pricing/) in GitLab 13.6.
GitLab can be used as a dependency proxy for a variety of common package managers.
......
......@@ -161,7 +161,8 @@ When avatar is replaced, `Upload` model is destroyed and a new one takes place w
#### CI artifacts
CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Core since **10.6**.
CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Free since
**10.6**.
#### LFS objects
......
......@@ -54,7 +54,7 @@ _The uploads are stored by default in
> **Notes:**
>
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3867) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Core](https://about.gitlab.com/pricing/) 10.7.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Free](https://about.gitlab.com/pricing/) 10.7.
> - Since version 11.1, we support direct_upload to S3.
If you don't want to use the local disk where GitLab is installed to store the
......
This diff is collapsed.
......@@ -94,7 +94,7 @@ with the **Add Panel** page:
## Duplicate a GitLab-defined dashboard
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/37238) in GitLab 12.7.
> - From [GitLab 12.8 onwards](https://gitlab.com/gitlab-org/gitlab/-/issues/39505), custom metrics are also duplicated when you duplicate a dashboard.
> - [GitLab versions 12.8 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/39505), custom metrics are also duplicated when you duplicate a dashboard.
You can save a complete copy of a GitLab-defined dashboard along with all custom metrics added to it.
The resulting `.yml` file can be customized and adapted to your project.
......@@ -128,7 +128,7 @@ any chart on a dashboard:
The options are:
- **Expand panel** - Displays a larger version of a visualization. To return to
the dashboard, click the **Back** button in your browser, or press the <kbd>Esc</kbd> key.
the dashboard, click the **Back** button in your browser, or press the <kbd>Escape</kbd> key.
([Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3100) in GitLab 13.0.)
- **View logs** **(ULTIMATE)** - Displays [Logs](../../../user/project/clusters/kubernetes_pod_logs.md),
if they are enabled. If used in conjunction with the [timeline zoom](#timeline-zoom-and-url-sharing)
......@@ -147,7 +147,7 @@ The options are:
You can use the **Timeline zoom** function at the bottom of a chart to zoom in
on a date and time of your choice. When you click and drag the sliders to select
a different beginning or end date of data to display, GitLab adds your selected start
and end times to the URL, enabling you to share specific timeframes more easily.
and end times to the URL, enabling you to share specific time frames more easily.
## Dashboard Annotations
......
......@@ -40,7 +40,7 @@ To unlock a locked user:
user.unlock_access!
```
1. Exit the console with <kbd>Ctrl</kbd>+<kbd>d</kbd>
1. Exit the console with <kbd>Control</kbd>+<kbd>d</kbd>
The user should now be able to log in.
......
......@@ -28,9 +28,13 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.1. Version Control and Git
<!-- vale gitlab.Spelling = NO -->
1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
1. [Katacoda: Learn Git Version Control using Interactive Browser-Based Scenarios](https://www.katacoda.com/courses/git)
<!-- vale gitlab.Spelling = YES -->
### 1.2. GitLab Basics
1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4)
......@@ -55,11 +59,14 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.5. Migrating from other Source Control
<!-- vale gitlab.Spelling = NO -->
1. [Migrating from Bitbucket/Stash](../user/project/import/bitbucket.md)
1. [Migrating from GitHub](../user/project/import/github.md)
1. [Migrating from SVN](../user/project/import/svn.md)
1. [Migrating from Fogbugz](../user/project/import/fogbugz.md)
<!-- vale gitlab.Spelling = YES -->
### 1.6. The GitLab team
1. [About GitLab](https://about.gitlab.com/company/)
......@@ -185,6 +192,8 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 3.9. Integrations
<!-- vale gitlab.Spelling = NO -->
1. [How to Integrate Jira and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
1. [How to Integrate Jira with GitLab](../user/project/integrations/jira.md)
1. [How to Integrate Jenkins with GitLab](../integration/jenkins.md)
......@@ -193,9 +202,11 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [How to Integrate Convox with GitLab](https://about.gitlab.com/blog/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/blog/2016/05/05/getting-started-gitlab-and-shippable/)
<!-- vale gitlab.Spelling = YES -->
## 4. External Articles
1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2011 Wall Street Journal article - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2014 Blog post by Chris Dixon - Software eats software development](https://cdixon.org/2014/04/13/software-eats-software-development/)
1. [2015 Venture Beat article - Actually, Open Source is Eating the World](https://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
......
......@@ -130,6 +130,10 @@ module Types
object.finding&.identifiers
end
def description
object.description || object.finding_description
end
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
......
---
title: Properly populate description in an issue created from a vulnerability
merge_request: 52619
author:
type: fixed
......@@ -29,7 +29,10 @@ RSpec.describe 'Kerberos clone instructions', :js do
it 'shows the Kerberos clone information' do
resize_screen_xs
visit_project
find('.dropdown-toggle').click
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy KRB5 clone URL')
end
......
......@@ -106,4 +106,44 @@ RSpec.describe GitlabSchema.types['Vulnerability'] do
end
end
end
describe '#description' do
let_it_be(:vulnerability_with_finding) { create(:vulnerability, :with_findings, project: project) }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
name
vulnerabilities {
nodes {
description
}
}
}
}
)
end
context 'when the vulnerability description field is populated' do
it 'returns the description for the vulnerability' do
vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')
expect(vulnerabilities.first['description']).to eq(vulnerability_with_finding.description)
end
end
context 'when the vulnerability description field is empty' do
before do
vulnerability_with_finding.description = nil
vulnerability_with_finding.save!
end
it 'returns the description for the vulnerability finding' do
vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')
expect(vulnerabilities.first['description']).to eq(vulnerability_with_finding.finding.description)
end
end
end
end
......@@ -3062,6 +3062,9 @@ msgstr ""
msgid "An error occured while saving changes: %{error}"
msgstr ""
msgid "An error occured while updating the notification settings. Please try again."
msgstr ""
msgid "An error occurred adding a draft to the thread."
msgstr ""
......
......@@ -37,7 +37,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows only the SSH clone information' do
resize_screen_xs
visit_project
find('.dropdown-toggle').click
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy SSH clone URL')
expect(page).not_to have_content('Copy HTTP clone URL')
......@@ -66,7 +69,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows only the HTTP clone information' do
resize_screen_xs
visit_project
find('.dropdown-toggle').click
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy HTTP clone URL')
expect(page).not_to have_content('Copy SSH clone URL')
......@@ -97,7 +103,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows both SSH and HTTP clone information' do
resize_screen_xs
visit_project
find('.dropdown-toggle').click
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy HTTP clone URL')
expect(page).to have_content('Copy SSH clone URL')
......
......@@ -6,6 +6,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
let(:project) { create(:project, :public, :repository) }
before do
stub_feature_flags(vue_notification_dropdown: false)
sign_in(project.owner)
end
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import { GlButtonGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import httpStatus from '~/lib/utils/http_status';
import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
import NotificationsDropdownItem from '~/notifications/components/notifications_dropdown_item.vue';
const mockDropdownItems = ['global', 'watch', 'participating', 'mention', 'disabled'];
const mockToastShow = jest.fn();
describe('NotificationsDropdown', () => {
let wrapper;
let mockAxios;
function createComponent(injectedProperties = {}) {
return shallowMount(NotificationsDropdown, {
stubs: {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
NotificationsDropdownItem,
},
directives: {
GlTooltip: createMockDirective(),
},
provide: {
...injectedProperties,
},
mocks: {
$toast: {
show: mockToastShow,
},
},
});
}
const findButtonGroup = () => wrapper.find(GlButtonGroup);
const findDropdown = () => wrapper.find(GlDropdown);
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
const findDropdownItemAt = (index) =>
findAllNotificationsDropdownItems().at(index).find(GlDropdownItem);
const clickDropdownItemAt = async (index) => {
const dropdownItem = findDropdownItemAt(index);
dropdownItem.vm.$emit('click');
await waitForPromises();
};
beforeEach(() => {
gon.api_version = 'v4';
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mockAxios.restore();
});
describe('template', () => {
describe('when notification level is "custom"', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'custom',
});
});
it('renders a button group', () => {
expect(findButtonGroup().exists()).toBe(true);
});
});
describe('when notification level is not "custom"', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
});
it('does not render a button group', () => {
expect(findButtonGroup().exists()).toBe(false);
});
});
describe('button tooltip', () => {
const tooltipTitlePrefix = 'Notification setting';
it.each`
level | title
${'global'} | ${'Global'}
${'watch'} | ${'Watch'}
${'participating'} | ${'Participate'}
${'mention'} | ${'On mention'}
${'disabled'} | ${'Disabled'}
${'custom'} | ${'Custom'}
`(`renders "${tooltipTitlePrefix} - $title" for "$level" level`, ({ level, title }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: level,
});
const tooltipElement = findByTestId('notificationButton');
const tooltip = getBinding(tooltipElement.element, 'gl-tooltip');
expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`);
});
});
describe('button icon', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'disabled',
});
});
it('renders the "notifications-off" icon when notification level is "disabled"', () => {
expect(findDropdown().props('icon')).toBe('notifications-off');
});
it('renders the "notifications" icon when notification level is not "disabled"', () => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
expect(findDropdown().props('icon')).toBe('notifications');
});
});
describe('dropdown items', () => {
it.each`
dropdownIndex | level | title | description
${0} | ${'global'} | ${'Global'} | ${'Use your global notification setting'}
${1} | ${'watch'} | ${'Watch'} | ${'You will receive notifications for any activity'}
${2} | ${'participating'} | ${'Participate'} | ${'You will only receive notifications for threads you have participated in'}
${3} | ${'mention'} | ${'On mention'} | ${'You will receive notifications only for comments in which you were @mentioned'}
${4} | ${'disabled'} | ${'Disabled'} | ${'You will not get any notifications via email'}
${5} | ${'custom'} | ${'Custom'} | ${'You will only receive notifications for the events you choose'}
`('displays "$title" and "$description"', ({ dropdownIndex, title, description }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('title')).toBe(title);
expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('description')).toBe(
description,
);
});
});
});
describe('when selecting an item', () => {
beforeEach(() => {
jest.spyOn(axios, 'put');
});
it.each`
projectId | groupId | endpointUrl | endpointType | condition
${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project notifications'} | ${'a projectId is given'}
${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group notifications'} | ${'a groupId is given'}
${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global notifications'} | ${'when neither projectId nor groupId are given'}
`(
'calls the $endpointType endpoint when $condition',
async ({ projectId, groupId, endpointUrl }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
projectId,
groupId,
});
await clickDropdownItemAt(1);
expect(axios.put).toHaveBeenCalledWith(endpointUrl, {
level: 'watch',
});
},
);
it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
const dropdownItem = findDropdownItemAt(1);
await clickDropdownItemAt(1);
expect(wrapper.vm.selectedNotificationLevel).toBe('watch');
expect(dropdownItem.props('isChecked')).toBe(true);
});
it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
await clickDropdownItemAt(1);
expect(wrapper.vm.selectedNotificationLevel).toBe('global');
expect(
mockToastShow,
).toHaveBeenCalledWith(
'An error occured while updating the notification settings. Please try again.',
{ type: 'error' },
);
});
});
});
......@@ -9,6 +9,7 @@ RSpec.describe 'projects/_home_panel' do
let(:project) { create(:project) }
before do
stub_feature_flags(vue_notification_dropdown: false)
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
......
......@@ -3334,10 +3334,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@^3.1.3, core-js@^3.6.4:
version "3.6.4"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647"
integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==
core-js@^3.1.3, core-js@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0"
integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
core-js@~2.3.0:
version "2.3.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment