summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Greiling <mgreiling@gitlab.com>2017-09-07 18:07:40 +0000
committerPhil Hughes <me@iamphill.com>2017-09-07 18:07:40 +0000
commit03b14b48a07e3bcab53b273bbeccdba1ef921ac2 (patch)
treef41b966fa0ea4e2b279be894ba9c59578e94e1fc
parent21b16c5a168ebd79c6a5bcb9633752ecf4958089 (diff)
downloadgitlab-ce-03b14b48a07e3bcab53b273bbeccdba1ef921ac2.tar.gz
Resolve "Make project and features visibility settings less confusing"
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_setting.vue104
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue51
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_setting_row.vue36
-rw-r--r--app/assets/javascripts/projects/permissions/components/settings_panel.vue312
-rw-r--r--app/assets/javascripts/projects/permissions/constants.js11
-rw-r--r--app/assets/javascripts/projects/permissions/external.js18
-rw-r--r--app/assets/javascripts/projects/permissions/index.js13
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/projects.scss191
-rw-r--r--app/helpers/projects_helper.rb37
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml2
-rw-r--r--app/views/projects/edit.html.haml80
-rw-r--r--changelogs/unreleased/32665-refactor-project-visibility-settings.yml5
-rw-r--r--features/steps/project/project.rb1
-rw-r--r--features/steps/shared/project.rb2
-rw-r--r--spec/features/projects/edit_spec.rb31
-rw-r--r--spec/features/projects/features_visibility_spec.rb43
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb19
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb11
21 files changed, 815 insertions, 166 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 84b5f007282..f3b537c83e2 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 00000000000..80c5d39f736
--- /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 00000000000..2403c60186a
--- /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 00000000000..6140d74fea8
--- /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 00000000000..326d9105666
--- /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 00000000000..ce47562f259
--- /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 00000000000..460af4a2111
--- /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 00000000000..dbde8dda634
--- /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 becdd7ff35b..3857226cddb 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 dd600a27545..94e4f4334d4 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 51c625ede4b..c0114dd0256 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 e3effe45d6c..1dd8778f800 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 994119051d2..0a3045604f4 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 00000000000..fde70c47ca6
--- /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 0a89c1baf20..3a762be8f1f 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 605c9a3ab71..96cc0745e97 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 d3b1d1f7be3..17f914c9c17 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 24691629063..57722276d79 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 104ce08d9f3..b1ec556bf16 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 1756c7d00fe..37ee6255bd1 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 c1398629749..5c6b2e4b042 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)