diff --git a/app/assets/javascripts/projects/new_v2/components/app.vue b/app/assets/javascripts/projects/new_v2/components/app.vue
index d4b394b858c0ce605a66838e278d7e969e70c11b..7b7e225064a9a8382f34c79841f569164a39ac29 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 a55a61c2139883ad052234650b849f765415b829..9d17c700713620342907bc1f77705c5f09d475f2 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 a480eba1063bebad9fa7e78acb5b1964acbe66a8..6def6f74db221454eb0189cff461520d7a9ed798 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 6b7bbb2ae53ee257137dd6f0f7ba9e6b7afbd513..fee9c87089e94581a16933e8027ace5de8731edc 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 be6ea12119d31091418dc8351029ea2b4b01b47e..d0b2f874790e90145a647fb062352e4a775b955b 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 4d2bb6b519d776abf83af1c8f937e1096c820bdf..ebfef15ee08508baed762b12d776c1a23b658307 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 3ea8a1b4bd7febfa1ed873deeb87b0881d67cb01..de339f8cc604343795a287846a925aca5efdd1ba 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');
+ });
+ });
});