diff options
author | Phil Hughes <me@iamphill.com> | 2018-01-11 12:38:30 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-01-16 16:45:33 +0000 |
commit | b69d7ac3c9de7b0ba3e60ede70f1de630a53dfe6 (patch) | |
tree | d6169abee8bc535276d3118665b9d15ab649dc43 /app/assets/javascripts/pages/projects/shared | |
parent | f084525fe4cceee1c6c3d86d5bd3150fa6334e42 (diff) | |
download | gitlab-ce-b69d7ac3c9de7b0ba3e60ede70f1de630a53dfe6.tar.gz |
Added dispatcher imports for some project routes
Diffstat (limited to 'app/assets/javascripts/pages/projects/shared')
8 files changed, 696 insertions, 0 deletions
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue new file mode 100644 index 00000000000..9b13b2a524f --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -0,0 +1,111 @@ +<script> + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + + export default { + components: { + projectFeatureToggle, + }, + + model: { + prop: 'value', + event: 'change', + }, + + 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, + }, + }, + + 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; + }, + }, + + 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" + :disabled-input="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/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue new file mode 100644 index 00000000000..25a88f846eb --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -0,0 +1,51 @@ +<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></slot> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue new file mode 100644 index 00000000000..380c6c3ea7d --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -0,0 +1,328 @@ +<script> + import projectFeatureSetting from './project_feature_setting.vue'; + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + import projectSettingRow from './project_setting_row.vue'; + import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; + import { toggleHiddenClassBySelector } from '../external'; + + export default { + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, + + 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, + default: '', + }, + lfsHelpPath: { + type: String, + required: false, + default: '', + }, + registryHelpPath: { + type: String, + required: false, + default: '', + }, + }, + + 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 }; + }, + + 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]; + }, + }, + + 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); + }, + }, + + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, + + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); + }, + }, + }; +</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" + :disabled-input="!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" + :disabled-input="!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" + :disabled-input="!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" + :disabled-input="!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/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js new file mode 100644 index 00000000000..ce47562f259 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/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/pages/projects/shared/permissions/external.js b/app/assets/javascripts/pages/projects/shared/permissions/external.js new file mode 100644 index 00000000000..460af4a2111 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/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/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js new file mode 100644 index 00000000000..dbde8dda634 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/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/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js new file mode 100644 index 00000000000..56627aa155c --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js @@ -0,0 +1,13 @@ +export default function projectAvatar() { + $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() { + const form = $(this).closest('form'); + return form.find('.js-project-avatar-input').click(); + }); + + $('.js-project-avatar-input').bind('change', function onClickAvatarInput() { + const form = $(this).closest('form'); + // eslint-disable-next-line no-useless-escape + const filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-avatar-filename').text(filename); + }); +} diff --git a/app/assets/javascripts/pages/projects/shared/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js new file mode 100644 index 00000000000..86faba0b910 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/project_new.js @@ -0,0 +1,151 @@ +/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ + +import VisibilitySelect from '../../../visibility_select'; + +function highlightChanges($elm) { + $elm.addClass('highlight-changes'); + setTimeout(() => $elm.removeClass('highlight-changes'), 10); +} + +export default class ProjectNew { + constructor() { + this.toggleSettings = this.toggleSettings.bind(this); + this.$selects = $('.features select'); + this.$repoSelects = this.$selects.filter('.js-repo-select'); + this.$projectSelects = this.$selects.not('.js-repo-select'); + + $('.project-edit-container').on('ajax:before', () => { + $('.project-edit-container').hide(); + return $('.save-project-loader').show(); + }); + + this.initVisibilitySelect(); + + this.toggleSettings(); + this.toggleSettingsOnclick(); + this.toggleRepoVisibility(); + } + + initVisibilitySelect() { + const visibilityContainer = document.querySelector('.js-visibility-select'); + if (!visibilityContainer) return; + const visibilitySelect = new VisibilitySelect(visibilityContainer); + visibilitySelect.init(); + + const $visibilitySelect = $(visibilityContainer).find('select'); + let projectVisibility = $visibilitySelect.val(); + const PROJECT_VISIBILITY_PRIVATE = '0'; + + $visibilitySelect.on('change', () => { + const newProjectVisibility = $visibilitySelect.val(); + + if (projectVisibility !== newProjectVisibility) { + this.$projectSelects.each((idx, select) => { + const $select = $(select); + const $options = $select.find('option'); + const values = $.map($options, e => e.value); + + // if switched to "private", limit visibility options + if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { + if ($select.val() !== values[0] && $select.val() !== values[1]) { + $select.val(values[1]).trigger('change'); + highlightChanges($select); + } + $options.slice(2).disable(); + } + + // if switched from "private", increase visibility for non-disabled options + if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { + $options.enable(); + if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { + $select.val(values[values.length - 1]).trigger('change'); + highlightChanges($select); + } + } + }); + + projectVisibility = newProjectVisibility; + } + }); + } + + toggleSettings() { + this.$selects.each(function () { + var $select = $(this); + var className = $select.data('field') + .replace(/_/g, '-') + .replace('access-level', 'feature'); + ProjectNew._showOrHide($select, '.' + className); + }); + } + + toggleSettingsOnclick() { + this.$selects.on('change', this.toggleSettings); + } + + static _showOrHide(checkElement, container) { + const $container = $(container); + + if ($(checkElement).val() !== '0') { + return $container.show(); + } + return $container.hide(); + } + + toggleRepoVisibility() { + var $repoAccessLevel = $('.js-repo-access-level select'); + var $lfsEnabledOption = $('.js-lfs-enabled select'); + var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; + var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") + .nextAll() + .hide(); + + $repoAccessLevel + .off('change') + .on('change', function () { + var selectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.each(function () { + var $this = $(this); + var repoSelectVal = parseInt($this.val(), 10); + + $this.find('option').enable(); + + if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { + $this.val(selectedVal).trigger('change'); + highlightChanges($this); + } + + $this.find("option[value='" + selectedVal + "']").nextAll().disable(); + }); + + if (selectedVal) { + this.$repoSelects.removeClass('disabled'); + + if ($lfsEnabledOption.length) { + $lfsEnabledOption.removeClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = ''; + } + } else { + this.$repoSelects.addClass('disabled'); + + if ($lfsEnabledOption.length) { + $lfsEnabledOption.val('false').addClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = 'none'; + containerRegistryCheckbox.checked = false; + } + } + + prevSelectedVal = selectedVal; + }.bind(this)); + } +} |