Commit 6a4ebc4a authored by Phil Hughes's avatar Phil Hughes

Merge branch '32665-refactor-project-visibility-settings' into 'master'

Resolve "Make project and features visibility settings less confusing"

Closes #32665

See merge request !14062
parents 21b16c5a 03b14b48
...@@ -577,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -577,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown';
case 'edit': case 'edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new ProjectNew(); new ProjectNew();
import(/* webpackChunkName: 'project_permissions' */ './projects/permissions')
.then(permissions => permissions.default())
.catch(() => {});
break; break;
case 'new': case 'new':
new ProjectNew(); new ProjectNew();
......
<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>
<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>
<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>
<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>
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.',
};
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));
}
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 } }),
});
}
...@@ -579,6 +579,11 @@ $project-breadcrumb-color: #999; ...@@ -579,6 +579,11 @@ $project-breadcrumb-color: #999;
$project-private-forks-notice-odd: $green-600; $project-private-forks-notice-odd: $green-600;
$project-network-controls-color: #888; $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 * Runners
*/ */
......
...@@ -10,41 +10,6 @@ ...@@ -10,41 +10,6 @@
.edit-project, .edit-project,
.import-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 { .help-block {
margin-bottom: 10px; margin-bottom: 10px;
} }
...@@ -90,6 +55,162 @@ ...@@ -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, .project-home-panel,
.group-home-panel { .group-home-panel {
padding-top: 24px; padding-top: 24px;
......
...@@ -545,6 +545,43 @@ module ProjectsHelper ...@@ -545,6 +545,43 @@ module ProjectsHelper
current_application_settings.restricted_visibility_levels || [] current_application_settings.restricted_visibility_levels || []
end 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 def find_file_path
return unless @project && !@project.empty_repo? return unless @project && !@project.empty_repo?
......
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
.form-group .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.label :only_allow_merge_if_pipeline_succeeds do
= form.check_box :only_allow_merge_if_pipeline_succeeds = form.check_box :only_allow_merge_if_pipeline_succeeds
%strong Only allow merge requests to be merged if the pipeline succeeds %strong Only allow merge requests to be merged if the pipeline succeeds
......
...@@ -66,90 +66,18 @@ ...@@ -66,90 +66,18 @@
%section.settings.sharing-permissions %section.settings.sharing-permissions
.settings-header .settings-header
%h4 %h4
Sharing and permissions Permissions
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Enable or disable certain project features and choose access levels. Enable or disable certain project features and choose access levels.
.settings-content.no-animate{ class: ('expanded' if expanded) } .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_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 %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.row.js-visibility-select .js-project-permissions-form
.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'
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
%section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
%section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) }
.settings-header .settings-header
%h4 %h4
Merge request settings Merge request settings
......
---
title: Redesign project feature permissions settings
merge_request: 14062
author:
type: changed
...@@ -6,7 +6,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps ...@@ -6,7 +6,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'change project settings' do step 'change project settings' do
fill_in 'project_name_edit', with: 'NewName' fill_in 'project_name_edit', with: 'NewName'
select 'Disabled', from: 'project_project_feature_attributes_issues_access_level'
end end
step 'I save project' do step 'I save project' do
......
...@@ -89,7 +89,7 @@ module SharedProject ...@@ -89,7 +89,7 @@ module SharedProject
step 'I should see project settings' do step 'I should see project settings' do
expect(current_path).to eq edit_project_path(@project) expect(current_path).to eq edit_project_path(@project)
expect(page).to have_content("Project name") expect(page).to have_content("Project name")
expect(page).to have_content("Sharing and permissions") expect(page).to have_content("Permissions")
end end
def current_project def current_project
......
require 'rails_helper' require 'rails_helper'
feature 'Project edit', js: true do feature 'Project edit', js: true do
let(:admin) { create(:admin) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
before do context 'feature visibility' do
project.team << [user, :master] before do
sign_in(user) project.team << [user, :master]
sign_in(user)
visit edit_project_path(project) visit edit_project_path(project)
end end
context 'feature visibility' do
context 'merge requests select' do context 'merge requests select' do
it 'hides merge requests section' 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) expect(page).to have_selector('.merge-requests-feature', visible: false)
end end
...@@ -30,7 +31,7 @@ feature 'Project edit', js: true do ...@@ -30,7 +31,7 @@ feature 'Project edit', js: true do
context 'builds select' do context 'builds select' do
it 'hides builds select section' 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) expect(page).to have_selector('.builds-feature', visible: false)
end end
...@@ -44,4 +45,18 @@ feature 'Project edit', js: true do ...@@ -44,4 +45,18 @@ feature 'Project edit', js: true do
end end
end 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 end
...@@ -19,21 +19,16 @@ describe 'Edit Project Settings' do ...@@ -19,21 +19,16 @@ describe 'Edit Project Settings' do
it 'toggles visibility' do it 'toggles visibility' do
visit edit_project_path(project) 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 page.within('.sharing-permissions') do
click_button 'Save changes' click_button 'Save changes'
end end
wait_for_requests wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" # re-enable by clicking toggle again
page.within('.sharing-permissions') do toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
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"
page.within('.sharing-permissions') do page.within('.sharing-permissions') do
click_button 'Save changes' click_button 'Save changes'
end end
...@@ -176,19 +171,19 @@ describe 'Edit Project Settings' do ...@@ -176,19 +171,19 @@ describe 'Edit Project Settings' do
end end
it "disables repository related features" do 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 page.within('.sharing-permissions') do
click_button "Save changes" click_button "Save changes"
end 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 end
it "shows empty features project homepage" do it "shows empty features project homepage" do
select "Disabled", from: "project_project_feature_attributes_repository_access_level" toggle_feature_off('project[project_feature_attributes][repository_access_level]')
select "Disabled", from: "project_project_feature_attributes_issues_access_level" toggle_feature_off('project[project_feature_attributes][issues_access_level]')
select "Disabled", from: "project_project_feature_attributes_wiki_access_level" toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
page.within('.sharing-permissions') do page.within('.sharing-permissions') do
click_button "Save changes" click_button "Save changes"
...@@ -201,9 +196,9 @@ describe 'Edit Project Settings' do ...@@ -201,9 +196,9 @@ describe 'Edit Project Settings' do
end end
it "hides project activity tabs" do it "hides project activity tabs" do
select "Disabled", from: "project_project_feature_attributes_repository_access_level" toggle_feature_off('project[project_feature_attributes][repository_access_level]')
select "Disabled", from: "project_project_feature_attributes_issues_access_level" toggle_feature_off('project[project_feature_attributes][issues_access_level]')
select "Disabled", from: "project_project_feature_attributes_wiki_access_level" toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
page.within('.sharing-permissions') do page.within('.sharing-permissions') do
click_button "Save changes" click_button "Save changes"
...@@ -222,7 +217,7 @@ describe 'Edit Project Settings' do ...@@ -222,7 +217,7 @@ describe 'Edit Project Settings' do
# Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272 # 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 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 save_changes_and_check_activity_tab do
expect(page).to have_content("Comments") expect(page).to have_content("Comments")
...@@ -230,7 +225,7 @@ describe 'Edit Project Settings' do ...@@ -230,7 +225,7 @@ describe 'Edit Project Settings' do
visit edit_project_path(project) 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 save_changes_and_check_activity_tab do
expect(page).to have_content("Comments") expect(page).to have_content("Comments")
...@@ -238,7 +233,7 @@ describe 'Edit Project Settings' do ...@@ -238,7 +233,7 @@ describe 'Edit Project Settings' do
visit edit_project_path(project) 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 save_changes_and_check_activity_tab do
expect(page).not_to have_content("Comments") expect(page).not_to have_content("Comments")
...@@ -275,4 +270,12 @@ describe 'Edit Project Settings' do ...@@ -275,4 +270,12 @@ describe 'Edit Project Settings' do
expect(page).not_to have_selector('.project-stats') expect(page).not_to have_selector('.project-stats')
end end
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 end
...@@ -19,8 +19,8 @@ feature 'Project settings > Merge Requests', :js do ...@@ -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 the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') 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 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') click_on('Save changes')
end end
...@@ -39,8 +39,8 @@ feature 'Project settings > Merge Requests', :js do ...@@ -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).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') 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 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') click_on('Save changes')
end end
...@@ -60,8 +60,8 @@ feature 'Project settings > Merge Requests', :js do ...@@ -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 the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') 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 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') click_on('Save changes')
end end
......
...@@ -11,19 +11,19 @@ feature 'Visibility settings', js: true do ...@@ -11,19 +11,19 @@ feature 'Visibility settings', js: true do
end end
scenario 'project visibility select is available' do 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.find('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).to have_content 'The project can be accessed by anyone, regardless of authentication.'
end end
scenario 'project visibility description updates on change' do scenario 'project visibility description updates on change' do
visibility_select_container = find('.js-visibility-select') visibility_select_container = find('.project-visibility-setting')
visibility_select = visibility_select_container.find('.visibility-select') visibility_select = visibility_select_container.find('select')
visibility_select.select('Private') visibility_select.select('Private')
expect(visibility_select.value).to eq '0' 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
end end
...@@ -37,11 +37,10 @@ feature 'Visibility settings', js: true do ...@@ -37,11 +37,10 @@ feature 'Visibility settings', js: true do
end end
scenario 'project visibility is locked' do 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_selector 'select[name="project[visibility_level]"]:disabled'
expect(visibility_select_container).to have_content 'Public' expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.'
expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end end
end end
end end
...@@ -15,17 +15,6 @@ describe 'projects/edit' do ...@@ -15,17 +15,6 @@ describe 'projects/edit' do
current_application_settings: Gitlab::CurrentSettings.current_application_settings) current_application_settings: Gitlab::CurrentSettings.current_application_settings)
end 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 context 'project export disabled' do
it 'does not display the project export option' do it 'does not display the project export option' do
stub_application_setting(project_export_enabled?: false) stub_application_setting(project_export_enabled?: false)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment