From bd63f0bf61bcdc4d22158f97ee1066cc0bfb2f57 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Tue, 29 Apr 2025 17:54:04 +0200 Subject: [PATCH] Add visibility select to the project creation form --- .../projects/new_v2/components/app.vue | 3 +- .../components/project_destination_select.vue | 1 - .../shared_project_creation_fields.vue | 94 ++++++++++++++++++- .../javascripts/projects/new_v2/index.js | 4 + .../javascripts/visibility_level/constants.js | 12 +++ app/views/projects/new.html.haml | 2 + .../shared_project_creation_fields_spec.js | 74 ++++++++++++++- 7 files changed, 180 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/projects/new_v2/components/app.vue b/app/assets/javascripts/projects/new_v2/components/app.vue index d4b394b858c0ce..7b7e225064a9a8 100644 --- a/app/assets/javascripts/projects/new_v2/components/app.vue +++ b/app/assets/javascripts/projects/new_v2/components/app.vue @@ -178,9 +178,10 @@ export default { this.currentStep = 1; } }, - onSelectNamespace({ id, fullPath }) { + onSelectNamespace({ id, fullPath, visibility }) { this.namespace.id = id; this.namespace.fullPath = fullPath; + this.namespace.visibility = visibility; this.showValidation = false; }, }, diff --git a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue index a55a61c2139883..9d17c700713620 100644 --- a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue +++ b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue @@ -170,7 +170,6 @@ export default { fullPath: namespace.fullPath, isPersonal: namespace.fullPath === this.userNamespaceFullPath, }); - this.setNamespace(namespace); }, handleSelectTemplate(id, fullPath) { diff --git a/app/assets/javascripts/projects/new_v2/components/shared_project_creation_fields.vue b/app/assets/javascripts/projects/new_v2/components/shared_project_creation_fields.vue index a480eba1063beb..6def6f74db2214 100644 --- a/app/assets/javascripts/projects/new_v2/components/shared_project_creation_fields.vue +++ b/app/assets/javascripts/projects/new_v2/components/shared_project_creation_fields.vue @@ -1,8 +1,21 @@ @@ -168,6 +232,30 @@ export default { - + + + + + {{ title }} + + + diff --git a/app/assets/javascripts/projects/new_v2/index.js b/app/assets/javascripts/projects/new_v2/index.js index 6b7bbb2ae53ee2..fee9c87089e945 100644 --- a/app/assets/javascripts/projects/new_v2/index.js +++ b/app/assets/javascripts/projects/new_v2/index.js @@ -30,6 +30,8 @@ export function initNewProjectForm() { canSelectNamespace, canCreateProject, userProjectLimit, + restrictedVisibilityLevels, + defaultProjectVisibility, importHistoryPath, importGitlabEnabled, importGitlabImportPath, @@ -70,6 +72,8 @@ export function initNewProjectForm() { canSelectNamespace: parseBoolean(canSelectNamespace), canCreateProject: parseBoolean(canCreateProject), userProjectLimit: parseInt(userProjectLimit, 10), + restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels), + defaultProjectVisibility, importHistoryPath, importGitlabEnabled: parseBoolean(importGitlabEnabled), importGitlabImportPath, diff --git a/app/assets/javascripts/visibility_level/constants.js b/app/assets/javascripts/visibility_level/constants.js index be6ea12119d310..d0b2f874790e90 100644 --- a/app/assets/javascripts/visibility_level/constants.js +++ b/app/assets/javascripts/visibility_level/constants.js @@ -57,6 +57,18 @@ export const ORGANIZATION_VISIBILITY_TYPE = { ), }; +export const PROJECT_VISIBILITY_LEVEL_DESCRIPTIONS = { + [VISIBILITY_LEVEL_PUBLIC_STRING]: s__( + 'VisibilityLevel|Project access must be granted explicitly to each user. If this project is part of a group, access is granted to members of the group.', + ), + [VISIBILITY_LEVEL_INTERNAL_STRING]: s__( + 'VisibilityLevel|The project can be accessed by any logged in user except external users.', + ), + [VISIBILITY_LEVEL_PRIVATE_STRING]: s__( + 'VisibilityLevel|The project can be accessed without any authentication.', + ), +}; + export const GROUP_VISIBILITY_LEVEL_DESCRIPTIONS = { [VISIBILITY_LEVEL_PUBLIC_STRING]: s__( 'VisibilityLevel|The group and any public projects can be viewed without any authentication.', diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 4d2bb6b519d776..ebfef15ee08508 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -25,6 +25,8 @@ can_select_namespace: current_user.can_select_namespace?.to_s, can_create_project: current_user.can_create_project?.to_s, user_project_limit: current_user.projects_limit, + restricted_visibility_levels: restricted_visibility_levels, + default_project_visibility: default_project_visibility, import_history_path: import_history_index_path, import_gitlab_enabled: gitlab_project_import_enabled?.to_s, import_gitlab_import_path: new_import_gitlab_project_path, diff --git a/spec/frontend/projects/new_v2/components/shared_project_creation_fields_spec.js b/spec/frontend/projects/new_v2/components/shared_project_creation_fields_spec.js index 3ea8a1b4bd7feb..de339f8cc60434 100644 --- a/spec/frontend/projects/new_v2/components/shared_project_creation_fields_spec.js +++ b/spec/frontend/projects/new_v2/components/shared_project_creation_fields_spec.js @@ -1,6 +1,7 @@ import { nextTick } from 'vue'; import { GlFormInput, GlFormSelect } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import SingleChoiceSelector from '~/vue_shared/components/single_choice_selector.vue'; import SharedProjectCreationFields from '~/projects/new_v2/components/shared_project_creation_fields.vue'; import NewProjectDestinationSelect from '~/projects/new_v2/components/project_destination_select.vue'; import { DEPLOYMENT_TARGET_SELECTIONS } from '~/projects/new_v2/form_constants'; @@ -16,12 +17,15 @@ describe('Project creation form fields component', () => { }, }; - const createComponent = (props = {}) => { + const createComponent = ({ props = {}, provide = {} } = {}) => { wrapper = shallowMountExtended(SharedProjectCreationFields, { propsData: { ...defaultProps, ...props, }, + provide: { + ...provide, + }, stubs: { GlFormInput, GlFormSelect, @@ -29,28 +33,34 @@ describe('Project creation form fields component', () => { }); }; - beforeEach(() => { - createComponent(); - }); - const findProjectNameInput = () => wrapper.findByTestId('project-name-input'); const findProjectSlugInput = () => wrapper.findByTestId('project-slug-input'); const findNamespaceSelect = () => wrapper.findComponent(NewProjectDestinationSelect); const findDeploymentTargetSelect = () => wrapper.findByTestId('deployment-target-select'); const findKubernetesHelpLink = () => wrapper.findByTestId('kubernetes-help-link'); + const findVisibilitySelector = () => wrapper.findComponent(SingleChoiceSelector); + const findPrivateVisibilityLevelOption = () => wrapper.findByTestId('private-visibility-level'); + const findInternalVisibilityLevelOption = () => wrapper.findByTestId('internal-visibility-level'); + const findPublicVisibilityLevelOption = () => wrapper.findByTestId('public-visibility-level'); describe('target select', () => { it('renders the optional deployment target select', () => { + createComponent(); + expect(findDeploymentTargetSelect().exists()).toBe(true); expect(findKubernetesHelpLink().exists()).toBe(false); }); it('has all the options', () => { + createComponent(); + expect(findDeploymentTargetSelect().props('options')).toEqual(DEPLOYMENT_TARGET_SELECTIONS); }); }); it('updates project slug according to a project name', async () => { + createComponent(); + // NOTE: vue3 test needs the .setValue(value) and the vm.$emit('input'), // while the vue2 needs either .setValue(value) or vm.$emit('input', value) const value = 'My Awesome Project 123'; @@ -62,6 +72,8 @@ describe('Project creation form fields component', () => { }); it('emits namespace change', () => { + createComponent(); + findNamespaceSelect().vm.$emit('onSelectNamespace', { id: '2', fullPath: 'group/subgroup', @@ -78,6 +90,8 @@ describe('Project creation form fields component', () => { describe('validation', () => { it('shows an error message when project name is cleared', async () => { + createComponent(); + findProjectNameInput().setValue(''); findProjectNameInput().trigger('blur'); await nextTick(); @@ -87,6 +101,8 @@ describe('Project creation form fields component', () => { }); it('shows an error message when slug is cleared', async () => { + createComponent(); + findProjectSlugInput().setValue(''); findProjectSlugInput().trigger('blur'); await nextTick(); @@ -95,4 +111,52 @@ describe('Project creation form fields component', () => { expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please enter project slug.'); }); }); + + describe('visibility selector', () => { + it('renders all levels when there are no restictions and parent is public', async () => { + createComponent(); + await nextTick(); + + expect(findPrivateVisibilityLevelOption().props('disabled')).toBe(false); + expect(findInternalVisibilityLevelOption().props('disabled')).toBe(false); + expect(findPublicVisibilityLevelOption().props('disabled')).toBe(false); + }); + + it('renders internal visibility level as disabled when it was rescticted by admin', async () => { + createComponent({ + provide: { restrictedVisibilityLevels: [10] }, + }); + await nextTick(); + + expect(findPrivateVisibilityLevelOption().props('disabled')).toBe(false); + expect(findInternalVisibilityLevelOption().props('disabled')).toBe(true); + expect(findPublicVisibilityLevelOption().props('disabled')).toBe(false); + }); + + it('renders public and internal visibility levels as disabled when parent is private', async () => { + createComponent({ + props: { + namespace: { + id: '1', + fullPath: 'root', + isPersonal: false, + visibility: 'private', + }, + }, + }); + await nextTick(); + + expect(findPrivateVisibilityLevelOption().props('disabled')).toBe(false); + expect(findInternalVisibilityLevelOption().props('disabled')).toBe(true); + expect(findPublicVisibilityLevelOption().props('disabled')).toBe(true); + }); + + it('renders internal visibility level as default when admin set it up', () => { + createComponent({ + provide: { defaultProjectVisibility: 10 }, + }); + + expect(findVisibilitySelector().props('checked')).toBe('internal'); + }); + }); }); -- GitLab