diff options
Diffstat (limited to 'app/assets')
32 files changed, 1269 insertions, 169 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 687f09882a7..16c5d0fa344 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => { propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); pipelineTableViewEl.appendChild(table.$el); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index dd751ec97a8..c931e1e0ea5 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -13,6 +13,10 @@ type: String, required: true, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, mixins: [ pipelinesMixin, @@ -95,6 +99,7 @@ <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsHelpPath" /> </div> </div> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 6db0b18ae5a..f3b537c83e2 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -160,6 +160,9 @@ import initChangesDropdown from './init_changes_dropdown'; const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } + if (page === 'projects:merge_requests:index') { + new UserCallout({ setCalloutPerProject: true }); + } const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; IssuableIndex.init(pagePrefix); @@ -342,6 +345,7 @@ import initChangesDropdown from './init_changes_dropdown'; case 'projects:show': shortcut_handler = new ShortcutsNavigation(); new NotificationsForm(); + new UserCallout({ setCalloutPerProject: true }); if ($('#tree-slider').length) new TreeView(); if ($('.blob-viewer').length) new BlobViewer(); @@ -361,6 +365,9 @@ import initChangesDropdown from './init_changes_dropdown'; case 'projects:pipelines:new': new NewBranchForm($('.js-new-pipeline-form')); break; + case 'projects:pipelines:index': + new UserCallout({ setCalloutPerProject: true }); + break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': @@ -418,6 +425,7 @@ import initChangesDropdown from './init_changes_dropdown'; new TreeView(); new BlobViewer(); new NewCommitForm($('.js-create-dir-form')); + new UserCallout({ setCalloutPerProject: true }); $('#tree-slider').waitForImages(function() { gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); @@ -569,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/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3b3620fe61b..0c3c877ff15 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -243,6 +243,7 @@ import bp from './breakpoints'; propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 2ca5ac2912f..f0b44dfa6d8 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -1,29 +1,45 @@ <script> -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + import popover from '../../vue_shared/directives/popover'; -export default { - props: { - pipeline: { - type: Object, - required: true, + export default { + props: { + pipeline: { + type: Object, + required: true, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, - }, - components: { - userAvatarLink, - }, - directives: { - tooltip, - }, - computed: { - user() { - return this.pipeline.user; + components: { + userAvatarLink, }, - }, -}; + directives: { + tooltip, + popover, + }, + computed: { + user() { + return this.pipeline.user; + }, + popoverOptions() { + return { + html: true, + delay: { hide: 600 }, + trigger: 'hover', + placement: 'top', + title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', + content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, + }; + }, + }, + }; </script> <template> - <div class="table-section section-15 hidden-xs hidden-sm"> + <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -57,6 +73,13 @@ export default { :title="pipeline.yaml_errors"> yaml invalid </span> + <a + v-if="pipeline.flags.auto_devops" + class="js-pipeline-url-autodevops label label-info autodevops-badge" + v-popover="popoverOptions" + role="button"> + Auto DevOps + </a> <span v-if="pipeline.flags.stuck" class="js-pipeline-url-stuck label label-warning"> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 010063a0240..5e6d6b2fbdc 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -25,8 +25,8 @@ return { endpoint: pipelinesData.endpoint, - cssClass: pipelinesData.cssClass, helpPagePath: pipelinesData.helpPagePath, + autoDevopsPath: pipelinesData.helpAutoDevopsPath, newPipelinePath: pipelinesData.newPipelinePath, canCreatePipeline: pipelinesData.canCreatePipeline, allPath: pipelinesData.allPath, @@ -139,9 +139,7 @@ }; </script> <template> - <div - class="pipelines-container" - :class="cssClass"> + <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="!isLoading && !shouldRenderEmptyState"> @@ -200,6 +198,7 @@ <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsPath" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 5088d92209f..7aa0c0e8a7f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -17,6 +17,10 @@ required: false, default: false, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, components: { pipelinesTableRowComponent, @@ -54,6 +58,7 @@ :key="model.id" :pipeline="model" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsHelpPath" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index c3f1c426d8a..5b9bb6c3750 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -25,6 +25,10 @@ export default { required: false, default: false, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, components: { asyncButtonComponent, @@ -218,7 +222,10 @@ export default { </div> </div> - <pipeline-url :pipeline="pipeline" /> + <pipeline-url + :pipeline="pipeline" + :auto-devops-help-path="autoDevopsHelpPath" + /> <div class="table-section section-25"> <div 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/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue index fa5efef2919..8d0c29177e6 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue @@ -27,7 +27,7 @@ export default { listEmptyMessage() { return this.searchFailed ? s__('ProjectsDropdown|Something went wrong on our end.') : - s__('ProjectsDropdown|No projects matched your query'); + s__('ProjectsDropdown|Sorry, no projects matched your search'); }, }, }; diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index b71997234e5..53bc76d0f2d 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -53,7 +53,7 @@ export default { class="form-control" ref="search" v-model="searchQuery" - :placeholder="s__('ProjectsDropdown|Search projects')" + :placeholder="s__('ProjectsDropdown|Search your projects')" /> <i v-if="!searchQuery" diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 7fa5996d600..8635ccece6e 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -41,4 +41,8 @@ export default function initSettingsPanels() { $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); }); + + if (location.hash) { + expandSection($(location.hash)); + } } diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index ff2208baeab..a45b22f3084 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,7 +1,11 @@ import Cookies from 'js-cookie'; export default class UserCallout { - constructor(className = 'user-callout') { + constructor(options = {}) { + this.options = options; + + const className = this.options.className || 'user-callout'; + this.userCalloutBody = $(`.${className}`); this.cookieName = this.userCalloutBody.data('uid'); this.isCalloutDismissed = Cookies.get(this.cookieName); @@ -17,7 +21,11 @@ export default class UserCallout { dismissCallout(e) { const $currentTarget = $(e.currentTarget); - Cookies.set(this.cookieName, 'true', { expires: 365 }); + if (this.options.setCalloutPerProject) { + Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('project-path') }); + } else { + Cookies.set(this.cookieName, 'true', { expires: 365 }); + } if ($currentTarget.hasClass('close')) { this.userCalloutBody.remove(); diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js new file mode 100644 index 00000000000..05fa563cbd0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/popover.js @@ -0,0 +1,20 @@ +/** + * Helper to user bootstrap popover in vue.js. + * Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover + * + * @example + * import popover from 'vue_shared/directives/popover.js'; + * { + * directives: [popover] + * } + * <a v-popover="{options}">popover</a> + */ +export default { + bind(el, binding) { + $(el).popover(binding.value); + }, + + unbind(el) { + $(el).popover('destroy'); + }, +}; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c0524bf6aa3..35e7a10379f 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -19,6 +19,7 @@ @import "framework/flash"; @import "framework/forms"; @import "framework/gfm"; +@import "framework/gitlab-theme"; @import "framework/header"; @import "framework/highlight"; @import "framework/issue_box"; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cf557d94bdd..2bcd23a15e6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -734,6 +734,11 @@ overflow: hidden; } +@mixin dropdown-item-hover { + background-color: $dropdown-item-hover-bg; + color: $gl-text-color; +} + // TODO: change global style and remove mixin @mixin new-style-dropdown($selector: '') { #{$selector}.dropdown-menu, @@ -760,6 +765,10 @@ padding: 8px 16px; } + &.droplab-item-active button { + @include dropdown-item-hover; + } + a, button, .menu-item { @@ -779,6 +788,8 @@ &:hover, &:active, &:focus { + @include dropdown-item-hover; + background-color: $dropdown-item-hover-bg; color: $gl-text-color; @@ -837,17 +848,30 @@ } } +@media (max-width: $screen-xs-max) { + .navbar-gitlab { + li.header-projects, + li.header-more, + li.header-new, + li.header-user { + position: static; + } + } + + header.navbar-gitlab .dropdown { + .dropdown-menu, + .dropdown-menu-nav { + width: 100%; + min-width: 100%; + } + } +} + @include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.js-namespace-select + '); header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { padding: 0; - - @media (max-width: $screen-xs-max) { - display: table; - left: -50px; - min-width: 300px; - } } .projects-dropdown-container { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss new file mode 100644 index 00000000000..71f764923ff --- /dev/null +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -0,0 +1,265 @@ +/** + * Styles the GitLab application with a specific color theme + */ + +@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { + // Header + + header.navbar-gitlab-new { + background: linear-gradient(to right, $color-900, $color-800); + + .navbar-collapse { + color: $color-200; + } + + .container-fluid { + .navbar-toggle { + border-left: 1px solid lighten($color-700, 10%); + } + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + background-color: rgba($color-200, .2); + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + svg { + fill: currentColor; + } + } + + &.line-separator { + border-left: 1px solid rgba($color-200, .2); + } + } + } + + .navbar-sub-nav { + color: $color-200; + } + + .nav { + > li { + color: $color-200; + + > a { + svg { + fill: $color-200; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $color-200; + } + } + + &:hover, + &:focus { + @media (min-width: $screen-sm-min) { + background-color: rgba($color-200, .2); + } + + svg { + fill: currentColor; + } + } + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + &:hover { + svg { + fill: $color-900; + } + } + } + + .impersonated-user, + .impersonated-user:hover { + svg { + fill: $color-900; + } + } + } + } + } + + .title { + > a { + &:hover, + &:focus { + background-color: rgba($color-200, .2); + } + } + } + + .search { + form { + background-color: rgba($color-200, .2); + + &:hover { + background-color: rgba($color-200, .3); + } + } + + .location-badge { + color: $color-100; + background-color: rgba($color-200, .1); + border-right: 1px solid $color-800; + } + + .search-input::placeholder { + color: rgba($color-200, .8); + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: rgba($color-200, .8); + } + } + + &.search-active { + form { + background-color: $white-light; + } + + .location-badge { + color: $gl-text-color; + } + + .search-input-wrap { + .search-icon { + color: rgba($color-200, .8); + } + } + } + } + + .btn-sign-in { + background-color: $color-100; + color: $color-900; + } + + + // Sidebar + .nav-sidebar li.active { + box-shadow: inset 4px 0 0 $color-700; + + > a { + color: $color-900; + } + + svg { + fill: $color-900; + } + } +} + + +body { + &.ui_indigo { + @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light); + } + + &.ui_dark { + @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light); + } + + &.ui_blue { + @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light); + } + + &.ui_green { + @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light); + } + + &.ui_light { + @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); + + header.navbar-gitlab-new { + background: $theme-gray-100; + box-shadow: 0 2px 0 0 $border-color; + + .logo-text svg { + fill: $theme-gray-900; + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + color: $theme-gray-900; + } + + &.active > a { + color: $white-light; + + &:hover { + color: $white-light; + } + } + } + } + + .container-fluid { + .navbar-toggle, + .navbar-toggle:hover { + color: $theme-gray-700; + border-left: 1px solid $theme-gray-200; + } + } + } + + .search { + form { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $border-color; + + &:hover { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $blue-100; + + .location-badge { + box-shadow: inset 0 0 0 1px $blue-100; + } + } + } + + .search-input-wrap { + .search-icon { + color: $theme-gray-200; + } + } + + .location-badge { + color: $theme-gray-700; + box-shadow: inset 0 0 0 1px $border-color; + background-color: $nav-badge-bg; + border-right: 0; + } + } + + .nav-sidebar li.active { + > a { + color: $theme-gray-900; + } + + svg { + fill: $theme-gray-900; + } + } + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b00a2d053e2..ab3c34df1fb 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -111,7 +111,6 @@ header { svg { height: 16px; width: 23px; - fill: currentColor; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 88b08998dfd..3857226cddb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -74,6 +74,8 @@ $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; +// GitLab themes + $indigo-50: #f7f7ff; $indigo-100: #ebebfa; $indigo-200: #d1d1f0; @@ -86,6 +88,43 @@ $indigo-800: #393982; $indigo-900: #292961; $indigo-950: #1a1a40; +$theme-gray-50: #fafafa; +$theme-gray-100: #f2f2f2; +$theme-gray-200: #dfdfdf; +$theme-gray-300: #cccccc; +$theme-gray-400: #bababa; +$theme-gray-500: #a7a7a7; +$theme-gray-600: #949494; +$theme-gray-700: #707070; +$theme-gray-800: #4f4f4f; +$theme-gray-900: #2e2e2e; +$theme-gray-950: #1f1f1f; + +$theme-blue-50: #f4f8fc; +$theme-blue-100: #e6edf5; +$theme-blue-200: #c8d7e6; +$theme-blue-300: #97b3cf; +$theme-blue-400: #648cb4; +$theme-blue-500: #4a79a8; +$theme-blue-600: #3e6fa0; +$theme-blue-700: #305c88; +$theme-blue-800: #25496e; +$theme-blue-900: #1a3652; +$theme-blue-950: #0f2235; + +$theme-green-50: #f2faf6; +$theme-green-100: #e4f3ea; +$theme-green-200: #c0dfcd; +$theme-green-300: #8ac2a1; +$theme-green-400: #52a274; +$theme-green-500: #35935c; +$theme-green-600: #288a50; +$theme-green-700: #1c7441; +$theme-green-800: #145d33; +$theme-green-900: #0d4524; +$theme-green-950: #072d16; + + $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); $almost-black: #242424; @@ -540,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/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 2b6c0fc015c..8e095cbdd7e 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -9,10 +9,20 @@ header.navbar-gitlab-new { color: $white-light; - background: linear-gradient(to right, $indigo-900, $indigo-800); border-bottom: 0; min-height: $new-navbar-height; + .logo-text { + line-height: initial; + + svg { + width: 55px; + height: 14px; + margin: 0; + fill: $white-light; + } + } + .header-content { display: -webkit-flex; display: flex; @@ -38,10 +48,10 @@ header.navbar-gitlab-new { img { height: 28px; - margin-right: 10px; + margin-right: 8px; } - > a { + a { display: -webkit-flex; display: flex; align-items: center; @@ -54,22 +64,6 @@ header.navbar-gitlab-new { margin-right: 8px; } } - - .logo-text { - line-height: initial; - - svg { - width: 55px; - height: 14px; - margin: 0; - fill: $white-light; - } - } - - &:hover, - &:focus { - background-color: rgba($indigo-200, .2); - } } } @@ -106,7 +100,6 @@ header.navbar-gitlab-new { .navbar-collapse { padding-left: 0; - color: $indigo-200; box-shadow: 0; @media (max-width: $screen-xs-max) { @@ -132,7 +125,6 @@ header.navbar-gitlab-new { font-size: 14px; text-align: center; color: currentColor; - border-left: 1px solid lighten($indigo-700, 10%); &:hover, &:focus, @@ -167,63 +159,49 @@ header.navbar-gitlab-new { will-change: color; margin: 4px 2px; padding: 6px 8px; - color: $indigo-200; height: 32px; @media (max-width: $screen-xs-max) { padding: 0; } - svg { - fill: $indigo-200; - } - &.header-user-dropdown-toggle { margin-left: 2px; .header-user-avatar { - border-color: $indigo-200; margin-right: 0; } } - } - - .header-new-dropdown-toggle { - margin-right: 0; - } - > a:hover, - > a:focus { - text-decoration: none; - outline: 0; - opacity: 1; - color: $white-light; - - @media (min-width: $screen-sm-min) { - background-color: rgba($indigo-200, .2); - } + &:hover, + &:focus { + text-decoration: none; + outline: 0; + opacity: 1; + color: $white-light; - svg { - fill: currentColor; - } + svg { + fill: currentColor; + } - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $white-light; + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } } } } + .header-new-dropdown-toggle { + margin-right: 0; + } + .impersonated-user, .impersonated-user:hover { margin-right: 1px; background-color: $white-light; border-top-right-radius: 0; border-bottom-right-radius: 0; - - svg { - fill: $indigo-900; - } } .impersonation-btn, @@ -241,8 +219,6 @@ header.navbar-gitlab-new { &.active > a, &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -256,7 +232,6 @@ header.navbar-gitlab-new { display: -webkit-flex; display: flex; margin: 0 0 0 6px; - color: $indigo-200; .dropdown-chevron { position: relative; @@ -274,17 +249,6 @@ header.navbar-gitlab-new { text-decoration: none; outline: 0; color: $white-light; - background-color: rgba($indigo-200, .2); - - svg { - fill: currentColor; - } - } - - &.active > a, - &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -309,7 +273,6 @@ header.navbar-gitlab-new { } &.line-separator { - border-left: 1px solid rgba($indigo-200, .2); margin: 8px; } } @@ -339,17 +302,14 @@ header.navbar-gitlab-new { height: 32px; border: 0; border-radius: $border-radius-default; - background-color: rgba($indigo-200, .2); transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; &:hover { - background-color: rgba($indigo-200, .3); box-shadow: none; } } &.search-active form { - background-color: $white-light; box-shadow: none; .search-input { @@ -377,43 +337,26 @@ header.navbar-gitlab-new { } .search-input::placeholder { - color: rgba($indigo-200, .8); transition: color ease-in-out 0.15s; } .location-badge { font-size: 12px; - color: $indigo-100; - background-color: rgba($indigo-200, .1); - will-change: color; margin: -4px 4px -4px -4px; line-height: 25px; padding: 4px 8px; border-radius: 2px 0 0 2px; - border-right: 1px solid $indigo-800; height: 32px; transition: border-color ease-in-out 0.15s; } - .search-input-wrap { - .search-icon, - .clear-icon { - color: rgba($indigo-200, .8); - } - } - &.search-active { .location-badge { - color: $gl-text-color; background-color: $nav-badge-bg; border-color: $border-color; } .search-input-wrap { - .search-icon { - color: rgba($indigo-200, .8); - } - .clear-icon { color: $white-light; } @@ -517,8 +460,6 @@ header.navbar-gitlab-new { .btn-sign-in { margin-top: 3px; - background-color: $indigo-100; - color: $indigo-900; font-weight: $gl-font-weight-bold; &:hover { diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 3082f728ac8..4bbd30056a9 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -155,16 +155,9 @@ $new-sidebar-collapsed-width: 50px; } li.active { - box-shadow: inset 4px 0 0 $active-border; - > a { - color: $active-color; font-weight: $gl-font-weight-bold; } - - svg { - fill: $active-color; - } } @media (max-width: $screen-xs-max) { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 587a202d6dd..994707422bb 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -226,6 +226,14 @@ vertical-align: baseline; } + a.autodevops-badge { + color: $white-light; + } + + a.autodevops-link { + color: $gl-link-color; + } + .commit-row-description { font-size: 14px; padding: 10px 15px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cb8815e4775..296b6310552 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -202,6 +202,10 @@ .btn-group.open .dropdown-toggle { box-shadow: none; } + + .pipeline-tags .label-container { + white-space: normal; + } } .stage-cell { @@ -932,3 +936,8 @@ button.mini-pipeline-graph-dropdown-toggle { .pipelines-container .top-area .nav-controls > .btn:last-child { float: none; } + +.autodevops-title { + font-weight: $gl-font-weight-normal; + line-height: 1.5; +} diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 305feaacaa1..c197494b152 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,3 +1,67 @@ +@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) { + .one { + background-color: $color-1; + border-top-left-radius: $border-radius-default; + } + + .two { + background-color: $color-2; + border-top-right-radius: $border-radius-default; + } + + .three { + background-color: $color-3; + border-bottom-left-radius: $border-radius-default; + } + + .four { + background-color: $color-4; + border-bottom-right-radius: $border-radius-default; + } +} + +.application-theme { + label { + margin-right: 20px; + text-align: center; + } + + .preview { + font-size: 0; + margin-bottom: 10px; + + &.indigo { + @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500); + } + + &.dark { + @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600); + } + + &.light { + @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100); + } + + &.blue { + @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500); + } + + &.green { + @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500); + } + } + + .preview-row { + display: block; + } + + .quadrant { + display: inline-block; + height: 50px; + width: 80px; + } +} + .syntax-theme { label { margin-right: 20px; 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/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index efc47861768..69abb13add4 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -43,8 +43,10 @@ display: inline-block; } -.blob-viewer[data-type="rich"] { - margin: 20px; +@media (min-width: $screen-md-min) { + .blob-viewer[data-type="rich"] { + margin: 20px; + } } .repository-view { |