summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-06 00:06:13 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-06 00:06:13 +0000
commitb66baea97dfb652e8a143e409ab44bbd38c856f1 (patch)
tree28d3033719035cfe3dcd8b4e40ad193674185cc1 /app
parent82cef8dd1f48ffbc7aaa1ff7374cdb859137e01e (diff)
downloadgitlab-ce-b66baea97dfb652e8a143e409ab44bbd38c856f1.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/group.js24
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js8
-rw-r--r--app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js7
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js91
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js31
-rw-r--r--app/assets/javascripts/project_select.js116
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/views/shared/_group_form.html.haml7
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