diff --git a/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue b/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue index 5a2ea77186ab70c8dec1b116110e4dcff8ec6dee..b52a846cb9113c2b3829fb98eff855bcd58a31ce 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue @@ -3,8 +3,6 @@ import { GlCollapsibleListbox } from '@gitlab/ui'; import { debounce } from 'lodash'; import produce from 'immer'; import { __ } from '~/locale'; -import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import { renderMultiSelectText } from 'ee/security_orchestration/components/policy_editor/utils'; import getGroupProjects from 'ee/security_orchestration/graphql/queries/get_group_projects.query.graphql'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; @@ -49,19 +47,12 @@ export default { required: false, default: 'left', }, - selectedProjectsIds: { - type: Array, + selected: { + type: [Array, String], required: false, default: () => [], }, - /** - * selected ids passed as short format - * [21,34,45] as number - * needs to be converted to full graphql id - * if false, selectedProjectsIds needs to be - * an array of full graphQl ids - */ - useShortIdFormat: { + multiple: { type: Boolean, required: false, default: true, @@ -76,16 +67,14 @@ export default { }, computed: { formattedSelectedProjectsIds() { - if (this.useShortIdFormat) { - return ( - this.selectedProjectsIds?.map((id) => convertToGraphQLId(TYPENAME_PROJECT, id)) || [] - ); - } - - return this.selectedProjectsIds || []; + return this.multiple ? this.selected : [this.selected]; }, existingFormattedSelectedProjectsIds() { - return this.formattedSelectedProjectsIds.filter((id) => this.projectsIds.includes(id)); + if (this.multiple) { + return this.selected.filter((id) => this.projectsIds.includes(id)); + } + + return this.selected; }, dropdownPlaceholder() { return renderMultiSelectText( @@ -140,9 +129,10 @@ export default { setSearchTerm(searchTerm = '') { this.searchTerm = searchTerm.trim(); }, - selectProjects(ids) { - const payload = this.useShortIdFormat ? ids.map((id) => getIdFromGraphQLId(id)) : ids; - + selectProjects(selected) { + const ids = this.multiple ? selected : [selected]; + const selectedProjects = this.projects.filter(({ id }) => ids.includes(id)); + const payload = this.multiple ? selectedProjects : selectedProjects[0]; this.$emit('select', payload); }, }, @@ -153,9 +143,9 @@ export default { <gl-collapsible-listbox block is-check-centered - multiple searchable fluid-width + :multiple="multiple" :loading="loading" :header-text="$options.i18n.projectDropdownHeader" :infinite-scroll="projectsPageInfo.hasNextPage" diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/scope_section.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/scope_section.vue index 242dca5d946c10ffbddffba48f61f76671daf670..6572161ba86dea45f3a294c3c99353ec8bc6dbe2 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/scope_section.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/scope_section.vue @@ -2,6 +2,8 @@ import { GlAlert, GlCollapsibleListbox, GlSprintf } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import PolicyPopover from 'ee/security_orchestration/components/policy_popover.vue'; import GroupProjectsDropdown from '../../group_projects_dropdown.vue'; import ComplianceFrameworkDropdown from './compliance_framework_dropdown.vue'; @@ -92,7 +94,8 @@ export default { const projects = Array.isArray(this.policyScope?.projects?.[this.projectsPayloadKey]) ? this.policyScope?.projects?.[this.projectsPayloadKey] : []; - return projects?.map(({ id }) => id) || []; + + return projects?.map(({ id }) => convertToGraphQLId(TYPENAME_PROJECT, id)) || []; }, complianceFrameworksIds() { /** @@ -150,9 +153,9 @@ export default { this.selectedExceptionType = type; this.resetPolicyScope(); }, - setSelectedProjectIds(ids) { - const projects = ids.map((id) => ({ id })); - const payload = { projects: { [this.projectsPayloadKey]: projects } }; + setSelectedProjectIds(projects) { + const projectsIds = projects.map(({ id }) => ({ id: getIdFromGraphQLId(id) })); + const payload = { projects: { [this.projectsPayloadKey]: projectsIds } }; this.triggerChanged(payload); }, @@ -224,7 +227,7 @@ export default { <group-projects-dropdown v-if="showGroupProjectsDropdown" :group-full-path="namespacePath" - :selected-projects-ids="projectIds" + :selected="projectIds" @projects-query-error="setShowAlert($options.i18n.groupProjectErrorDescription)" @select="setSelectedProjectIds" /> diff --git a/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js b/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js index 7d19e0ae68958d423cbcab34227432d116f9997e..8caebcf3ab41ac3d689b583f89f8f7c5b361fed1 100644 --- a/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js +++ b/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js @@ -2,7 +2,7 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMount } from '@vue/test-utils'; import { GlCollapsibleListbox } from '@gitlab/ui'; -import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -87,26 +87,38 @@ describe('GroupProjectsDropdown', () => { await waitForPromises(); findDropdown().vm.$emit('select', [id]); - expect(wrapper.emitted('select')).toEqual([[[getIdFromGraphQLId(id)]]]); + expect(wrapper.emitted('select')).toEqual([[[defaultNodes[0]]]]); }); it('should select all projects', async () => { await waitForPromises(); selectAllProjects(); - expect(wrapper.emitted('select')).toEqual([ - [defaultNodesIds.map((id) => getIdFromGraphQLId(id))], - ]); + expect(wrapper.emitted('select')).toEqual([[defaultNodes]]); }); it('renders default text when loading', () => { expect(findDropdown().props('toggleText')).toBe('Select projects'); }); + it('should select full projects with full id format', async () => { + createComponent({ + propsData: { + useShortIdFormat: false, + }, + }); + + const [{ id }] = defaultNodes; + + await waitForPromises(); + findDropdown().vm.$emit('select', [id]); + expect(wrapper.emitted('select')).toEqual([[[defaultNodes[0]]]]); + }); + describe('selected projects', () => { beforeEach(() => { createComponent({ propsData: { - selectedProjectsIds: defaultNodesIds, + selected: defaultNodesIds, }, }); }); @@ -133,7 +145,7 @@ describe('GroupProjectsDropdown', () => { it('renders default placeholder when selected projects do not exist', async () => { createComponent({ propsData: { - selectedProjectsIds: ['one', 'two'], + selected: ['one', 'two'], }, }); @@ -144,14 +156,43 @@ describe('GroupProjectsDropdown', () => { it('filters selected projects that does not exist', async () => { createComponent({ propsData: { - selectedProjectsIds: ['one', 'two'], + selected: ['one', 'two'], + useShortIdFormat: false, }, }); await waitForPromises(); findDropdown().vm.$emit('select', [defaultNodesIds[0]]); - expect(wrapper.emitted('select')).toEqual([[[getIdFromGraphQLId(defaultNodesIds[0])]]]); + expect(wrapper.emitted('select')).toEqual([[[defaultNodes[0]]]]); + }); + }); + + describe('select single project', () => { + it('support single selection mode', async () => { + createComponent({ + propsData: { + multiple: false, + }, + }); + + await waitForPromises(); + + findDropdown().vm.$emit('select', defaultNodesIds[0]); + expect(wrapper.emitted('select')).toEqual([[defaultNodes[0]]]); + }); + + it('should render single selected project', async () => { + createComponent({ + propsData: { + multiple: false, + selected: defaultNodesIds[0], + }, + }); + + await waitForPromises(); + + expect(findDropdown().props('selected')).toEqual(defaultNodesIds[0]); }); }); @@ -202,13 +243,13 @@ describe('GroupProjectsDropdown', () => { await waitForPromises(); selectAllProjects(); - expect(wrapper.emitted('select')).toEqual([[defaultNodesIds]]); + expect(wrapper.emitted('select')).toEqual([[defaultNodes]]); }); it('should render selected ids in full format', async () => { createComponent({ propsData: { - selectedProjectsIds: defaultNodesIds, + selected: defaultNodesIds, useShortIdFormat: false, }, }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scope/scope_section_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scope/scope_section_spec.js index c64c9305c8b1643e2f4edbc772852a34f48e64e5..92daa622420448ead16d360acaddb60b41067643 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scope/scope_section_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scope/scope_section_spec.js @@ -1,5 +1,7 @@ import { GlAlert, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import ScopeSection from 'ee/security_orchestration/components/policy_editor/scope/scope_section.vue'; import ComplianceFrameworkDropdown from 'ee/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue'; import GroupProjectsDropdown from 'ee/security_orchestration/components/group_projects_dropdown.vue'; @@ -96,7 +98,10 @@ describe('PolicyScope', () => { expect(findGroupProjectsDropdown().exists()).toBe(true); - findGroupProjectsDropdown().vm.$emit('select', ['id1', 'id2']); + findGroupProjectsDropdown().vm.$emit('select', [ + { id: convertToGraphQLId(TYPENAME_PROJECT, '1') }, + { id: convertToGraphQLId(TYPENAME_PROJECT, '2') }, + ]); expect(wrapper.emitted('changed')).toEqual([ [ @@ -113,7 +118,7 @@ describe('PolicyScope', () => { }, }, ], - [{ projects: { excluding: [{ id: 'id1' }, { id: 'id2' }] } }], + [{ projects: { excluding: [{ id: 1 }, { id: 2 }] } }], ]); }); @@ -122,7 +127,10 @@ describe('PolicyScope', () => { expect(findGroupProjectsDropdown().exists()).toBe(true); - findGroupProjectsDropdown().vm.$emit('select', ['id1', 'id2']); + findGroupProjectsDropdown().vm.$emit('select', [ + { id: convertToGraphQLId(TYPENAME_PROJECT, '1') }, + { id: convertToGraphQLId(TYPENAME_PROJECT, '2') }, + ]); expect(wrapper.emitted('changed')).toEqual([ [ @@ -132,7 +140,7 @@ describe('PolicyScope', () => { }, }, ], - [{ projects: { including: [{ id: 'id1' }, { id: 'id2' }] } }], + [{ projects: { including: [{ id: 1 }, { id: 2 }] } }], ]); }); @@ -180,7 +188,10 @@ describe('PolicyScope', () => { expect(findExceptionTypeDropdown().props('selected')).toBe(EXCEPT_PROJECTS); expect(findExceptionTypeDropdown().exists()).toBe(true); expect(findGroupProjectsDropdown().exists()).toBe(true); - expect(findGroupProjectsDropdown().props('selectedProjectsIds')).toEqual(['id1', 'id2']); + expect(findGroupProjectsDropdown().props('selected')).toEqual([ + convertToGraphQLId(TYPENAME_PROJECT, 'id1'), + convertToGraphQLId(TYPENAME_PROJECT, 'id2'), + ]); }); it('should render existing including projects', () => { @@ -197,7 +208,10 @@ describe('PolicyScope', () => { expect(findComplianceFrameworkDropdown().exists()).toBe(false); expect(findExceptionTypeDropdown().exists()).toBe(false); expect(findGroupProjectsDropdown().exists()).toBe(true); - expect(findGroupProjectsDropdown().props('selectedProjectsIds')).toEqual(['id1', 'id2']); + expect(findGroupProjectsDropdown().props('selected')).toEqual([ + convertToGraphQLId(TYPENAME_PROJECT, 'id1'), + convertToGraphQLId(TYPENAME_PROJECT, 'id2'), + ]); }); it('should render alert message for projects dropdown', async () => { @@ -205,7 +219,7 @@ describe('PolicyScope', () => { propsData: { policyScope: { projects: { - including: ['id1', 'id2'], + including: [{ id: 'id1' }, { id: 'id2' }], }, }, },