Commit 03b14b48 authored by Mike Greiling's avatar Mike Greiling Committed by Phil Hughes
Browse files

Resolve "Make project and features visibility settings less confusing"

parent c763efd25a92
......@@ -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();
......
<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;
$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
*/
......
......@@ -10,41 +10,6 @@
.edit-project,
.import-project {