From 58eae6d90c94bab98354a346c2df4fa223890eab Mon Sep 17 00:00:00 2001 From: Peter Hegman <phegman@gitlab.com> Date: Tue, 20 Feb 2024 16:00:23 -0800 Subject: [PATCH] Add visibility level field To form for adding new group to organization. --- .../groups/components/group_path_field.vue | 2 +- .../groups/components/new_group_form.vue | 36 +++++++++- app/assets/javascripts/groups/constants.js | 1 + .../groups/new/components/app.vue | 2 + .../visibility_level_radio_buttons.vue | 64 +++++++++++++++++ .../javascripts/visibility_level/constants.js | 18 +++++ .../javascripts/visibility_level/utils.js | 16 +++++ .../groups/components/new_group_form_spec.js | 29 +++++++- .../groups/new/components/app_spec.js | 2 + .../visibility_level_radio_buttons_spec.js | 69 +++++++++++++++++++ spec/frontend/visibility_level/utils_spec.js | 45 ++++++++++++ 11 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/visibility_level/components/visibility_level_radio_buttons.vue create mode 100644 app/assets/javascripts/visibility_level/utils.js create mode 100644 spec/frontend/visibility_level/components/visibility_level_radio_buttons_spec.js create mode 100644 spec/frontend/visibility_level/utils_spec.js diff --git a/app/assets/javascripts/groups/components/group_path_field.vue b/app/assets/javascripts/groups/components/group_path_field.vue index d6a7cd00b80f0..53e757ff1b27f 100644 --- a/app/assets/javascripts/groups/components/group_path_field.vue +++ b/app/assets/javascripts/groups/components/group_path_field.vue @@ -11,7 +11,7 @@ const DEBOUNCE_DURATION = 1000; export default { i18n: { - placeholder: __('My awesome group'), + placeholder: __('my-awesome-group'), apiErrorMessage: __( 'An error occurred while checking group path. Please refresh and try again.', ), diff --git a/app/assets/javascripts/groups/components/new_group_form.vue b/app/assets/javascripts/groups/components/new_group_form.vue index 42ade46f11e41..c48138dbe394c 100644 --- a/app/assets/javascripts/groups/components/new_group_form.vue +++ b/app/assets/javascripts/groups/components/new_group_form.vue @@ -3,7 +3,13 @@ import { GlForm, GlFormFields, GlButton } from '@gitlab/ui'; import { formValidators } from '@gitlab/ui/dist/utils'; import { __, s__, sprintf } from '~/locale'; import { slugify } from '~/lib/utils/text_utility'; -import { FORM_FIELD_NAME, FORM_FIELD_PATH } from '../constants'; +import VisibilityLevelRadioButtons from '~/visibility_level/components/visibility_level_radio_buttons.vue'; +import { + VISIBILITY_LEVEL_PRIVATE_INTEGER, + GROUP_VISIBILITY_LEVEL_DESCRIPTIONS, +} from '~/visibility_level/constants'; +import { restrictedVisibilityLevelsMessage } from '~/visibility_level/utils'; +import { FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_VISIBILITY_LEVEL } from '../constants'; import GroupPathField from './group_path_field.vue'; export default { @@ -13,11 +19,13 @@ export default { GlFormFields, GlButton, GroupPathField, + VisibilityLevelRadioButtons, }, i18n: { cancel: __('Cancel'), submitButtonText: __('Create group'), }, + GROUP_VISIBILITY_LEVEL_DESCRIPTIONS, formId: 'organization-new-group-form', props: { basePath: { @@ -36,6 +44,14 @@ export default { required: true, type: String, }, + availableVisibilityLevels: { + type: Array, + required: true, + }, + restrictedVisibilityLevels: { + type: Array, + required: true, + }, }, data() { return { @@ -44,6 +60,7 @@ export default { formValues: { [FORM_FIELD_NAME]: '', [FORM_FIELD_PATH]: '', + [FORM_FIELD_VISIBILITY_LEVEL]: VISIBILITY_LEVEL_PRIVATE_INTEGER, }, }; }, @@ -90,6 +107,15 @@ export default { : null, }, }, + [FORM_FIELD_VISIBILITY_LEVEL]: { + label: __('Visibility level'), + groupAttrs: { + description: restrictedVisibilityLevelsMessage({ + availableVisibilityLevels: this.availableVisibilityLevels, + restrictedVisibilityLevels: this.restrictedVisibilityLevels, + }), + }, + }, }; }, }, @@ -134,6 +160,14 @@ export default { @loading-change="onPathLoading" /> </template> + <template #input(visibilityLevel)="{ value, input }"> + <visibility-level-radio-buttons + :checked="value" + :visibility-levels="availableVisibilityLevels" + :visibility-level-descriptions="$options.GROUP_VISIBILITY_LEVEL_DESCRIPTIONS" + @input="input" + /> + </template> </gl-form-fields> <div class="gl-display-flex gl-gap-3"> <gl-button diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js index d9ec00ab51074..bf1ca39b02c34 100644 --- a/app/assets/javascripts/groups/constants.js +++ b/app/assets/javascripts/groups/constants.js @@ -64,3 +64,4 @@ export const OVERVIEW_TABS_ARCHIVED_PROJECTS_SORTING_ITEMS = [ export const FORM_FIELD_NAME = 'name'; export const FORM_FIELD_PATH = 'path'; +export const FORM_FIELD_VISIBILITY_LEVEL = 'visibilityLevel'; diff --git a/app/assets/javascripts/organizations/groups/new/components/app.vue b/app/assets/javascripts/organizations/groups/new/components/app.vue index 4df74cac9120e..934ba730d095b 100644 --- a/app/assets/javascripts/organizations/groups/new/components/app.vue +++ b/app/assets/javascripts/organizations/groups/new/components/app.vue @@ -57,6 +57,8 @@ export default { :path-maxlength="pathMaxlength" :path-pattern="pathPattern" :cancel-path="groupsOrganizationPath" + :available-visibility-levels="availableVisibilityLevels" + :restricted-visibility-levels="restrictedVisibilityLevels" /> </div> </template> diff --git a/app/assets/javascripts/visibility_level/components/visibility_level_radio_buttons.vue b/app/assets/javascripts/visibility_level/components/visibility_level_radio_buttons.vue new file mode 100644 index 0000000000000..8260b6c1fd2e0 --- /dev/null +++ b/app/assets/javascripts/visibility_level/components/visibility_level_radio_buttons.vue @@ -0,0 +1,64 @@ +<script> +import { GlIcon, GlFormRadio, GlFormRadioGroup } from '@gitlab/ui'; +import { + VISIBILITY_LEVEL_LABELS, + VISIBILITY_TYPE_ICON, + VISIBILITY_LEVELS_INTEGER_TO_STRING, +} from '~/visibility_level/constants'; + +export default { + name: 'VisibilityLevelRadioButtons', + components: { + GlIcon, + GlFormRadio, + GlFormRadioGroup, + }, + model: { + prop: 'checked', + }, + props: { + checked: { + type: Number, + required: true, + }, + visibilityLevels: { + type: Array, + required: true, + }, + visibilityLevelDescriptions: { + type: Object, + required: true, + }, + }, + computed: { + visibilityLevelsOptions() { + return this.visibilityLevels.map((visibilityLevel) => { + const stringValue = VISIBILITY_LEVELS_INTEGER_TO_STRING[visibilityLevel]; + + return { + label: VISIBILITY_LEVEL_LABELS[stringValue], + description: this.visibilityLevelDescriptions[stringValue], + icon: VISIBILITY_TYPE_ICON[stringValue], + value: visibilityLevel, + }; + }); + }, + }, +}; +</script> + +<template> + <gl-form-radio-group :checked="checked" @input="$emit('input', $event)"> + <gl-form-radio + v-for="{ label, description, icon, value } in visibilityLevelsOptions" + :key="value" + :value="value" + > + <div> + <gl-icon :name="icon" /> + <span>{{ label }}</span> + </div> + <template #help>{{ description }}</template> + </gl-form-radio> + </gl-form-radio-group> +</template> diff --git a/app/assets/javascripts/visibility_level/constants.js b/app/assets/javascripts/visibility_level/constants.js index 0d9ededc550f5..3060b55ce25e9 100644 --- a/app/assets/javascripts/visibility_level/constants.js +++ b/app/assets/javascripts/visibility_level/constants.js @@ -51,6 +51,24 @@ export const ORGANIZATION_VISIBILITY_TYPE = { ), }; +export const GROUP_VISIBILITY_LEVEL_DESCRIPTIONS = { + [VISIBILITY_LEVEL_PUBLIC_STRING]: s__( + 'VisibilityLevel|The group and any public projects can be viewed without any authentication.', + ), + [VISIBILITY_LEVEL_INTERNAL_STRING]: s__( + 'VisibilityLevel|The group and any internal projects can be viewed by any logged in user except external users.', + ), + [VISIBILITY_LEVEL_PRIVATE_STRING]: s__( + 'VisibilityLevel|The group and its projects can only be viewed by members.', + ), +}; + +export const VISIBILITY_LEVEL_LABELS = { + [VISIBILITY_LEVEL_PUBLIC_STRING]: s__('VisibilityLevel|Public'), + [VISIBILITY_LEVEL_INTERNAL_STRING]: s__('VisibilityLevel|Internal'), + [VISIBILITY_LEVEL_PRIVATE_STRING]: s__('VisibilityLevel|Private'), +}; + export const VISIBILITY_TYPE_ICON = { [VISIBILITY_LEVEL_PUBLIC_STRING]: 'earth', [VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield', diff --git a/app/assets/javascripts/visibility_level/utils.js b/app/assets/javascripts/visibility_level/utils.js new file mode 100644 index 0000000000000..c8ac3c97d3e3a --- /dev/null +++ b/app/assets/javascripts/visibility_level/utils.js @@ -0,0 +1,16 @@ +import { __ } from '~/locale'; + +export const restrictedVisibilityLevelsMessage = ({ + availableVisibilityLevels, + restrictedVisibilityLevels, +}) => { + if (!restrictedVisibilityLevels.length) { + return ''; + } + + if (!availableVisibilityLevels.length) { + return __('Visibility settings have been disabled by the administrator.'); + } + + return __('Other visibility settings have been disabled by the administrator.'); +}; diff --git a/spec/frontend/groups/components/new_group_form_spec.js b/spec/frontend/groups/components/new_group_form_spec.js index 23dc0f3296780..97ab55b85b210 100644 --- a/spec/frontend/groups/components/new_group_form_spec.js +++ b/spec/frontend/groups/components/new_group_form_spec.js @@ -2,6 +2,13 @@ import { nextTick } from 'vue'; import NewGroupForm from '~/groups/components/new_group_form.vue'; import GroupPathField from '~/groups/components/group_path_field.vue'; +import VisibilityLevelRadioButtons from '~/visibility_level/components/visibility_level_radio_buttons.vue'; +import { + VISIBILITY_LEVELS_STRING_TO_INTEGER, + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, + GROUP_VISIBILITY_LEVEL_DESCRIPTIONS, +} from '~/visibility_level/constants'; import { mountExtended } from 'helpers/vue_test_utils_helper'; describe('NewGroupForm', () => { @@ -12,6 +19,8 @@ describe('NewGroupForm', () => { cancelPath: '/-/organizations/default/groups_and_projects?display=groups', pathMaxlength: 10, pathPattern: '[a-zA-Z0-9_\\.][a-zA-Z0-9_\\-\\.]{0,254}[a-zA-Z0-9_\\-]|[a-zA-Z0-9_]', + availableVisibilityLevels: Object.values(VISIBILITY_LEVELS_STRING_TO_INTEGER), + restrictedVisibilityLevels: [], }; const createComponent = ({ propsData = {} } = {}) => { @@ -29,11 +38,16 @@ describe('NewGroupForm', () => { const findNameField = () => wrapper.findByLabelText('Group name'); const findPathField = () => wrapper.findComponent(GroupPathField); + const findVisibilityLevelField = () => wrapper.findComponent(VisibilityLevelRadioButtons); const setPathFieldValue = async (value) => { findPathField().vm.$emit('input', value); await nextTick(); }; + const setVisibilityLevelFieldValue = async (value) => { + findVisibilityLevelField().vm.$emit('input', value); + await nextTick(); + }; const submitForm = async () => { await wrapper.findByRole('button', { name: 'Create group' }).trigger('click'); }; @@ -50,6 +64,16 @@ describe('NewGroupForm', () => { expect(findPathField().exists()).toBe(true); }); + it('renders `Visibility level` field with correct props', () => { + createComponent(); + + expect(findVisibilityLevelField().props()).toMatchObject({ + checked: VISIBILITY_LEVEL_PRIVATE_INTEGER, + visibilityLevels: defaultPropsData.availableVisibilityLevels, + visibilityLevelDescriptions: GROUP_VISIBILITY_LEVEL_DESCRIPTIONS, + }); + }); + describe('when form is submitted without filling in required fields', () => { beforeEach(async () => { createComponent(); @@ -127,11 +151,14 @@ describe('NewGroupForm', () => { createComponent(); await findNameField().setValue('Foo bar'); + await setVisibilityLevelFieldValue(VISIBILITY_LEVEL_PUBLIC_INTEGER); await submitForm(); }); it('emits `submit` event with form values', () => { - expect(wrapper.emitted('submit')).toEqual([[{ name: 'Foo bar', path: 'foo-bar' }]]); + expect(wrapper.emitted('submit')).toEqual([ + [{ name: 'Foo bar', path: 'foo-bar', visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER }], + ]); }); }); diff --git a/spec/frontend/organizations/groups/new/components/app_spec.js b/spec/frontend/organizations/groups/new/components/app_spec.js index 68ad940e076c5..649598e00925a 100644 --- a/spec/frontend/organizations/groups/new/components/app_spec.js +++ b/spec/frontend/organizations/groups/new/components/app_spec.js @@ -56,6 +56,8 @@ describe('OrganizationGroupsNewApp', () => { cancelPath: '/-/organizations/carrot/groups_and_projects?display=groups', pathMaxlength: 10, pathPattern: 'mockPattern', + availableVisibilityLevels: defaultProvide.availableVisibilityLevels, + restrictedVisibilityLevels: defaultProvide.restrictedVisibilityLevels, }); }); }); diff --git a/spec/frontend/visibility_level/components/visibility_level_radio_buttons_spec.js b/spec/frontend/visibility_level/components/visibility_level_radio_buttons_spec.js new file mode 100644 index 0000000000000..b239c3d93ea84 --- /dev/null +++ b/spec/frontend/visibility_level/components/visibility_level_radio_buttons_spec.js @@ -0,0 +1,69 @@ +import { GlIcon, GlFormRadio, GlFormRadioGroup } from '@gitlab/ui'; + +import { + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, + VISIBILITY_LEVELS_STRING_TO_INTEGER, + GROUP_VISIBILITY_LEVEL_DESCRIPTIONS, +} from '~/visibility_level/constants'; +import VisibilityLevelRadioButtons from '~/visibility_level/components/visibility_level_radio_buttons.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +describe('VisibilityLevelRadioButtons', () => { + let wrapper; + + const defaultPropsData = { + checked: VISIBILITY_LEVEL_PRIVATE_INTEGER, + visibilityLevels: Object.values(VISIBILITY_LEVELS_STRING_TO_INTEGER), + visibilityLevelDescriptions: GROUP_VISIBILITY_LEVEL_DESCRIPTIONS, + }; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = mountExtended(VisibilityLevelRadioButtons, { + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + }; + + const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); + + it('renders radio group with `checked` prop correctly set', () => { + createComponent(); + + expect(findRadioGroup().vm.$attrs.checked).toBe(defaultPropsData.checked); + }); + + describe('when radio group emits `input` event', () => { + beforeEach(() => { + createComponent(); + findRadioGroup().vm.$emit('input', VISIBILITY_LEVEL_PUBLIC_INTEGER); + }); + + it('emits `input` event', () => { + expect(wrapper.emitted('input')).toEqual([[VISIBILITY_LEVEL_PUBLIC_INTEGER]]); + }); + }); + + it('renders visibility level radio buttons with label, description, and icon', () => { + createComponent(); + + const radioButtons = wrapper.findAllComponents(GlFormRadio); + + expect(radioButtons.at(0).text()).toMatchInterpolatedText( + 'Private The group and its projects can only be viewed by members.', + ); + expect(radioButtons.at(0).findComponent(GlIcon).props('name')).toBe('lock'); + + expect(radioButtons.at(1).text()).toMatchInterpolatedText( + 'Internal The group and any internal projects can be viewed by any logged in user except external users.', + ); + expect(radioButtons.at(1).findComponent(GlIcon).props('name')).toBe('shield'); + + expect(radioButtons.at(2).text()).toMatchInterpolatedText( + 'Public The group and any public projects can be viewed without any authentication.', + ); + expect(radioButtons.at(2).findComponent(GlIcon).props('name')).toBe('earth'); + }); +}); diff --git a/spec/frontend/visibility_level/utils_spec.js b/spec/frontend/visibility_level/utils_spec.js new file mode 100644 index 0000000000000..531b6fc4e9052 --- /dev/null +++ b/spec/frontend/visibility_level/utils_spec.js @@ -0,0 +1,45 @@ +import { restrictedVisibilityLevelsMessage } from '~/visibility_level/utils'; +import { + VISIBILITY_LEVELS_STRING_TO_INTEGER, + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, +} from '~/visibility_level/constants'; + +describe('restrictedVisibilityLevelsMessage', () => { + describe('when no levels are restricted', () => { + it('returns empty string', () => { + expect( + restrictedVisibilityLevelsMessage({ + availableVisibilityLevels: Object.values(VISIBILITY_LEVELS_STRING_TO_INTEGER), + restrictedVisibilityLevels: [], + }), + ).toBe(''); + }); + }); + + describe('when some levels have been restricted', () => { + it('returns expected message', () => { + expect( + restrictedVisibilityLevelsMessage({ + availableVisibilityLevels: [VISIBILITY_LEVEL_PRIVATE_INTEGER], + restrictedVisibilityLevels: [ + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, + ], + }), + ).toBe('Other visibility settings have been disabled by the administrator.'); + }); + }); + + describe('when all visibility levels are restricted', () => { + it('returns expected message', () => { + expect( + restrictedVisibilityLevelsMessage({ + availableVisibilityLevels: [], + restrictedVisibilityLevels: Object.values(VISIBILITY_LEVELS_STRING_TO_INTEGER), + }), + ).toBe('Visibility settings have been disabled by the administrator.'); + }); + }); +}); -- GitLab