diff options
17 files changed, 278 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 diff --git a/changelogs/unreleased/32458-update-group-creation-url-so-it-is-always-unique-and-does-not-gener.yml b/changelogs/unreleased/32458-update-group-creation-url-so-it-is-always-unique-and-does-not-gener.yml new file mode 100644 index 00000000000..53a7331b4d9 --- /dev/null +++ b/changelogs/unreleased/32458-update-group-creation-url-so-it-is-always-unique-and-does-not-gener.yml @@ -0,0 +1,5 @@ +--- +title: New group path uniqueness check +merge_request: 17394 +author: +type: added diff --git a/changelogs/unreleased/34624-remove-IIFEs-from-project_select-js.yml b/changelogs/unreleased/34624-remove-IIFEs-from-project_select-js.yml new file mode 100644 index 00000000000..a5acaf7c5dc --- /dev/null +++ b/changelogs/unreleased/34624-remove-IIFEs-from-project_select-js.yml @@ -0,0 +1,5 @@ +--- +title: Remove IIFEs from project_select.js +merge_request: 19288 +author: minghuan lei +type: other diff --git a/changelogs/unreleased/Remove-IIFEs-from-network-js.yml b/changelogs/unreleased/Remove-IIFEs-from-network-js.yml new file mode 100644 index 00000000000..2db5ab239d2 --- /dev/null +++ b/changelogs/unreleased/Remove-IIFEs-from-network-js.yml @@ -0,0 +1,5 @@ +--- +title: Removed IIFEs from network.js file +merge_request: 19254 +author: nuwe1 +type: other diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c08a72813d6..b8db9e69e33 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1517,6 +1517,9 @@ msgstr "" msgid "An error occurred when updating the issue weight" msgstr "" +msgid "An error occurred while checking group path" +msgstr "" + msgid "An error occurred while deleting the approvers group" msgstr "" @@ -1679,6 +1682,9 @@ msgstr "" msgid "An error occurred while updating the comment" msgstr "" +msgid "An error occurred while validating group path" +msgstr "" + msgid "An error occurred while validating username" msgstr "" @@ -3059,6 +3065,9 @@ msgstr "" msgid "Checking branch availability..." msgstr "" +msgid "Checking group path availability..." +msgstr "" + msgid "Checking username availability..." msgstr "" @@ -8359,6 +8368,12 @@ msgstr "" msgid "Group overview content" msgstr "" +msgid "Group path is already taken. Suggestions: " +msgstr "" + +msgid "Group path is available." +msgstr "" + msgid "Group pipeline minutes were successfully reset." msgstr "" @@ -16967,6 +16982,12 @@ msgstr "" msgid "There was an error fetching configuration for charts" msgstr "" +msgid "There was an error fetching cycle analytics stages." +msgstr "" + +msgid "There was an error fetching data for the selected stage" +msgstr "" + msgid "There was an error fetching label data for the selected group" msgstr "" @@ -17009,6 +17030,9 @@ msgstr "" msgid "There was an error while fetching cycle analytics data." msgstr "" +msgid "There was an error while fetching cycle analytics summary data." +msgstr "" + msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." msgstr "" diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index b6f1aef9ce4..deb6dab772e 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -90,6 +90,19 @@ describe('text_utility', () => { }); }); + describe('convertToSnakeCase', () => { + it.each` + txt | result + ${'snakeCase'} | ${'snake_case'} + ${'snake Case'} | ${'snake_case'} + ${'snake case'} | ${'snake_case'} + ${'snake_case'} | ${'snake_case'} + ${'snakeCasesnake Case'} | ${'snake_casesnake_case'} + `('converts string $txt to $result string', ({ txt, result }) => { + expect(textUtils.convertToSnakeCase(txt)).toEqual(result); + }); + }); + describe('convertToSentenceCase', () => { it('converts Sentence Case to Sentence case', () => { expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world'); diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100644..100755 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100644..100755 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |