diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js index 27efb3f775c6dc14eff8c393c60c5aaa90398fc5..aa9dc0fc1583fe134a038692f84986f3cf66ad97 100644 --- a/app/assets/javascripts/boards/issue_board_filters.js +++ b/app/assets/javascripts/boards/issue_board_filters.js @@ -1,5 +1,7 @@ +import { BoardType } from 'ee_else_ce/boards/constants'; import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql'; import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql'; +import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql'; import groupBoardMilestonesQuery from './graphql/group_board_milestones.query.graphql'; import projectBoardMilestonesQuery from './graphql/project_board_milestones.query.graphql'; import boardLabels from './graphql/board_labels.query.graphql'; @@ -14,6 +16,17 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) { }; const fetchUsers = (usersSearchTerm) => { + if (gon.features?.newGraphqlUsersAutocomplete) { + const namespace = isGroupBoard ? BoardType.group : BoardType.project; + + return apollo + .query({ + query: usersAutocompleteQuery, + variables: { fullPath, search: usersSearchTerm, isProject: !isGroupBoard }, + }) + .then(({ data }) => data[namespace]?.autocompleteUsers); + } + return apollo .query({ query: boardAssigneesQuery(), diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 6bb807be1c439d574b20ea43c66317bbdec169c2..cf0b6cd815a449a501bde99d1020384055f3dde9 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController before_action do push_frontend_feature_flag(:board_multi_select, group) push_frontend_feature_flag(:apollo_boards, group) + push_frontend_feature_flag(:new_graphql_users_autocomplete, group) experiment(:prominent_create_board_btn, subject: current_user) do |e| e.control {} e.candidate {} diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 84872d1e97875ea917f6a3ab9443c4babbbe1a38..1c3463fb70c0729abfb2612baf1ce9f60c364f1e 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -8,6 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action do push_frontend_feature_flag(:board_multi_select, project) push_frontend_feature_flag(:apollo_boards, project) + push_frontend_feature_flag(:new_graphql_users_autocomplete, project) experiment(:prominent_create_board_btn, subject: current_user) do |e| e.control {} e.candidate {} diff --git a/ee/app/assets/javascripts/boards/components/board_add_new_column.vue b/ee/app/assets/javascripts/boards/components/board_add_new_column.vue index 4d832b2c341408a32be9c7b1610b131200689932..22bcd41c2e911b455ab1d4e1cd67adfb75d7d7c8 100644 --- a/ee/app/assets/javascripts/boards/components/board_add_new_column.vue +++ b/ee/app/assets/javascripts/boards/components/board_add_new_column.vue @@ -32,6 +32,7 @@ import groupBoardMembersQuery from '~/boards/graphql/group_board_members.query.g import projectBoardMembersQuery from '~/boards/graphql/project_board_members.query.graphql'; import { setError } from '~/boards/graphql/cache_updates'; import { getListByTypeId } from '~/boards//boards_util'; +import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql'; import searchIterationQuery from 'ee/issues/list/queries/search_iterations.query.graphql'; export const listTypeInfo = { @@ -171,15 +172,31 @@ export default { }, assigneesApollo: { query() { + if (gon.features?.newGraphqlUsersAutocomplete) { + return usersAutocompleteQuery; + } + if (this.boardType === BoardType.project) { return projectBoardMembersQuery; } return groupBoardMembersQuery; }, variables() { + if (gon.features?.newGraphqlUsersAutocomplete) { + return { + fullPath: this.fullPath, + search: this.searchTerm, + isProject: this.boardType === BoardType.project, + }; + } + return { ...this.baseVariables, search: this.searchTerm }; }, update(data) { + if (gon.features?.newGraphqlUsersAutocomplete) { + return data[this.boardType]?.autocompleteUsers; + } + return data.workspace.assignees.nodes.map(({ user }) => user); }, skip() { diff --git a/ee/app/assets/javascripts/boards/stores/actions.js b/ee/app/assets/javascripts/boards/stores/actions.js index 42ec99c92dfaa0b5f5c918822b0ae2501e0392ad..d168e4021a5c5a05bc0d51e84f2b91cbc4fc2b09 100644 --- a/ee/app/assets/javascripts/boards/stores/actions.js +++ b/ee/app/assets/javascripts/boards/stores/actions.js @@ -13,6 +13,7 @@ import projectBoardMembersQuery from '~/boards/graphql/project_board_members.que import actionsCE from '~/boards/stores/actions'; import * as typesCE from '~/boards/stores/mutation_types'; import { TYPENAME_ITERATION, TYPENAME_ITERATIONS_CADENCE } from '~/graphql_shared/constants'; +import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import { fetchPolicies } from '~/lib/graphql'; @@ -649,6 +650,21 @@ export default { throw new Error('Unknown board type'); } + if (gon.features?.newGraphqlUsersAutocomplete) { + return gqlClient + .query({ + query: usersAutocompleteQuery, + variables: { ...variables, isProject: boardType === WORKSPACE_PROJECT }, + }) + .then(({ data }) => { + commit(types.RECEIVE_ASSIGNEES_SUCCESS, data[boardType]?.autocompleteUsers); + }) + .catch((e) => { + commit(types.RECEIVE_ASSIGNEES_FAILURE); + throw e; + }); + } + return gqlClient .query({ query, diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb index 006b7ce45d4f4261118e46cfaf6189719fb2b28e..bfed6e338aedeaf2c57851ddf7829bd73d49ae40 100644 --- a/spec/features/boards/board_filters_spec.rb +++ b/spec/features/boards/board_filters_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe 'Issue board filters', :js, feature_category: :team_planning do - let_it_be(:project) { create(:project, :repository) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:user) { create(:user) } - let_it_be(:board) { create(:board, project: project) } let_it_be(:project_label) { create(:label, project: project, title: 'Label') } let_it_be(:milestone_1) { create(:milestone, project: project, due_date: 3.days.from_now) } let_it_be(:milestone_2) { create(:milestone, project: project, due_date: Date.tomorrow) } @@ -21,166 +21,210 @@ let(:filter_first_suggestion) { find('.gl-filtered-search-suggestion-list').first('.gl-filtered-search-suggestion') } let(:filter_submit) { find('.gl-search-box-by-click-search-button') } - before do - stub_feature_flags(apollo_boards: false) - project.add_maintainer(user) - sign_in(user) + context 'for a project board' do + let_it_be(:board) { create(:board, project: project) } - visit_project_board - end - - shared_examples 'loads all the users when opened' do - it 'and submit one as filter', :aggregate_failures do - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) + before do + stub_feature_flags(apollo_boards: false) + project.add_maintainer(user) + sign_in(user) + visit project_board_path(project, board) wait_for_requests + end - expect_filtered_search_dropdown_results(filter_dropdown, 4) + shared_examples 'loads all the users when opened' do + it 'and submit one as filter', :aggregate_failures do + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) - click_on user.username - filter_submit.click + wait_for_requests - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) - expect(find('.board-card')).to have_content(issue.title) - end - end + expect_filtered_search_dropdown_results(filter_dropdown, 3) - describe 'filters by assignee' do - before do - set_filter('assignee') - end + click_on user.username + filter_submit.click - it_behaves_like 'loads all the users when opened', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/351426' do - let(:issue) { issue_2 } + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + expect(find('.board-card')).to have_content(issue.title) + end end - end - describe 'filters by author' do - before do - set_filter('author') - end + describe 'filters by assignee' do + before do + set_filter('assignee') + end - it_behaves_like 'loads all the users when opened' do - let(:issue) { issue_1 } + it_behaves_like 'loads all the users when opened', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/351426' do + let(:issue) { issue_2 } + end end - end - describe 'filters by label' do - before do - set_filter('label') + describe 'filters by author' do + before do + set_filter('author') + end + + it_behaves_like 'loads all the users when opened' do + let(:issue) { issue_1 } + end end - it 'loads all the labels when opened and submit one as filter', :aggregate_failures do - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) + describe 'filters by label' do + before do + set_filter('label') + end - expect_filtered_search_dropdown_results(filter_dropdown, 3) + it 'loads all the labels when opened and submit one as filter', :aggregate_failures do + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) - filter_dropdown.click_on project_label.title - filter_submit.click + expect_filtered_search_dropdown_results(filter_dropdown, 3) - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) - expect(find('.board-card')).to have_content(issue_2.title) - end - end + filter_dropdown.click_on project_label.title + filter_submit.click - describe 'filters by releases' do - before do - set_filter('release') + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + expect(find('.board-card')).to have_content(issue_2.title) + end end - it 'loads all the releases when opened and submit one as filter', :aggregate_failures do - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) + describe 'filters by releases' do + before do + set_filter('release') + end - expect_filtered_search_dropdown_results(filter_dropdown, 2) + it 'loads all the releases when opened and submit one as filter', :aggregate_failures do + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) - click_on release.tag - filter_submit.click + expect_filtered_search_dropdown_results(filter_dropdown, 2) - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) - expect(find('.board-card')).to have_content(issue_1.title) - end - end + click_on release.tag + filter_submit.click - describe 'filters by confidentiality' do - before do - filter_input.click - filter_input.set("confidential:") + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + expect(find('.board-card')).to have_content(issue_1.title) + end end - it 'loads all the confidentiality options when opened and submit one as filter', :aggregate_failures do - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) + describe 'filters by confidentiality' do + before do + filter_input.click + filter_input.set("confidential:") + end - expect_filtered_search_dropdown_results(filter_dropdown, 2) + it 'loads all the confidentiality options when opened and submit one as filter', :aggregate_failures do + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) - filter_dropdown.click_on 'Yes' - filter_submit.click + expect_filtered_search_dropdown_results(filter_dropdown, 2) - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) - expect(find('.board-card')).to have_content(issue_2.title) - end - end + filter_dropdown.click_on 'Yes' + filter_submit.click - describe 'filters by milestone' do - before do - set_filter('milestone') + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + expect(find('.board-card')).to have_content(issue_2.title) + end end - it 'loads all the milestones when opened and submit one as filter', :aggregate_failures do - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) + describe 'filters by milestone' do + before do + set_filter('milestone') + end - expect_filtered_search_dropdown_results(filter_dropdown, 6) - expect(filter_dropdown).to have_content('None') - expect(filter_dropdown).to have_content('Any') - expect(filter_dropdown).to have_content('Started') - expect(filter_dropdown).to have_content('Upcoming') + it 'loads all the milestones when opened and submit one as filter', :aggregate_failures do + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) - dropdown_nodes = page.find_all('.gl-filtered-search-suggestion-list > .gl-filtered-search-suggestion') + expect_filtered_search_dropdown_results(filter_dropdown, 6) + expect(filter_dropdown).to have_content('None') + expect(filter_dropdown).to have_content('Any') + expect(filter_dropdown).to have_content('Started') + expect(filter_dropdown).to have_content('Upcoming') - expect(dropdown_nodes[4]).to have_content(milestone_2.title) - expect(dropdown_nodes.last).to have_content(milestone_1.title) + dropdown_nodes = page.find_all('.gl-filtered-search-suggestion-list > .gl-filtered-search-suggestion') - click_on milestone_1.title - filter_submit.click + expect(dropdown_nodes[4]).to have_content(milestone_2.title) + expect(dropdown_nodes.last).to have_content(milestone_1.title) - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + click_on milestone_1.title + filter_submit.click + + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + end end - end - describe 'filters by reaction emoji' do - before do - set_filter('my-reaction') + describe 'filters by reaction emoji' do + before do + set_filter('my-reaction') + end + + it 'loads all the emojis when opened and submit one as filter', :aggregate_failures do + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) + + expect_filtered_search_dropdown_results(filter_dropdown, 3) + + click_on 'thumbsup' + filter_submit.click + + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + expect(find('.board-card')).to have_content(issue_1.title) + end end - it 'loads all the emojis when opened and submit one as filter', :aggregate_failures do - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2) + describe 'filters by type' do + let_it_be(:incident) { create(:incident, project: project) } - expect_filtered_search_dropdown_results(filter_dropdown, 3) + before do + set_filter('type') + end - click_on 'thumbsup' - filter_submit.click + it 'loads all the types when opened and submit one as filter', :aggregate_failures do + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 3) - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) - expect(find('.board-card')).to have_content(issue_1.title) + expect_filtered_search_dropdown_results(filter_dropdown, 2) + + click_on 'Incident' + filter_submit.click + + expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) + expect(find('.board-card')).to have_content(incident.title) + end end end - describe 'filters by type' do - let_it_be(:incident) { create(:incident, project: project) } + context 'for a group board' do + let_it_be(:board) { create(:board, group: group) } + + let_it_be(:child_project_member) { create(:user).tap { |u| project.add_maintainer(u) } } before do - set_filter('type') + stub_feature_flags(apollo_boards: false) + + group.add_maintainer(user) + sign_in(user) end - it 'loads all the types when opened and submit one as filter', :aggregate_failures do - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 3) + context 'when filtering by assignee' do + it 'includes descendant project members in autocomplete' do + visit group_board_path(group, board) + wait_for_requests + + set_filter('assignee') - expect_filtered_search_dropdown_results(filter_dropdown, 2) + expect(page).to have_css('.gl-filtered-search-suggestion', text: child_project_member.name) + end - click_on 'Incident' - filter_submit.click + context 'when new_graphql_users_autocomplete is disabled' do + before do + stub_feature_flags(new_graphql_users_autocomplete: false) + end - expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1) - expect(find('.board-card')).to have_content(incident.title) + it 'does not include descendant project members in autocomplete' do + visit group_board_path(group, board) + wait_for_requests + + set_filter('assignee') + + expect(page).not_to have_css('.gl-filtered-search-suggestion', text: child_project_member.name) + end + end end end @@ -193,9 +237,4 @@ def set_filter(filter) def expect_filtered_search_dropdown_results(filter_dropdown, count) expect(filter_dropdown).to have_selector('.gl-dropdown-item', count: count) end - - def visit_project_board - visit project_board_path(project, board) - wait_for_requests - end end