diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-06 00:06:13 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-06 00:06:13 +0000 |
commit | b66baea97dfb652e8a143e409ab44bbd38c856f1 (patch) | |
tree | 28d3033719035cfe3dcd8b4e40ad193674185cc1 /app | |
parent | 82cef8dd1f48ffbc7aaa1ff7374cdb859137e01e (diff) | |
download | gitlab-ce-b66baea97dfb652e8a143e409ab44bbd38c856f1.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/group.js | 24 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_utility.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js | 7 | ||||
-rw-r--r-- | app/assets/javascripts/pages/groups/new/group_path_validator.js | 91 | ||||
-rw-r--r-- | app/assets/javascripts/pages/groups/new/index.js | 6 | ||||
-rw-r--r-- | app/assets/javascripts/pages/projects/network/network.js | 31 | ||||
-rw-r--r-- | app/assets/javascripts/project_select.js | 116 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/common.scss | 7 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/variables.scss | 6 | ||||
-rw-r--r-- | app/views/shared/_group_form.html.haml | 7 |
10 files changed, 226 insertions, 77 deletions
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js index 460174caf4d..eda0f5d1d23 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -1,15 +1,23 @@ import $ from 'jquery'; import { slugify } from './lib/utils/text_utility'; +import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability'; +import flash from '~/flash'; +import { __ } from '~/locale'; export default class Group { constructor() { this.groupPath = $('#group_path'); this.groupName = $('#group_name'); + this.parentId = $('#group_parent_id'); this.updateHandler = this.update.bind(this); this.resetHandler = this.reset.bind(this); + this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this); if (this.groupName.val() === '') { this.groupName.on('keyup', this.updateHandler); this.groupPath.on('keydown', this.resetHandler); + if (!this.parentId.val()) { + this.groupName.on('blur', this.updateGroupPathSlugHandler); + } } } @@ -21,5 +29,21 @@ export default class Group { reset() { this.groupName.off('keyup', this.updateHandler); this.groupPath.off('keydown', this.resetHandler); + this.groupName.off('blur', this.checkPathHandler); + } + + updateGroupPathSlug() { + const slug = this.groupPath.val() || slugify(this.groupName.val()); + if (!slug) return; + + fetchGroupPathAvailability(slug) + .then(({ data }) => data) + .then(data => { + if (data.exists && data.suggests.length > 0) { + const suggestedSlug = data.suggests[0]; + this.groupPath.val(suggestedSlug); + } + }) + .catch(() => flash(__('An error occurred while checking group path'))); } } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index d13fbeb5fc7..b839a994455 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -139,6 +139,14 @@ export const stripHtml = (string, replace = '') => { export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase()); /** + * Converts camelCase string to snake_case + * + * @param {*} string + */ +export const convertToSnakeCase = string => + slugifyWithUnderscore(string.match(/([a-zA-Z][^A-Z]*)/g).join(' ')); + +/** * Converts a sentence to lower case from the second word onwards * e.g. Hello World => Hello world * diff --git a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js new file mode 100644 index 00000000000..1d68ccd724d --- /dev/null +++ b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js @@ -0,0 +1,7 @@ +import axios from '~/lib/utils/axios_utils'; + +const rootUrl = gon.relative_url_root; + +export default function fetchGroupPathAvailability(groupPath) { + return axios.get(`${rootUrl}/users/${groupPath}/suggests`); +} diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js new file mode 100644 index 00000000000..2021ad117e8 --- /dev/null +++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js @@ -0,0 +1,91 @@ +import InputValidator from '~/validators/input_validator'; + +import _ from 'underscore'; +import fetchGroupPathAvailability from './fetch_group_path_availability'; +import flash from '~/flash'; +import { __ } from '~/locale'; + +const debounceTimeoutDuration = 1000; +const invalidInputClass = 'gl-field-error-outline'; +const successInputClass = 'gl-field-success-outline'; +const successMessageSelector = '.validation-success'; +const pendingMessageSelector = '.validation-pending'; +const unavailableMessageSelector = '.validation-error'; +const suggestionsMessageSelector = '.gl-path-suggestions'; + +export default class GroupPathValidator extends InputValidator { + constructor(opts = {}) { + super(); + + const container = opts.container || ''; + const validateElements = document.querySelectorAll(`${container} .js-validate-group-path`); + + this.debounceValidateInput = _.debounce(inputDomElement => { + GroupPathValidator.validateGroupPathInput(inputDomElement); + }, debounceTimeoutDuration); + + validateElements.forEach(element => + element.addEventListener('input', this.eventHandler.bind(this)), + ); + } + + eventHandler(event) { + const inputDomElement = event.target; + + GroupPathValidator.resetInputState(inputDomElement); + this.debounceValidateInput(inputDomElement); + } + + static validateGroupPathInput(inputDomElement) { + const groupPath = inputDomElement.value; + + if (inputDomElement.checkValidity() && groupPath.length > 0) { + GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector); + + fetchGroupPathAvailability(groupPath) + .then(({ data }) => data) + .then(data => { + GroupPathValidator.setInputState(inputDomElement, !data.exists); + GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector, false); + GroupPathValidator.setMessageVisibility( + inputDomElement, + data.exists ? unavailableMessageSelector : successMessageSelector, + ); + + if (data.exists) { + GroupPathValidator.showSuggestions(inputDomElement, data.suggests); + } + }) + .catch(() => flash(__('An error occurred while validating group path'))); + } + } + + static showSuggestions(inputDomElement, suggestions) { + const messageElement = inputDomElement.parentElement.parentElement.querySelector( + suggestionsMessageSelector, + ); + const textSuggestions = suggestions && suggestions.length > 0 ? suggestions.join(', ') : 'none'; + messageElement.textContent = textSuggestions; + } + + static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) { + const messageElement = inputDomElement.parentElement.parentElement.querySelector( + messageSelector, + ); + messageElement.classList.toggle('hide', !isVisible); + } + + static setInputState(inputDomElement, success = true) { + inputDomElement.classList.toggle(successInputClass, success); + inputDomElement.classList.toggle(invalidInputClass, !success); + } + + static resetInputState(inputDomElement) { + GroupPathValidator.setMessageVisibility(inputDomElement, successMessageSelector, false); + GroupPathValidator.setMessageVisibility(inputDomElement, unavailableMessageSelector, false); + + if (inputDomElement.checkValidity()) { + inputDomElement.classList.remove(successInputClass, invalidInputClass); + } + } +} diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index 57b53eb9e5d..0710fefe70c 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -1,8 +1,14 @@ +import $ from 'jquery'; import BindInOut from '~/behaviors/bind_in_out'; import Group from '~/group'; import initAvatarPicker from '~/avatar_picker'; +import GroupPathValidator from './group_path_validator'; document.addEventListener('DOMContentLoaded', () => { + const parentId = $('#group_parent_id'); + if (!parentId.val()) { + new GroupPathValidator(); // eslint-disable-line no-new + } BindInOut.initAll(); new Group(); // eslint-disable-line no-new initAvatarPicker(); diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index 43417fa9702..5f2014f1631 100644 --- a/app/assets/javascripts/pages/projects/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -1,22 +1,19 @@ -/* eslint-disable func-names, no-var */ - import $ from 'jquery'; import BranchGraph from '../../../network/branch_graph'; -export default (function() { - function Network(opts) { - var vph; - $('#filter_ref').click(function() { - return $(this) - .closest('form') - .submit(); - }); - this.branch_graph = new BranchGraph($('.network-graph'), opts); - vph = $(window).height() - 250; - $('.network-graph').css({ - height: `${vph}px`, - }); +const vph = $(window).height() - 250; + +export default class Network { + constructor(opts) { + this.opts = opts; + this.filter_ref = $('#filter_ref'); + this.network_graph = $('.network-graph'); + this.filter_ref.click(() => this.submit()); + this.branch_graph = new BranchGraph(this.network_graph, this.opts); + this.network_graph.css({ height: `${vph}px` }); } - return Network; -})(); + submit() { + return this.filter_ref.closest('form').submit(); + } +} diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 81f2fc2e686..6a32cef79bc 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -30,67 +30,65 @@ const projectSelect = () => { $(select).select2({ placeholder, minimumInputLength: 0, - query: (function(_this) { - return function(query) { - var finalCallback, projectsCallback; - finalCallback = function(projects) { - var data; - data = { - results: projects, - }; - return query.callback(data); + query: query => { + var finalCallback, projectsCallback; + finalCallback = function(projects) { + var data; + data = { + results: projects, }; - if (_this.includeGroups) { - projectsCallback = function(projects) { - var groupsCallback; - groupsCallback = function(groups) { - var data; - data = groups.concat(projects); - return finalCallback(data); - }; - return Api.groups(query.term, {}, groupsCallback); - }; - } else { - projectsCallback = finalCallback; - } - if (_this.groupId) { - return Api.groupProjects( - _this.groupId, - query.term, - { - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - with_shared: _this.withShared, - include_subgroups: _this.includeProjectsInSubgroups, - }, - projectsCallback, - ); - } else if (_this.userId) { - return Api.userProjects( - _this.userId, - query.term, - { - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - with_shared: _this.withShared, - include_subgroups: _this.includeProjectsInSubgroups, - }, - projectsCallback, - ); - } else { - return Api.projects( - query.term, - { - order_by: _this.orderBy, - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - membership: !_this.allProjects, - }, - projectsCallback, - ); - } + return query.callback(data); }; - })(this), + if (this.includeGroups) { + projectsCallback = function(projects) { + var groupsCallback; + groupsCallback = function(groups) { + var data; + data = groups.concat(projects); + return finalCallback(data); + }; + return Api.groups(query.term, {}, groupsCallback); + }; + } else { + projectsCallback = finalCallback; + } + if (this.groupId) { + return Api.groupProjects( + this.groupId, + query.term, + { + with_issues_enabled: this.withIssuesEnabled, + with_merge_requests_enabled: this.withMergeRequestsEnabled, + with_shared: this.withShared, + include_subgroups: this.includeProjectsInSubgroups, + }, + projectsCallback, + ); + } else if (this.userId) { + return Api.userProjects( + this.userId, + query.term, + { + with_issues_enabled: this.withIssuesEnabled, + with_merge_requests_enabled: this.withMergeRequestsEnabled, + with_shared: this.withShared, + include_subgroups: this.includeProjectsInSubgroups, + }, + projectsCallback, + ); + } else { + return Api.projects( + query.term, + { + order_by: this.orderBy, + with_issues_enabled: this.withIssuesEnabled, + with_merge_requests_enabled: this.withMergeRequestsEnabled, + membership: !this.allProjects, + }, + projectsCallback, + ); + } + }, id(project) { if (simpleFilter) return project.id; return JSON.stringify({ diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 4b89a2f2b04..c2f6aa47c47 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -563,3 +563,10 @@ img.emoji { .gl-font-size-small { font-size: $gl-font-size-small; } .gl-line-height-24 { line-height: $gl-line-height-24; } + +.gl-font-size-12 { font-size: $gl-font-size-12; } +.gl-font-size-14 { font-size: $gl-font-size-14; } +.gl-font-size-16 { font-size: $gl-font-size-16; } +.gl-font-size-20 { font-size: $gl-font-size-20; } +.gl-font-size-28 { font-size: $gl-font-size-28; } +.gl-font-size-42 { font-size: $gl-font-size-42; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index a7d94281008..0f77c451fac 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -325,6 +325,12 @@ $gl-grayish-blue: #7f8fa4; $gl-gray-dark: #313236; $gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; +$gl-font-size-12: 12px; +$gl-font-size-14: 14px; +$gl-font-size-16: 16px; +$gl-font-size-20: 20px; +$gl-font-size-28: 28px; +$gl-font-size-42: 42px; $type-scale: ( 1: 12px, diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 959792718ca..9a65981ed58 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -22,11 +22,16 @@ - if parent %strong= parent.full_path + '/' = f.hidden_field :parent_id - = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control', + = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control js-validate-group-path', autofocus: local_assigns[:autofocus] || false, required: true, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, title: _('Please choose a group URL with no special characters.'), "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}" + %p.validation-error.gl-field-error.field-validation.hide + = _('Group path is already taken. Suggestions: ') + %span.gl-path-suggestions + %p.validation-success.gl-field-success.field-validation.hide= _('Group path is available.') + %p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking group path availability...') - if @group.persisted? .alert.alert-warning.prepend-top-10 |