Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
276941b2c4
commit
3feda79a55
31 changed files with 191 additions and 167 deletions
|
@ -4,20 +4,28 @@ import {
|
|||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
GlAvatar,
|
||||
GlAlert,
|
||||
GlLoadingIcon,
|
||||
GlSafeHtmlDirective as SafeHtml,
|
||||
} from '@gitlab/ui';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
import highlight from '~/lib/utils/highlight';
|
||||
import { GROUPS_CATEGORY, PROJECTS_CATEGORY, LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'HeaderSearchAutocompleteItems',
|
||||
i18n: {
|
||||
autocompleteErrorMessage: s__(
|
||||
'GlobalSearch|There was an error fetching search autocomplete suggestions.',
|
||||
),
|
||||
},
|
||||
components: {
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
GlAvatar,
|
||||
GlAlert,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
|
@ -31,7 +39,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['search', 'loading']),
|
||||
...mapState(['search', 'loading', 'autocompleteError']),
|
||||
...mapGetters(['autocompleteGroupedSearchOptions']),
|
||||
},
|
||||
watch: {
|
||||
|
@ -93,5 +101,13 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
<gl-loading-icon v-else size="lg" class="my-4" />
|
||||
<gl-alert
|
||||
v-if="autocompleteError"
|
||||
class="gl-text-body gl-mt-2"
|
||||
:dismissible="false"
|
||||
variant="danger"
|
||||
>
|
||||
{{ $options.i18n.autocompleteErrorMessage }}
|
||||
</gl-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const fetchAutocompleteOptions = ({ commit, getters }) => {
|
||||
|
@ -10,7 +8,6 @@ export const fetchAutocompleteOptions = ({ commit, getters }) => {
|
|||
.then(({ data }) => commit(types.RECEIVE_AUTOCOMPLETE_SUCCESS, data))
|
||||
.catch(() => {
|
||||
commit(types.RECEIVE_AUTOCOMPLETE_ERROR);
|
||||
createFlash({ message: __('There was an error fetching search autocomplete suggestions') });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -4,19 +4,23 @@ export default {
|
|||
[types.REQUEST_AUTOCOMPLETE](state) {
|
||||
state.loading = true;
|
||||
state.autocompleteOptions = [];
|
||||
state.autocompleteError = false;
|
||||
},
|
||||
[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) {
|
||||
state.loading = false;
|
||||
state.autocompleteOptions = data.map((d, i) => {
|
||||
return { html_id: `autocomplete-${d.category}-${i}`, ...d };
|
||||
});
|
||||
state.autocompleteError = false;
|
||||
},
|
||||
[types.RECEIVE_AUTOCOMPLETE_ERROR](state) {
|
||||
state.loading = false;
|
||||
state.autocompleteOptions = [];
|
||||
state.autocompleteError = true;
|
||||
},
|
||||
[types.CLEAR_AUTOCOMPLETE](state) {
|
||||
state.autocompleteOptions = [];
|
||||
state.autocompleteError = false;
|
||||
},
|
||||
[types.SET_SEARCH](state, value) {
|
||||
state.search = value;
|
||||
|
|
|
@ -6,6 +6,7 @@ const createState = ({ searchPath, issuesPath, mrPath, autocompletePath, searchC
|
|||
searchContext,
|
||||
search: '',
|
||||
autocompleteOptions: [],
|
||||
autocompleteError: false,
|
||||
loading: false,
|
||||
});
|
||||
export default createState;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false) {
|
||||
group(fullPath: $fullPath) @skip(if: $isProject) {
|
||||
id
|
||||
groupMembers(search: $search) {
|
||||
groupMembers(search: $search, relations: [DIRECT, INHERITED, SHARED_FROM_GROUPS]) {
|
||||
nodes {
|
||||
id
|
||||
user {
|
||||
|
@ -14,7 +14,7 @@ query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false)
|
|||
}
|
||||
project(fullPath: $fullPath) @include(if: $isProject) {
|
||||
id
|
||||
projectMembers(search: $search) {
|
||||
projectMembers(search: $search, relations: [DIRECT, INHERITED, INVITED_GROUPS]) {
|
||||
nodes {
|
||||
id
|
||||
user {
|
||||
|
|
|
@ -63,7 +63,7 @@ export default {
|
|||
computed: {
|
||||
shouldEnableRealtime() {
|
||||
// Note: Realtime is only available on issues right now, future support for MR wil be built later.
|
||||
return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue';
|
||||
return this.issuableType === 'issue';
|
||||
},
|
||||
queryVariables() {
|
||||
return {
|
||||
|
|
|
@ -112,7 +112,7 @@ export default {
|
|||
computed: {
|
||||
shouldEnableRealtime() {
|
||||
// Note: Realtime is only available on issues right now, future support for MR wil be built later.
|
||||
return this.glFeatures.realTimeIssueSidebar && this.issuableType === IssuableType.Issue;
|
||||
return this.issuableType === IssuableType.Issue;
|
||||
},
|
||||
queryVariables() {
|
||||
return {
|
||||
|
|
|
@ -1810,6 +1810,9 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav {
|
|||
body.gl-dark .navbar-gitlab .nav > li {
|
||||
color: #fafafa;
|
||||
}
|
||||
body.gl-dark .navbar-gitlab .nav > li.header-search-new {
|
||||
color: #fafafa;
|
||||
}
|
||||
body.gl-dark .navbar-gitlab .nav > li > a .notification-dot {
|
||||
border: 2px solid #fafafa;
|
||||
}
|
||||
|
@ -1847,8 +1850,8 @@ body.gl-dark
|
|||
body.gl-dark .header-search {
|
||||
background-color: rgba(250, 250, 250, 0.2) !important;
|
||||
}
|
||||
body.gl-dark .header-search svg {
|
||||
color: rgba(250, 250, 250, 0.8) !important;
|
||||
body.gl-dark .header-search svg.gl-search-box-by-type-search-icon {
|
||||
color: rgba(250, 250, 250, 0.8);
|
||||
}
|
||||
body.gl-dark .header-search input {
|
||||
background-color: transparent;
|
||||
|
|
|
@ -64,6 +64,10 @@
|
|||
> li {
|
||||
color: $search-and-nav-links;
|
||||
|
||||
&.header-search-new {
|
||||
color: $sidebar-text;
|
||||
}
|
||||
|
||||
> a {
|
||||
.notification-dot {
|
||||
border: 2px solid $nav-svg-color;
|
||||
|
@ -151,10 +155,11 @@
|
|||
background-color: rgba($search-and-nav-links, 0.3) !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: rgba($search-and-nav-links, 0.8) !important;
|
||||
svg.gl-search-box-by-type-search-icon {
|
||||
color: rgba($search-and-nav-links, 0.8);
|
||||
}
|
||||
|
||||
|
||||
input {
|
||||
background-color: transparent;
|
||||
color: rgba($search-and-nav-links, 0.8);
|
||||
|
|
|
@ -4,7 +4,7 @@ module FlocOptOut
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_action :set_floc_opt_out_header, unless: :floc_enabled?
|
||||
before_action :set_floc_opt_out_header, unless: :floc_enabled?
|
||||
end
|
||||
|
||||
def floc_enabled?
|
||||
|
|
|
@ -45,7 +45,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
before_action only: :show do
|
||||
push_frontend_feature_flag(:real_time_issue_sidebar, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:confidential_notes, project&.group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:issue_assignees_widget, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:paginated_issue_discussions, project, default_enabled: :yaml)
|
||||
|
|
|
@ -79,9 +79,7 @@ module Issues
|
|||
todo_service.reassigned_assignable(issue, current_user, old_assignees)
|
||||
track_incident_action(current_user, issue, :incident_assigned)
|
||||
|
||||
if Feature.enabled?(:broadcast_issue_updates, issue.project, default_enabled: :yaml)
|
||||
GraphqlTriggers.issuable_assignees_updated(issue)
|
||||
end
|
||||
GraphqlTriggers.issuable_assignees_updated(issue)
|
||||
end
|
||||
|
||||
def handle_task_changes(issuable)
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
= render 'layouts/header/new_dropdown', class: 'gl-display-none gl-sm-display-block'
|
||||
- if top_nav_show_search
|
||||
- search_menu_item = top_nav_search_menu_item_attrs
|
||||
%li.nav-item.d-none.d-lg-block.m-auto
|
||||
%li.nav-item.header-search-new.d-none.d-lg-block.m-auto
|
||||
- unless current_controller?(:search)
|
||||
- if Feature.enabled?(:new_header_search)
|
||||
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
.form-group.project-name.col-sm-12
|
||||
= f.label :name, class: 'label-bold' do
|
||||
%span= _("Project name")
|
||||
= f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", data: { track_label: "#{track_label}", track_action: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
|
||||
= f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", data: { qa_selector: 'project_name', track_label: "#{track_label}", track_action: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
|
||||
.form-group.project-path.col-sm-6
|
||||
= f.label :namespace_id, class: 'label-bold' do
|
||||
%span= _('Project URL')
|
||||
|
@ -29,7 +29,7 @@
|
|||
.form-group.project-path.col-sm-6
|
||||
= f.label :path, class: 'label-bold' do
|
||||
%span= _("Project slug")
|
||||
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }, data: { username: current_user.username }
|
||||
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }, data: { qa_selector: 'project_path', username: current_user.username }
|
||||
- if current_user.can_create_group?
|
||||
.form-text.text-muted
|
||||
- link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
|
||||
|
@ -44,14 +44,14 @@
|
|||
.form-group
|
||||
= f.label :description, class: 'label-bold' do
|
||||
= s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe }
|
||||
= f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control gl-form-input", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_action: "activate_form_input", track_property: "project_description", track_value: "" }
|
||||
= f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control gl-form-input", rows: 3, maxlength: 250, data: { qa_selector: 'project_description', track_label: "#{track_label}", track_action: "activate_form_input", track_property: "project_description", track_value: "" }
|
||||
|
||||
.js-deployment-target-select
|
||||
|
||||
= f.label :visibility_level, class: 'label-bold' do
|
||||
= s_('ProjectsNew|Visibility Level')
|
||||
= link_to sprite_icon('question-o'), help_page_path('public_access/public_access'), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
|
||||
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
|
||||
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false, data: { qa_selector: 'visibility_radios'}
|
||||
|
||||
- if !hide_init_with_readme
|
||||
= f.label :project_configuration, class: 'label-bold' do
|
||||
|
@ -74,5 +74,5 @@
|
|||
- e.variant(:unchecked_free_indicator) do
|
||||
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: false, with_free_badge: true
|
||||
|
||||
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
|
||||
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { qa_selector: 'project_create_button', track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
|
||||
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" }
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: broadcast_issue_updates
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30732
|
||||
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/3413
|
||||
milestone: '13.0'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: true
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: real_time_issue_sidebar
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30239
|
||||
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1210
|
||||
milestone: '13.0'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: true
|
|
@ -578,13 +578,11 @@ To copy the issue's email address:
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17589) in GitLab 13.3. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/3413) in GitLab 13.9.
|
||||
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/17589) in GitLab 14.5.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature per project or for your entire instance, ask an administrator to
|
||||
[disable the feature flags](../../../administration/feature_flags.md) named `real_time_issue_sidebar` and `broadcast_issue_updates`.
|
||||
On GitLab.com, this feature is available.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/17589) in GitLab 14.9. Feature flags `real_time_issue_sidebar` and `broadcast_issue_updates` removed.
|
||||
|
||||
Assignees in the sidebar are updated in real time.
|
||||
When you're viewing an issue and somebody changes its assignee,
|
||||
you can see the change without having to refresh the page.
|
||||
|
||||
## Assignee
|
||||
|
||||
|
|
|
@ -702,6 +702,8 @@ module API
|
|||
|
||||
if user.ldap_blocked?
|
||||
forbidden!('LDAP blocked users cannot be modified by the API')
|
||||
elsif current_user == user
|
||||
forbidden!('The API initiating user cannot be blocked by the API')
|
||||
end
|
||||
|
||||
break if user.blocked?
|
||||
|
|
|
@ -16837,6 +16837,9 @@ msgstr ""
|
|||
msgid "GlobalSearch|Search results are loading"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|There was an error fetching search autocomplete suggestions."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Type and press the enter key to submit search."
|
||||
msgstr ""
|
||||
|
||||
|
@ -37200,9 +37203,6 @@ msgstr ""
|
|||
msgid "There was an error fetching projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error fetching search autocomplete suggestions"
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error fetching stage total counts"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -13,11 +13,11 @@ module QA
|
|||
|
||||
view 'app/views/projects/_new_project_fields.html.haml' do
|
||||
element :initialize_with_readme_checkbox
|
||||
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
|
||||
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
|
||||
element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern
|
||||
element :project_create_button, "submit _('Create project')" # rubocop:disable QA/ElementWithPattern
|
||||
element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
|
||||
element :project_name
|
||||
element :project_path
|
||||
element :project_description
|
||||
element :project_create_button
|
||||
element :visibility_radios
|
||||
end
|
||||
|
||||
view 'app/views/projects/_new_project_initialize_with_sast.html.haml' do
|
||||
|
|
|
@ -12,20 +12,12 @@ module QA
|
|||
end
|
||||
|
||||
before do
|
||||
Runtime::Feature.enable('real_time_issue_sidebar', project: project)
|
||||
Runtime::Feature.enable('broadcast_issue_updates', project: project)
|
||||
|
||||
Flow::Login.sign_in
|
||||
|
||||
project.add_member(user1)
|
||||
project.add_member(user2)
|
||||
end
|
||||
|
||||
after do
|
||||
Runtime::Feature.disable('real_time_issue_sidebar', project: project)
|
||||
Runtime::Feature.disable('broadcast_issue_updates', project: project)
|
||||
end
|
||||
|
||||
it 'update without refresh', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347941' do
|
||||
issue = Resource::Issue.fabricate_via_api! do |issue|
|
||||
issue.project = project
|
||||
|
|
|
@ -1059,15 +1059,25 @@ RSpec.describe ApplicationController do
|
|||
describe 'setting permissions-policy header' do
|
||||
controller do
|
||||
skip_before_action :authenticate_user!
|
||||
before_action :redirect_to_example, only: [:redirect]
|
||||
|
||||
def index
|
||||
render html: 'It is a flock of sheep, not a floc of sheep.'
|
||||
end
|
||||
|
||||
def redirect
|
||||
raise 'Should not be reached'
|
||||
end
|
||||
|
||||
def redirect_to_example
|
||||
redirect_to('https://example.com')
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
routes.draw do
|
||||
get 'index' => 'anonymous#index'
|
||||
get 'redirect' => 'anonymous#redirect'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1093,6 +1103,13 @@ RSpec.describe ApplicationController do
|
|||
|
||||
expect(response.headers['Permissions-Policy']).to eq('interest-cohort=()')
|
||||
end
|
||||
|
||||
it 'sets the Permissions-Policy header even when redirected before_action' do
|
||||
get :redirect
|
||||
|
||||
expect(response).to have_gitlab_http_status(:redirect)
|
||||
expect(response.headers['Permissions-Policy']).to eq('interest-cohort=()')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1065,35 +1065,6 @@ RSpec.describe Projects::IssuesController do
|
|||
.not_to exceed_query_limit(control_count + 2 * labels.count)
|
||||
end
|
||||
|
||||
context 'real-time sidebar feature flag' do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
|
||||
context 'when enabled' do
|
||||
before do
|
||||
stub_feature_flags(real_time_issue_sidebar: true)
|
||||
end
|
||||
|
||||
it 'pushes the correct value to the frontend' do
|
||||
go(id: issue.to_param)
|
||||
|
||||
expect(Gon.features).to include('realTimeIssueSidebar' => true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disabled' do
|
||||
before do
|
||||
stub_feature_flags(real_time_issue_sidebar: false)
|
||||
end
|
||||
|
||||
it 'pushes the correct value to the frontend' do
|
||||
go(id: issue.to_param)
|
||||
|
||||
expect(Gon.features).to include('realTimeIssueSidebar' => false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'logs the view with Gitlab::Search::RecentIssues' do
|
||||
sign_in(user)
|
||||
recent_issues_double = instance_double(::Gitlab::Search::RecentIssues, log_view: nil)
|
||||
|
|
|
@ -12,14 +12,14 @@ RSpec.describe 'Dropdown assignee', :js do
|
|||
let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
|
||||
let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit project_issues_path(project)
|
||||
end
|
||||
|
||||
describe 'behavior' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit project_issues_path(project)
|
||||
end
|
||||
|
||||
it 'loads all the assignees when opened' do
|
||||
input_filtered_search('assignee:=', submit: false, extra_space: false)
|
||||
|
||||
|
@ -35,6 +35,11 @@ RSpec.describe 'Dropdown assignee', :js do
|
|||
|
||||
describe 'selecting from dropdown without Ajax call' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit project_issues_path(project)
|
||||
|
||||
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
|
||||
input_filtered_search('assignee:=', submit: false, extra_space: false)
|
||||
end
|
||||
|
@ -51,4 +56,60 @@ RSpec.describe 'Dropdown assignee', :js do
|
|||
expect_filtered_search_input_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'assignee suggestions' do
|
||||
let!(:group) { create(:group) }
|
||||
let!(:group_project) { create(:project, namespace: group) }
|
||||
let!(:group_user) { create(:user) }
|
||||
|
||||
let!(:subgroup) { create(:group, parent: group) }
|
||||
let!(:subgroup_project) { create(:project, namespace: subgroup) }
|
||||
let!(:subgroup_project_issue) { create(:issue, project: subgroup_project) }
|
||||
let!(:subgroup_user) { create(:user) }
|
||||
|
||||
let!(:subsubgroup) { create(:group, parent: subgroup) }
|
||||
let!(:subsubgroup_project) { create(:project, namespace: subsubgroup) }
|
||||
let!(:subsubgroup_user) { create(:user) }
|
||||
|
||||
let!(:invited_to_group_group) { create(:group) }
|
||||
let!(:invited_to_group_group_user) { create(:user) }
|
||||
|
||||
let!(:invited_to_project_group) { create(:group) }
|
||||
let!(:invited_to_project_group_user) { create(:user) }
|
||||
|
||||
before do
|
||||
group.add_developer(group_user)
|
||||
subgroup.add_developer(subgroup_user)
|
||||
subsubgroup.add_developer(subsubgroup_user)
|
||||
invited_to_group_group.add_developer(invited_to_group_group_user)
|
||||
invited_to_project_group.add_developer(invited_to_project_group_user)
|
||||
|
||||
create(:group_group_link, shared_group: subgroup, shared_with_group: invited_to_group_group)
|
||||
create(:project_group_link, project: subgroup_project, group: invited_to_project_group)
|
||||
|
||||
sign_in(subgroup_user)
|
||||
end
|
||||
|
||||
it 'shows inherited, direct, and invited group members but not descendent members', :aggregate_failures do
|
||||
visit issues_group_path(subgroup)
|
||||
|
||||
input_filtered_search('assignee:=', submit: false, extra_space: false)
|
||||
|
||||
expect(page).to have_text group_user.name
|
||||
expect(page).to have_text subgroup_user.name
|
||||
expect(page).to have_text invited_to_group_group_user.name
|
||||
expect(page).not_to have_text subsubgroup_user.name
|
||||
expect(page).not_to have_text invited_to_project_group_user.name
|
||||
|
||||
visit project_issues_path(subgroup_project)
|
||||
|
||||
input_filtered_search('assignee:=', submit: false, extra_space: false)
|
||||
|
||||
expect(page).to have_text group_user.name
|
||||
expect(page).to have_text subgroup_user.name
|
||||
expect(page).to have_text invited_to_project_group_user.name
|
||||
expect(page).not_to have_text subsubgroup_user.name
|
||||
expect(page).not_to have_text invited_to_group_group_user.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDropdownItem, GlLoadingIcon, GlAvatar } from '@gitlab/ui';
|
||||
import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
@ -46,6 +46,7 @@ describe('HeaderSearchAutocompleteItems', () => {
|
|||
const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
|
||||
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
const findGlAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
describe('template', () => {
|
||||
describe('when loading is true', () => {
|
||||
|
@ -62,6 +63,15 @@ describe('HeaderSearchAutocompleteItems', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when api returns error', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ autocompleteError: true });
|
||||
});
|
||||
|
||||
it('renders Alert', () => {
|
||||
expect(findGlAlert().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('when loading is false', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ loading: false });
|
||||
|
@ -86,6 +96,7 @@ describe('HeaderSearchAutocompleteItems', () => {
|
|||
expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
item | showAvatar | avatarSize
|
||||
${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import createFlash from '~/flash';
|
||||
import * as actions from '~/header_search/store/actions';
|
||||
import * as types from '~/header_search/store/mutation_types';
|
||||
import createState from '~/header_search/store/state';
|
||||
|
@ -13,11 +12,6 @@ describe('Header Search Store Actions', () => {
|
|||
let state;
|
||||
let mock;
|
||||
|
||||
const flashCallback = (callCount) => {
|
||||
expect(createFlash).toHaveBeenCalledTimes(callCount);
|
||||
createFlash.mockClear();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
state = createState({});
|
||||
mock = new MockAdapter(axios);
|
||||
|
@ -29,10 +23,10 @@ describe('Header Search Store Actions', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
axiosMock | type | expectedMutations | flashCallCount
|
||||
${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]} | ${0}
|
||||
${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]} | ${1}
|
||||
`('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations, flashCallCount }) => {
|
||||
axiosMock | type | expectedMutations
|
||||
${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
|
||||
${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
|
||||
`('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations }) => {
|
||||
describe(`on ${type}`, () => {
|
||||
beforeEach(() => {
|
||||
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
|
||||
|
@ -42,7 +36,7 @@ describe('Header Search Store Actions', () => {
|
|||
action: actions.fetchAutocompleteOptions,
|
||||
state,
|
||||
expectedMutations,
|
||||
}).then(() => flashCallback(flashCallCount));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('Header Search Store Mutations', () => {
|
|||
|
||||
expect(state.loading).toBe(true);
|
||||
expect(state.autocompleteOptions).toStrictEqual([]);
|
||||
expect(state.autocompleteError).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -29,6 +30,7 @@ describe('Header Search Store Mutations', () => {
|
|||
|
||||
expect(state.loading).toBe(false);
|
||||
expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS);
|
||||
expect(state.autocompleteError).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -38,6 +40,7 @@ describe('Header Search Store Mutations', () => {
|
|||
|
||||
expect(state.loading).toBe(false);
|
||||
expect(state.autocompleteOptions).toStrictEqual([]);
|
||||
expect(state.autocompleteError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -46,6 +49,7 @@ describe('Header Search Store Mutations', () => {
|
|||
mutations[types.CLEAR_AUTOCOMPLETE](state);
|
||||
|
||||
expect(state.autocompleteOptions).toStrictEqual([]);
|
||||
expect(state.autocompleteError).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -340,21 +340,9 @@ describe('Sidebar assignees widget', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('when realtime feature flag is disabled', async () => {
|
||||
it('includes the real-time assignees component', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
expect(findRealtimeAssignees().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('when realtime feature flag is enabled', async () => {
|
||||
createComponent({
|
||||
provide: {
|
||||
glFeatures: {
|
||||
realTimeIssueSidebar: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitForPromises();
|
||||
expect(findRealtimeAssignees().exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('sidebar assignees', () => {
|
|||
let wrapper;
|
||||
let mediator;
|
||||
let axiosMock;
|
||||
const createComponent = (realTimeIssueSidebar = false, props) => {
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMount(SidebarAssignees, {
|
||||
propsData: {
|
||||
issuableIid: '1',
|
||||
|
@ -25,11 +25,6 @@ describe('sidebar assignees', () => {
|
|||
changing: false,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
realTimeIssueSidebar,
|
||||
},
|
||||
},
|
||||
// Attaching to document is required because this component emits something from the parent element :/
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
@ -86,27 +81,17 @@ describe('sidebar assignees', () => {
|
|||
expect(wrapper.find(Assigness).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when realTimeIssueSidebar is turned on', () => {
|
||||
describe('when issuableType is issue', () => {
|
||||
it('finds AssigneesRealtime componeont', () => {
|
||||
createComponent(true);
|
||||
describe('when issuableType is issue', () => {
|
||||
it('finds AssigneesRealtime component', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find(AssigneesRealtime).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when issuableType is MR', () => {
|
||||
it('does not find AssigneesRealtime componeont', () => {
|
||||
createComponent(true, { issuableType: 'MR' });
|
||||
|
||||
expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
|
||||
});
|
||||
expect(wrapper.find(AssigneesRealtime).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when realTimeIssueSidebar is turned off', () => {
|
||||
it('does not find AssigneesRealtime', () => {
|
||||
createComponent(false, { issuableType: 'issue' });
|
||||
describe('when issuableType is MR', () => {
|
||||
it('does not find AssigneesRealtime component', () => {
|
||||
createComponent({ issuableType: 'MR' });
|
||||
|
||||
expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
|
||||
});
|
||||
|
|
|
@ -3117,6 +3117,18 @@ RSpec.describe API::Users do
|
|||
expect(response.body).to eq('null')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the API initiating user' do
|
||||
let(:user_id) { admin.id }
|
||||
|
||||
it 'does not block the API initiating user, returns 403' do
|
||||
block_user
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(json_response['message']).to eq('403 Forbidden - The API initiating user cannot be blocked by the API')
|
||||
expect(admin.reload.state).to eq('active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'is not available for non admin users' do
|
||||
|
|
|
@ -1332,32 +1332,14 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
context 'broadcasting issue assignee updates' do
|
||||
let(:update_params) { { assignee_ids: [user2.id] } }
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(broadcast_issue_updates: true)
|
||||
end
|
||||
it 'triggers the GraphQL subscription' do
|
||||
expect(GraphqlTriggers).to receive(:issuable_assignees_updated).with(issue)
|
||||
|
||||
it 'triggers the GraphQL subscription' do
|
||||
expect(GraphqlTriggers).to receive(:issuable_assignees_updated).with(issue)
|
||||
|
||||
update_issue(update_params)
|
||||
end
|
||||
|
||||
context 'when assignee is not updated' do
|
||||
let(:update_params) { { title: 'Some other title' } }
|
||||
|
||||
it 'does not trigger the GraphQL subscription' do
|
||||
expect(GraphqlTriggers).not_to receive(:issuable_assignees_updated).with(issue)
|
||||
|
||||
update_issue(update_params)
|
||||
end
|
||||
end
|
||||
update_issue(update_params)
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(broadcast_issue_updates: false)
|
||||
end
|
||||
context 'when assignee is not updated' do
|
||||
let(:update_params) { { title: 'Some other title' } }
|
||||
|
||||
it 'does not trigger the GraphQL subscription' do
|
||||
expect(GraphqlTriggers).not_to receive(:issuable_assignees_updated).with(issue)
|
||||
|
|
Loading…
Reference in a new issue