diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 84b5f00728284c7906b7ddb751d1e42592c28aca..f3b537c83e213d84802ca1a8b183959c73127061 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -577,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown'; case 'edit': shortcut_handler = new ShortcutsNavigation(); new ProjectNew(); + import(/* webpackChunkName: 'project_permissions' */ './projects/permissions') + .then(permissions => permissions.default()) + .catch(() => {}); break; case 'new': new ProjectNew(); diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue new file mode 100644 index 0000000000000000000000000000000000000000..80c5d39f7362789cd7b062bced4551f60da02a78 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue @@ -0,0 +1,104 @@ +<script> +import projectFeatureToggle from './project_feature_toggle.vue'; + +export default { + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, + + components: { + projectFeatureToggle, + }, + + computed: { + featureEnabled() { + return this.value !== 0; + }, + + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [ + [0, 'Enable feature to choose access level'], + ]; + }, + + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; + }, + }, + + model: { + prop: 'value', + event: 'change', + }, + + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, + + selectOption(e) { + this.$emit('change', Number(e.target.value)); + }, + }, +}; +</script> + +<template> + <div class="project-feature-controls" :data-for="name"> + <input + v-if="name" + type="hidden" + :name="name" + :value="value" + /> + <project-feature-toggle + :value="featureEnabled" + @change="toggleFeature" + :disabledInput="disabledInput" + /> + <div class="select-wrapper"> + <select + class="form-control project-repo-select select-control" + @change="selectOption" + :disabled="displaySelectInput" + > + <option + v-for="[optionValue, optionName] in displayOptions" + :key="optionValue" + :value="optionValue" + :selected="optionValue === value" + > + {{optionName}} + </option> + </select> + <i aria-hidden="true" class="fa fa-chevron-down"></i> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue new file mode 100644 index 0000000000000000000000000000000000000000..2403c60186a465da401bb83644b3d90e1ef07e67 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue @@ -0,0 +1,51 @@ +<script> +export default { + props: { + name: { + type: String, + required: false, + default: '', + }, + value: { + type: Boolean, + required: true, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, + + model: { + prop: 'value', + event: 'change', + }, + + methods: { + toggleFeature() { + if (!this.disabledInput) this.$emit('change', !this.value); + }, + }, +}; +</script> + +<template> + <label class="toggle-wrapper"> + <input + v-if="name" + type="hidden" + :name="name" + :value="value" + /> + <button + type="button" + aria-label="Toggle" + class="project-feature-toggle" + data-enabled-text="Enabled" + data-disabled-text="Disabled" + :class="{ checked: value, disabled: disabledInput }" + @click="toggleFeature" + /> + </label> +</template> diff --git a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue new file mode 100644 index 0000000000000000000000000000000000000000..6140d74fea81e619526131a4e4c0241dbd1ff3b5 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue @@ -0,0 +1,36 @@ +<script> +export default { + props: { + label: { + type: String, + required: false, + default: null, + }, + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, +}; +</script> + +<template> + <div class="project-feature-row"> + <label v-if="label" class="label-light"> + {{label}} + <a v-if="helpPath" :href="helpPath" target="_blank"> + <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i> + </a> + </label> + <span v-if="helpText" class="help-block"> + {{helpText}} + </span> + <slot /> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue new file mode 100644 index 0000000000000000000000000000000000000000..326d910566690979ec90820e83efb93e628baecd --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue @@ -0,0 +1,312 @@ +<script> +import projectFeatureSetting from './project_feature_setting.vue'; +import projectFeatureToggle from './project_feature_toggle.vue'; +import projectSettingRow from './project_setting_row.vue'; +import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; +import { toggleHiddenClassBySelector } from '../external'; + +export default { + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + }, + lfsHelpPath: { + type: String, + required: false, + }, + registryHelpPath: { + type: String, + required: false, + }, + }, + + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; + + return { ...defaults, ...this.currentSettings }; + }, + + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, + + computed: { + featureAccessLevelOptions() { + const options = [ + [10, 'Only Project Members'], + ]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, + + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, + + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, + + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; + }, + }, + + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, + + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); + }, + }, + + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + this.highlightChanges(); + } + }, + + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; + } + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; + } + }, + + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, + + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, + + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); + }, + }, +}; + +</script> + +<template> + <div> + <div class="project-visibility-setting"> + <project-setting-row + label="Project visibility" + :help-path="visibilityHelpPath" + > + <div class="project-feature-controls"> + <div class="select-wrapper"> + <select + name="project[visibility_level]" + v-model="visibilityLevel" + class="form-control select-control" + :disabled="!canChangeVisibilityLevel" + > + <option + :value="visibilityOptions.PRIVATE" + :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" + > + Private + </option> + <option + :value="visibilityOptions.INTERNAL" + :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" + > + Internal + </option> + <option + :value="visibilityOptions.PUBLIC" + :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" + > + Public + </option> + </select> + <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i> + </div> + </div> + <span class="help-block">{{ visibilityLevelDescription }}</span> + <label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access"> + <input + type="hidden" + name="project[request_access_enabled]" + :value="requestAccessEnabled" + /> + <input type="checkbox" v-model="requestAccessEnabled" /> + Allow users to request access + </label> + </project-setting-row> + </div> + <div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }"> + <project-setting-row + label="Issues" + help-text="Lightweight issue tracking system for this project" + > + <project-feature-setting + name="project[project_feature_attributes][issues_access_level]" + :options="featureAccessLevelOptions" + v-model="issuesAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Repository" + help-text="View and edit files in this project" + > + <project-feature-setting + name="project[project_feature_attributes][repository_access_level]" + :options="featureAccessLevelOptions" + v-model="repositoryAccessLevel" + /> + </project-setting-row> + <div class="project-feature-setting-group"> + <project-setting-row + label="Merge requests" + help-text="Submit changes to be merged upstream" + > + <project-feature-setting + name="project[project_feature_attributes][merge_requests_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="mergeRequestsAccessLevel" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + label="Pipelines" + help-text="Build, test, and deploy your changes" + > + <project-feature-setting + name="project[project_feature_attributes][builds_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="buildsAccessLevel" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="registryAvailable" + label="Container registry" + :help-path="registryHelpPath" + help-text="Every project can have its own space to store its Docker images" + > + <project-feature-toggle + name="project[container_registry_enabled]" + v-model="containerRegistryEnabled" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="lfsAvailable" + label="Git Large File Storage" + :help-path="lfsHelpPath" + help-text="Manages large files such as audio, video, and graphics files" + > + <project-feature-toggle + name="project[lfs_enabled]" + v-model="lfsEnabled" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + </div> + <project-setting-row + label="Wiki" + help-text="Pages for project documentation" + > + <project-feature-setting + name="project[project_feature_attributes][wiki_access_level]" + :options="featureAccessLevelOptions" + v-model="wikiAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Snippets" + help-text="Share code pastes with others out of Git repository" + > + <project-feature-setting + name="project[project_feature_attributes][snippets_access_level]" + :options="featureAccessLevelOptions" + v-model="snippetsAccessLevel" + /> + </project-setting-row> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/constants.js b/app/assets/javascripts/projects/permissions/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..ce47562f259db48aef2ff8117dde9e6e977c73b4 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/constants.js @@ -0,0 +1,11 @@ +export const visibilityOptions = { + PRIVATE: 0, + INTERNAL: 10, + PUBLIC: 20, +}; + +export const visibilityLevelDescriptions = { + [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', + [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.', + [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.', +}; diff --git a/app/assets/javascripts/projects/permissions/external.js b/app/assets/javascripts/projects/permissions/external.js new file mode 100644 index 0000000000000000000000000000000000000000..460af4a211132aeabecdfb3894f88fb782fb07f7 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/external.js @@ -0,0 +1,18 @@ +const selectorCache = []; + +// workaround since we don't have a polyfill for classList.toggle 2nd parameter +export function toggleHiddenClass(element, hidden) { + if (hidden) { + element.classList.add('hidden'); + } else { + element.classList.remove('hidden'); + } +} + +// hide external feature-specific settings when a given feature is disabled +export function toggleHiddenClassBySelector(selector, hidden) { + if (!selectorCache[selector]) { + selectorCache[selector] = document.querySelectorAll(selector); + } + selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden)); +} diff --git a/app/assets/javascripts/projects/permissions/index.js b/app/assets/javascripts/projects/permissions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dbde8dda63418c9460a39195cb6a1ce983cd53ed --- /dev/null +++ b/app/assets/javascripts/projects/permissions/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import settingsPanel from './components/settings_panel.vue'; + +export default function initProjectPermissionsSettings() { + const mountPoint = document.querySelector('.js-project-permissions-form'); + const componentPropsEl = document.querySelector('.js-project-permissions-form-data'); + const componentProps = JSON.parse(componentPropsEl.innerHTML); + + return new Vue({ + el: mountPoint, + render: createElement => createElement(settingsPanel, { props: { ...componentProps } }), + }); +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index becdd7ff35b20945ecca3819d84c37e761716563..3857226cddb543d5ad76c511f457d271f3a9400a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -579,6 +579,11 @@ $project-breadcrumb-color: #999; $project-private-forks-notice-odd: $green-600; $project-network-controls-color: #888; +$feature-toggle-color: #fff; +$feature-toggle-text-color: #fff; +$feature-toggle-color-disabled: #999; +$feature-toggle-color-enabled: #4a8bee; + /* * Runners */ diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index dd600a275455ba9ea7011f48b65bfb75d79fe0bb..94e4f4334d489bbf3e9d1ce5b087e58dfabe1f7a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -10,41 +10,6 @@ .edit-project, .import-project { - .sharing-and-permissions { - .header { - padding-top: $gl-vert-padding; - } - - .label-light { - margin-bottom: 0; - } - - .help-block { - margin-top: 0; - } - - .form-group { - margin-bottom: 5px; - } - - > .form-group { - padding-left: 0; - } - - select option[disabled] { - display: none; - } - } - - select { - transition: background 2s ease-out; - - &.highlight-changes { - background: $highlight-changes-color; - transition: none; - } - } - .help-block { margin-bottom: 10px; } @@ -90,6 +55,162 @@ } } +.toggle-wrapper { + margin-top: 5px; +} + +.project-feature-row > .toggle-wrapper { + margin: 10px 0; +} + +.project-visibility-setting, +.project-feature-settings { + border: 1px solid $border-color; + padding: 10px 32px; + + @media (max-width: $screen-xs-min) { + padding: 10px 20px; + } +} + +.project-visibility-setting .request-access { + line-height: 2; +} + +.project-feature-settings { + background: $gray-lighter; + border-top: none; + margin-bottom: 16px; +} + +.project-repo-select { + transition: background 2s ease-out; + + &:disabled { + opacity: 0.75; + } + + .highlight-changes & { + background: $highlight-changes-color; + transition: none; + } +} + +.project-feature-controls { + display: flex; + align-items: center; + margin: 8px 0; + max-width: 432px; + + .toggle-wrapper { + flex: 0; + margin-right: 10px; + } + + .select-wrapper { + flex: 1; + } +} + +.project-feature-setting-group { + padding-left: 32px; + + .project-feature-controls { + max-width: 400px; + } + + @media (max-width: $screen-xs-min) { + padding-left: 20px; + } +} + +.project-feature-toggle { + position: relative; + border: none; + outline: 0; + display: block; + width: 100px; + height: 24px; + cursor: pointer; + user-select: none; + background: $feature-toggle-color-disabled; + border-radius: 12px; + padding: 3px; + transition: all .4s ease; + + &::selection, + &::before::selection, + &::after::selection { + background: none; + } + + &::before { + color: $feature-toggle-text-color; + font-size: 12px; + line-height: 24px; + position: absolute; + top: 0; + left: 25px; + right: 5px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + animation: animate-disabled .2s ease-in; + content: attr(data-disabled-text); + } + + &::after { + position: relative; + display: block; + content: ""; + width: 22px; + height: 18px; + left: 0; + border-radius: 9px; + background: $feature-toggle-color; + transition: all .2s ease; + } + + &.checked { + background: $feature-toggle-color-enabled; + + &::before { + left: 5px; + right: 25px; + animation: animate-enabled .2s ease-in; + content: attr(data-enabled-text); + } + + &::after { + left: calc(100% - 22px); + } + } + + &.disabled { + opacity: 0.4; + cursor: not-allowed; + } + + @media (max-width: $screen-xs-min) { + width: 50px; + + &::before, + &.checked::before { + display: none; + } + } + + @keyframes animate-enabled { + 0%, 35% { opacity: 0; } + 100% { opacity: 1; } + } + + @keyframes animate-disabled { + 0%, 35% { opacity: 0; } + 100% { opacity: 1; } + } +} + .project-home-panel, .group-home-panel { padding-top: 24px; diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 51c625ede4b65093ea0308399d0a3ef519fccc1d..c0114dd025641c1b1b90afb8061019c8d78905af 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -545,6 +545,43 @@ module ProjectsHelper current_application_settings.restricted_visibility_levels || [] end + def project_permissions_settings(project) + feature = project.project_feature + { + visibilityLevel: project.visibility_level, + requestAccessEnabled: !!project.request_access_enabled, + issuesAccessLevel: feature.issues_access_level, + repositoryAccessLevel: feature.repository_access_level, + mergeRequestsAccessLevel: feature.merge_requests_access_level, + buildsAccessLevel: feature.builds_access_level, + wikiAccessLevel: feature.wiki_access_level, + snippetsAccessLevel: feature.snippets_access_level, + containerRegistryEnabled: !!project.container_registry_enabled, + lfsEnabled: !!project.lfs_enabled + } + end + + def project_permissions_panel_data(project) + data = { + currentSettings: project_permissions_settings(project), + canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), + allowedVisibilityOptions: project_allowed_visibility_levels(project), + visibilityHelpPath: help_page_path('public_access/public_access'), + registryAvailable: Gitlab.config.registry.enabled, + registryHelpPath: help_page_path('user/project/container_registry'), + lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?, + lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + } + + data.to_json.html_safe + end + + def project_allowed_visibility_levels(project) + Gitlab::VisibilityLevel.values.select do |level| + project.visibility_level_allowed?(level) && !restricted_levels.include?(level) + end + end + def find_file_path return unless @project && !@project.empty_repo? diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index e3effe45d6c7367ad6d08da259644822af1c262d..1dd8778f8007c2d93774ec5390c7cbc1c81137dd 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -1,7 +1,7 @@ - form = local_assigns.fetch(:form) .form-group - .checkbox.builds-feature + .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) } = form.label :only_allow_merge_if_pipeline_succeeds do = form.check_box :only_allow_merge_if_pipeline_succeeds %strong Only allow merge requests to be merged if the pipeline succeeds diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 994119051d273f7ee7819e57f4cebd7e3064b3e9..0a3045604f4a1b3cd3c5b6d0aa6623e78b7dbe94 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -66,90 +66,18 @@ %section.settings.sharing-permissions .settings-header %h4 - Sharing and permissions + Permissions %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p Enable or disable certain project features and choose access levels. .settings-content.no-animate{ class: ('expanded' if expanded) } = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| - .form_group.sharing-and-permissions - .row.js-visibility-select - .col-md-8 - .label-light - = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - %span.help-block - .col-md-4.visibility-select-container - = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) - = f.fields_for :project_feature do |feature_fields| - %fieldset.features - .row - .col-md-8.project-feature - = feature_fields.label :repository_access_level, "Repository", class: 'label-light' - %span.help-block View and edit files in this project - .col-md-4.js-repo-access-level - = project_feature_access_select(:repository_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-4 - = project_feature_access_select(:merge_requests_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' - %span.help-block Build, test, and deploy your changes - .col-md-4 - = project_feature_access_select(:builds_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-4 - = project_feature_access_select(:snippets_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-4 - = project_feature_access_select(:issues_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-4 - = project_feature_access_select(:wiki_access_level) - .form-group - = render 'shared/allow_request_access', form: f - - if Gitlab.config.lfs.enabled && current_user.admin? - .row.js-lfs-enabled.form-group.sharing-and-permissions - .col-md-8 - = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light' - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - %span.help-block Manages large files such as audio, video and graphics files. - .col-md-4 - .select-wrapper - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } - = icon('chevron-down') - - if Gitlab.config.registry.enabled - .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } - .checkbox - = f.label :container_registry_enabled do - = f.check_box :container_registry_enabled - %strong Container Registry - %br - %span.descr Enable Container Registry for this project - = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' + %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project) + .js-project-permissions-form = f.submit 'Save changes', class: "btn btn-save" - - %section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } + %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) } .settings-header %h4 Merge request settings diff --git a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..fde70c47ca654630c57a5b561f75d9ae6a649aea --- /dev/null +++ b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml @@ -0,0 +1,5 @@ +--- +title: Redesign project feature permissions settings +merge_request: 14062 +author: +type: changed diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 0a89c1baf206b5967c9aef7ef1a304172ccf0d52..3a762be8f1f5df290fd3b505ba64cbe24dbb10ba 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -6,7 +6,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'change project settings' do fill_in 'project_name_edit', with: 'NewName' - select 'Disabled', from: 'project_project_feature_attributes_issues_access_level' end step 'I save project' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 605c9a3ab715294af5c70b87c0890b44a70c1679..96cc0745e9763286b15c29c13bb1efb90f6efc31 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -89,7 +89,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_project_path(@project) expect(page).to have_content("Project name") - expect(page).to have_content("Sharing and permissions") + expect(page).to have_content("Permissions") end def current_project diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb index d3b1d1f7be3b9a786bbd348d5e23d7c9dc9b8f94..17f914c9c17b83d0994ecb1ed710f6cb06fd574d 100644 --- a/spec/features/projects/edit_spec.rb +++ b/spec/features/projects/edit_spec.rb @@ -1,20 +1,21 @@ require 'rails_helper' feature 'Project edit', js: true do + let(:admin) { create(:admin) } let(:user) { create(:user) } let(:project) { create(:project) } - before do - project.team << [user, :master] - sign_in(user) + context 'feature visibility' do + before do + project.team << [user, :master] + sign_in(user) - visit edit_project_path(project) - end + visit edit_project_path(project) + end - context 'feature visibility' do context 'merge requests select' do it 'hides merge requests section' do - select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') + find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click expect(page).to have_selector('.merge-requests-feature', visible: false) end @@ -30,7 +31,7 @@ feature 'Project edit', js: true do context 'builds select' do it 'hides builds select section' do - select('Disabled', from: 'project_project_feature_attributes_builds_access_level') + find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click expect(page).to have_selector('.builds-feature', visible: false) end @@ -44,4 +45,18 @@ feature 'Project edit', js: true do end end end + + context 'LFS enabled setting' do + before do + sign_in(admin) + end + + it 'displays the correct elements' do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + visit edit_project_path(project) + + expect(page).to have_content('Git Large File Storage') + expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true) + end + end end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 2469162906330ecb54afa260ce668497f421af2d..57722276d79e5b9675b42fa65b5b5aef2b7becc8 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -19,21 +19,16 @@ describe 'Edit Project Settings' do it 'toggles visibility' do visit edit_project_path(project) - select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level" + # disable by clicking toggle + toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do click_button 'Save changes' end wait_for_requests expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") - select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" - page.within('.sharing-permissions') do - click_button 'Save changes' - end - wait_for_requests - expect(page).to have_selector(".shortcuts-#{shortcut_name}") - - select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level" + # re-enable by clicking toggle again + toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do click_button 'Save changes' end @@ -176,19 +171,19 @@ describe 'Edit Project Settings' do end it "disables repository related features" do - select "Disabled", from: "project_project_feature_attributes_repository_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') page.within('.sharing-permissions') do click_button "Save changes" end - expect(find(".sharing-permissions")).to have_selector("select.disabled", count: 2) + expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2) end it "shows empty features project homepage" do - select "Disabled", from: "project_project_feature_attributes_repository_access_level" - select "Disabled", from: "project_project_feature_attributes_issues_access_level" - select "Disabled", from: "project_project_feature_attributes_wiki_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') + toggle_feature_off('project[project_feature_attributes][issues_access_level]') + toggle_feature_off('project[project_feature_attributes][wiki_access_level]') page.within('.sharing-permissions') do click_button "Save changes" @@ -201,9 +196,9 @@ describe 'Edit Project Settings' do end it "hides project activity tabs" do - select "Disabled", from: "project_project_feature_attributes_repository_access_level" - select "Disabled", from: "project_project_feature_attributes_issues_access_level" - select "Disabled", from: "project_project_feature_attributes_wiki_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') + toggle_feature_off('project[project_feature_attributes][issues_access_level]') + toggle_feature_off('project[project_feature_attributes][wiki_access_level]') page.within('.sharing-permissions') do click_button "Save changes" @@ -222,7 +217,7 @@ describe 'Edit Project Settings' do # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272 it "hides comments activity tab only on disabled issues, merge requests and repository" do - select "Disabled", from: "project_project_feature_attributes_issues_access_level" + toggle_feature_off('project[project_feature_attributes][issues_access_level]') save_changes_and_check_activity_tab do expect(page).to have_content("Comments") @@ -230,7 +225,7 @@ describe 'Edit Project Settings' do visit edit_project_path(project) - select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level" + toggle_feature_off('project[project_feature_attributes][merge_requests_access_level]') save_changes_and_check_activity_tab do expect(page).to have_content("Comments") @@ -238,7 +233,7 @@ describe 'Edit Project Settings' do visit edit_project_path(project) - select "Disabled", from: "project_project_feature_attributes_repository_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') save_changes_and_check_activity_tab do expect(page).not_to have_content("Comments") @@ -275,4 +270,12 @@ describe 'Edit Project Settings' do expect(page).not_to have_selector('.project-stats') end end + + def toggle_feature_off(feature_name) + find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click + end + + def toggle_feature_on(feature_name) + find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click + end end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 104ce08d9f3cef1694f82a7c2bde13eca77c92aa..b1ec556bf1651720d3a9b2dcc22e751e97f6552b 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -19,8 +19,8 @@ feature 'Project settings > Merge Requests', :js do expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') - select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" within('.sharing-permissions-form') do + find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click click_on('Save changes') end @@ -39,8 +39,8 @@ feature 'Project settings > Merge Requests', :js do expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') - select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" within('.sharing-permissions-form') do + find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click click_on('Save changes') end @@ -60,8 +60,8 @@ feature 'Project settings > Merge Requests', :js do expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') - select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" within('.sharing-permissions-form') do + find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click click_on('Save changes') end diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index 1756c7d00fe95a2901152e84ee10575ef3c3c5ae..37ee6255bd1420e5226c377634f27e9e9e5499f0 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -11,19 +11,19 @@ feature 'Visibility settings', js: true do end scenario 'project visibility select is available' do - visibility_select_container = find('.js-visibility-select') + visibility_select_container = find('.project-visibility-setting') - expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s - expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.' + expect(visibility_select_container.find('select').value).to eq project.visibility_level.to_s + expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.' end scenario 'project visibility description updates on change' do - visibility_select_container = find('.js-visibility-select') - visibility_select = visibility_select_container.find('.visibility-select') + visibility_select_container = find('.project-visibility-setting') + visibility_select = visibility_select_container.find('select') visibility_select.select('Private') expect(visibility_select.value).to eq '0' - expect(visibility_select_container).to have_content 'Project access must be granted explicitly to each user.' + expect(visibility_select_container).to have_content 'Access must be granted explicitly to each user.' end end @@ -37,11 +37,10 @@ feature 'Visibility settings', js: true do end scenario 'project visibility is locked' do - visibility_select_container = find('.js-visibility-select') + visibility_select_container = find('.project-visibility-setting') - expect(visibility_select_container).not_to have_select '.visibility-select' - expect(visibility_select_container).to have_content 'Public' - expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.' + expect(visibility_select_container).to have_selector 'select[name="project[visibility_level]"]:disabled' + expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.' end end end diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb index c139862974916f7bb6d7c5552bf5d8f7bdecb3ce..5c6b2e4b04251e446098c6742682c2b3e1c39f44 100644 --- a/spec/views/projects/edit.html.haml_spec.rb +++ b/spec/views/projects/edit.html.haml_spec.rb @@ -15,17 +15,6 @@ describe 'projects/edit' do current_application_settings: Gitlab::CurrentSettings.current_application_settings) end - context 'LFS enabled setting' do - it 'displays the correct elements' do - allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) - - render - - expect(rendered).to have_select('project_lfs_enabled') - expect(rendered).to have_content('Git Large File Storage') - end - end - context 'project export disabled' do it 'does not display the project export option' do stub_application_setting(project_export_enabled?: false)