diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 68690ff33da..d30deef0096 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,6 @@ stages: .dedicated-runner: &dedicated-runner tags: - gitlab-org - - 2gb .knapsack-state: &knapsack-state services: [] diff --git a/CHANGELOG.md b/CHANGELOG.md index cabfef84b24..aecacbee2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,136 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.16.0 (2017-02-22) + +- Add LDAP Rake task to rename a provider. !2181 +- Validate label's title length. !5767 (Tomáš Kukrál) +- Allow to add deploy keys with write-access. !5807 (Ali Ibrahim) +- Allow to use + symbol in filenames. !6644 (blackst0ne) +- Search bar redesign first iteration. !7345 +- Fix date inconsistency on due date picker. !7422 (Giuliano Varriale) +- Add email confirmation field to registration form. !7432 +- Updated project visibility settings UX. !7645 +- Go to a project order. !7737 (Jacopo Beschi @jacopo-beschi) +- Support slash comand `/merge` for merging merge requests. !7746 (Jarka Kadlecova) +- Add more storage statistics. !7754 (Markus Koller) +- Add support for PlantUML diagrams in AsciiDoc documents. !7810 (Horacio Sanson) +- Remove extra orphaned rows when removing stray namespaces. !7841 +- Added lighter count badge background-color for on white backgrounds. !7873 +- Fixes issue boards list colored top border visual glitch. !7898 (Pier Paolo Ramon) +- change 'gray' color theme name to 'black' to match the actual color. !7908 (BM5k) +- Remove trailing whitespace when generating changelog entry. !7948 +- Remove checking branches state in issue new branch button. !8023 +- Log LDAP blocking/unblocking events to application log. !8042 (Markus Koller) +- ensure permalinks scroll to correct position on multiple clicks. !8046 +- Allow to use ENV variables in redis config. !8073 (Semyon Pupkov) +- fix button layout issue on branches page. !8074 +- Reduce DB-load for build-queues by storing last_update in Redis. !8084 +- Record and show last used date of SSH Keys. !8113 (Vincent Wong) +- Resolves overflow in compare branch and tags dropdown. !8118 +- Replace wording for slash command confirmation message. !8123 +- remove build_user. !8162 (Arsenev Vladislav) +- Prevent empty pagination when list is not empty. !8172 +- Make successful pipeline emails off for watchers. !8176 +- Improve copy in Issue Tracker empty state. !8202 +- Adds CSS class to status icon on MR widget to prevent non-colored icon. !8219 +- Improve visibility of "Resolve conflicts" and "Merge locally" actions. !8229 +- Add Gitaly to the architecture documentation. !8264 (Pablo Carranza ) +- Sort numbers in build names more intelligently. !8277 +- Show nested groups tab on group page. !8308 +- Rename users with namespace ending with .git. !8309 +- Rename filename to file path in tooltip of file header in merge request diff. !8314 +- About GitLab link in sidebar that links to help page. !8316 +- Merged the 'Groups' and 'Projects' tabs when viewing user profiles. !8323 (James Gregory) +- re-enable change username button after failure. !8332 +- Darkened hr border color in descriptions because of update of bootstrap. !8333 +- display merge request discussion tab for empty branches. !8347 +- Fix double spaced CI log. !8349 (Jared Deckard ) +- Refactored note edit form to improve frontend performance on MR and Issues pages, especially pages with has a lot of discussions in it. !8356 +- Make CTRL+Enter submits a new merge request. !8360 (Saad Shahd) +- Fixes too short input for placeholder message in commit listing page. !8367 +- Fix typo: seach to search. !8370 +- Adds label to Environments "Date Created". !8376 (Saad Shahd) +- Convert project setting text into protected branch path link. !8377 (Ken Ding) +- Precompile all JavaScript fixtures. !8384 +- Use original casing for build action text. !8387 +- Scroll to bottom on build completion if autoscroll was active. !8391 +- Properly handle failed reCAPTCHA on user registration. !8403 +- Changed alerts to be responsive, centered text on smaller viewports. !8424 (Connor Smallman) +- Pass Gitaly resource path to gitlab-workhorse if Gitaly is enabled. !8440 +- Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown. !8443 +- Don't instrument 405 Grape calls. !8445 +- Change CI template linter textarea with Ace Editor. !8452 (Didem Acet) +- Removes unneeded `window` declaration in environments related code. !8456 +- API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`. !8457 (Panagiotis Atmatzidis, David Eisner) +- Fix broken url on group avatar. !8464 (hogewest) +- Fixes buttons not being accessible via the keyboard when creating new group. !8469 +- Restore backup correctly when "BACKUP" environment variable is passed. !8477 +- Add new endpoints for Time Tracking. !8483 +- Fix Compare page throws 500 error when any branch/reference is not selected. !8492 (Martin Cabrera) +- Treat environments matching `production/*` as Production. !8500 +- Hide build artifacts keep button if operation is not allowed. !8501 +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Remove Lock Icon on Protected Tag. !8513 (Sergey Nikitin) +- Use cached values to compute total issues count in milestone index pages. !8518 +- Speed up dashboard milestone index by scoping IssuesFinder to user authorized projects. !8524 +- Copy to clipboard. !8535 +- Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route. !8544 +- Fixes builds dropdown making request when clicked to be closed. !8545 +- Fixes pipeline status cell is too wide by adding missing classes in table head cells. !8549 +- Mutate the attribute instead of issuing a write operation to the DB in `ProjectFeaturesCompatibility` concern. !8552 +- Fix links to commits pages on pipelines list page. !8558 +- Ensure updating project settings shows a flash message on success. !8579 (Sandish Chen) +- Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip. !8593 +- Autoresize markdown preview. !8607 (Didem Acet) +- Link external build badge to its target URL. !8611 +- Adjust ProjectStatistic#repository_size with values saved as MB. !8616 +- Correct User-agent placement in robots.txt. !8623 (Eric Sabelhaus) +- Record used SSH keys only once per day. !8655 +- Do not generate pipeline branch/tag path if not present. !8658 +- Fix Merge When Pipeline Succeeds immediate merge bug. !8685 +- Fix blame 500 error on invalid path. !25761 (Jeff Stubler) +- Added animations to issue boards interactions. +- Check if user can read project before being assigned to issue. +- Show 'too many changes' message for created merge requests when they are too large. +- Fix redirect after update file when user has forked project. +- Parse JIRA issue references even if Issue Tracker is disabled. +- Made download artifacts button accessible via keyboard by changing it from an anchor tag to an actual button. (Ryan Harris) +- Make play button on Pipelines page accessible via keyboard. (Ryan Harris) +- Decreases font-size on login page. +- Fixed merge request tabs dont move when opening collapsed sidebar. +- Display project avatars on Admin Area and Projects pages for mobile views. (Ryan Harris) +- Fix participants margins to fit on one line. +- 26352 Change Profile settings to User / Settings. +- Fix Commits API to accept a Project path upon POST. +- Expire related caches after changing HEAD. (Minqi Pan) +- Add various hover animations throughout the application. +- Re-order update steps in the 8.14 -> 8.15 upgrade guide. +- Move award emoji's out of the discussion tab for merge requests. +- Synchronize all project authorization refreshing work to prevent race conditions. +- Remove the project_authorizations.id column. +- Combined the settings options project members and groups into a single one called members. +- Change earlier to task_status_short to avoid titlebar line wraps. +- 25701 standardize text colors. +- Handle HTTP errors in environment list. +- Re-add Google Cloud Storage as a backup strategy. +- Change status colors of runners to better defaults. +- Added number_with_delimiter to counter on milestone panels. (Ryan Harris) +- Query external CI statuses in the background. +- Allow group and project paths when transferring projects via the API. +- Don't validate environment urls on .gitlab-ci.yml. +- Fix a Grape deprecation, use `#request_method` instead of `#route_method`. +- Fill missing authorized projects rows. +- Allow API query to find projects with dots in their name. (Bruno Melli) +- Fix import/export wrong user mapping. +- Removed bottom padding from merge manually from CLI because of repositioning award emoji's. +- Fix project queued for deletion re-creation tooltip. +- Fix search group/project filtering to show results. +- Fix 500 error when POSTing to Users API with optional confirm param. +- 26504 Fix styling of MR jump to discussion button. +- Add margin to markdown math blocks. +- Add hover state to MR comment reply button. + ## 8.15.4 (2017-01-09) - Make successful pipeline emails off for watchers. !8176 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 6085e946503..f0bb29e7638 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.2.1 +1.3.0 diff --git a/Gemfile b/Gemfile index 83ba5d31b92..4e9cf91c429 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0' # Authentication libraries gem 'devise', '~> 4.2' gem 'doorkeeper', '~> 4.2.0' -gem 'omniauth', '~> 1.3.1' +gem 'omniauth', '~> 1.3.2' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-cas3', '~> 1.1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 104e6444803..c9115982838 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -449,7 +449,7 @@ GEM octokit (4.6.2) sawyer (~> 0.8.0, >= 0.5.3) oj (2.17.4) - omniauth (1.3.1) + omniauth (1.3.2) hashie (>= 1.2, < 4) rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) @@ -925,7 +925,7 @@ DEPENDENCIES oauth2 (~> 1.2.0) octokit (~> 4.6.2) oj (~> 2.17.4) - omniauth (~> 1.3.1) + omniauth (~> 1.3.2) omniauth-auth0 (~> 1.4.1) omniauth-authentiq (~> 0.2.0) omniauth-azure-oauth2 (~> 0.0.6) diff --git a/README.md b/README.md index 4e28f3aacfd..4f85fac4a56 100644 --- a/README.md +++ b/README.md @@ -113,4 +113,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ## Is it awesome? Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. -[These people](https://twitter.com/gitlab/favorites) seem to like it. +[These people](https://twitter.com/gitlab/likes) seem to like it. diff --git a/VERSION b/VERSION index 8e9258150a9..5c99c061a47 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.16.0-pre +8.17.0-pre diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index ed545ec8748..c79f0230951 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -62,6 +62,7 @@ var DropDown = function(list) { this.list = list; this.items = []; this.getItems(); + this.initTemplateString(); this.addEvents(); this.initialState = list.innerHTML; }; @@ -72,6 +73,17 @@ Object.assign(DropDown.prototype, { return this.items; }, + initTemplateString: function() { + var items = this.items || this.getItems(); + + var templateString = ''; + if(items.length > 0) { + templateString = items[items.length - 1].outerHTML; + } + this.templateString = templateString; + return this.templateString; + }, + clickEvent: function(e) { // climb up the tree to find the LI var selected = utils.closest(e.target, 'LI'); @@ -111,30 +123,21 @@ Object.assign(DropDown.prototype, { addData: function(data) { this.data = (this.data || []).concat(data); - this.render(data); + this.render(this.data); }, // call render manually on data; render: function(data){ // debugger // empty the list first - var sampleItem; + var templateString = this.templateString; var newChildren = []; var toAppend; - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - sampleItem = item; - if(item.parentNode && item.parentNode.dataset.hasOwnProperty('dynamic')) { - item.parentNode.removeChild(item); - } - } - - newChildren = this.data.map(function(dat){ - var html = utils.t(sampleItem.outerHTML, dat); + newChildren = (data ||[]).map(function(dat){ + var html = utils.t(templateString, dat); var template = document.createElement('div'); template.innerHTML = html; - // console.log(template.content) // Help set the image src template var imageTags = template.querySelectorAll('img[data-src]'); @@ -156,7 +159,7 @@ Object.assign(DropDown.prototype, { if(toAppend) { toAppend.innerHTML = newChildren.join(''); } else { - this.list.innerHTML = newChildren.join(''); + this.list.innerHTML = newChildren.join(''); } }, @@ -173,10 +176,7 @@ Object.assign(DropDown.prototype, { }, destroy: function() { - if (!this.hidden) { - this.hide(); - } - + this.hide(); this.list.removeEventListener('click', this.clickWrapper); } }); @@ -278,7 +278,7 @@ require('./window')(function(w){ self.hooks[i].list.hide(); } }.bind(this); - w.addEventListener('click', this.windowClickedWrapper); + document.addEventListener('click', this.windowClickedWrapper); }, removeEvents: function(){ @@ -307,7 +307,7 @@ require('./window')(function(w){ if(!list){ list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); } - + if(hook) { if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { this.hooks.push(new HookButton(hook, list, plugins, config)); @@ -462,6 +462,8 @@ Object.assign(HookInput.prototype, { var self = this; this.mousedown = function mousedown(e) { + if(self.hasRemovedEvents) return; + var mouseEvent = new CustomEvent('mousedown.dl', { detail: { hook: self, @@ -474,6 +476,8 @@ Object.assign(HookInput.prototype, { } this.input = function input(e) { + if(self.hasRemovedEvents) return; + var inputEvent = new CustomEvent('input.dl', { detail: { hook: self, @@ -487,10 +491,14 @@ Object.assign(HookInput.prototype, { } this.keyup = function keyup(e) { + if(self.hasRemovedEvents) return; + keyEvent(e, 'keyup.dl'); } this.keydown = function keydown(e) { + if(self.hasRemovedEvents) return; + keyEvent(e, 'keydown.dl'); } @@ -520,7 +528,8 @@ Object.assign(HookInput.prototype, { this.trigger.addEventListener('keydown', this.keydown); }, - removeEvents: function(){ + removeEvents: function() { + this.hasRemovedEvents = true; this.trigger.removeEventListener('mousedown', this.mousedown); this.trigger.removeEventListener('input', this.input); this.trigger.removeEventListener('keyup', this.keyup); @@ -578,7 +587,7 @@ require('./window')(function(w){ var listItems = removeHighlight(list); if(currentIndex>0){ if(!listItems[currentIndex-1]){ - currentIndex = currentIndex-1; + currentIndex = currentIndex-1; } listItems[currentIndex-1].classList.add('dropdown-active'); } @@ -630,7 +639,7 @@ require('./window')(function(w){ return; } if(currentKey === 'ArrowUp') { - isUpArrow = true; + isUpArrow = true; } if(currentKey === 'ArrowDown') { isDownArrow = true; @@ -668,16 +677,16 @@ var camelize = function(str) { }; var closest = function(thisTag, stopTag) { - while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ thisTag = thisTag.parentNode; } return thisTag; }; var isDropDownParts = function(target) { - if(target.tagName === 'HTML') { return false; } + if(!target || target.tagName === 'HTML') { return false; } return ( - target.hasAttribute(DATA_TRIGGER) || + target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN) ); }; diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6 new file mode 100644 index 00000000000..abedae4c1c7 --- /dev/null +++ b/app/assets/javascripts/extensions/custom_event.js.es6 @@ -0,0 +1,12 @@ +/* global CustomEvent */ +/* eslint-disable no-global-assign */ + +// Custom event support for IE +CustomEvent = function CustomEvent(event, parameters) { + const params = parameters || { bubbles: false, cancelable: false, detail: undefined }; + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; +}; + +CustomEvent.prototype = window.Event.prototype; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 63c20f57520..7d297b8eee8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -9,7 +9,7 @@ this.config = { droplabFilter: { template: 'hint', - filterFunction: gl.DropdownUtils.filterHint, + filterFunction: gl.DropdownUtils.filterHint.bind(null, input), }, }; } @@ -20,6 +20,9 @@ if (selected.tagName === 'LI') { if (selected.hasAttribute('data-value')) { this.dismissDropdown(); + } else if (selected.getAttribute('data-action') === 'submit') { + this.dismissDropdown(); + this.dispatchFormSubmitEvent(); } else { const token = selected.querySelector('.js-filter-hint').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index f06c3fc9c6f..13cbec1be4a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -15,7 +15,7 @@ loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol), + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), }, }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index e80d266ae89..7bf199d9274 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -37,7 +37,7 @@ } getSearchInput() { - const query = this.input.value.trim(); + const query = gl.DropdownUtils.getSearchInput(this.input); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); return lastToken.value || ''; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index c27ef3042d1..443ac222f70 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -20,17 +20,15 @@ return escapedText; } - static filterWithSymbol(filterSymbol, item, query) { + static filterWithSymbol(filterSymbol, input, item) { const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); if (lastToken !== searchToken) { const title = updatedItem.title.toLowerCase(); let value = lastToken.value.toLowerCase(); - - if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { - value = value.slice(1); - } + value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -1)); // Eg. filterSymbol = ~ for labels const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; @@ -44,8 +42,9 @@ return updatedItem; } - static filterHint(item, query) { + static filterHint(input, item) { const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); lastToken = lastToken.key || lastToken || ''; @@ -72,6 +71,48 @@ // Return boolean based on whether it was set return dataValue !== null; } + + static getSearchInput(filteredSearchInput) { + const inputValue = filteredSearchInput.value; + const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + + return inputValue.slice(0, right); + } + + static getInputSelectionPosition(input) { + const selectionStart = input.selectionStart; + let inputValue = input.value; + // Replace all spaces inside quote marks with underscores + // This helps with matching the beginning & end of a token:key + inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_')); + + // Get the right position for the word selected + // Regex matches first space + let right = inputValue.slice(selectionStart).search(/\s/); + + if (right >= 0) { + right += selectionStart; + } else if (right < 0) { + right = inputValue.length; + } + + // Get the left position for the word selected + // Regex matches last non-whitespace character + let left = inputValue.slice(0, right).search(/\S+$/); + + if (selectionStart === 0) { + left = 0; + } else if (selectionStart === inputValue.length && left < 0) { + left = inputValue.length; + } else if (left < 0) { + left = selectionStart; + } + + return { + left, + right, + }; + } } window.gl = window.gl || {}; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 886d8113f4a..859d6515531 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -39,6 +39,7 @@ } this.dismissDropdown(); + this.dispatchInputEvent(); } } @@ -78,7 +79,16 @@ dispatchInputEvent() { // Propogate input change to FilteredSearchDropdownManager // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); + this.input.dispatchEvent(new CustomEvent('input', { + bubbles: true, + cancelable: true, + })); + } + + dispatchFormSubmitEvent() { + // dispatchEvent() is necessary as form.submit() does not + // trigger event handlers + this.input.form.dispatchEvent(new Event('submit')); } hideDropdown() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 1cd0483877a..00e1c28692f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -57,28 +57,33 @@ static addWordToInput(tokenName, tokenValue = '') { const input = document.querySelector('.filtered-search'); + const inputValue = input.value; const word = `${tokenName}:${tokenValue}`; - const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value); - const lastSearchToken = searchToken.split(' ').last(); - const lastInputCharacter = input.value[input.value.length - 1]; - const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1]; + // Get the string to replace + let newCaretPosition = input.selectionStart; + const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); - // Remove the typed tokenName - if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') { - // Remove spaces after the colon - if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') { - input.value = input.value.trim(); - } + input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; - input.value = input.value.slice(0, -1 * lastSearchToken.length); - } else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) { - // Remove the existing tokenValue - const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`; - input.value = input.value.slice(0, -1 * lastTokenString.length); + // If we have added a tokenValue at the end of the input, + // add a space and set selection to the end + if (right >= inputValue.length && tokenValue !== '') { + input.value += ' '; + newCaretPosition = input.value.length; } - input.value += word; + gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); + } + + static updateInputCaretPosition(selectionStart, input) { + // Reset the position + // Sometimes can end up at end of input + input.setSelectionRange(selectionStart, selectionStart); + + const { right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.setSelectionRange(right, right); } updateCurrentDropdownOffset() { @@ -90,9 +95,18 @@ this.font = window.getComputedStyle(this.filteredSearchInput).font; } + const input = this.filteredSearchInput; + const inputText = input.value.slice(0, input.selectionStart); const filterIconPadding = 27; - const offset = gl.text - .getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; + + const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : + this.mapping[key].element.clientWidth; + const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; + + if (offsetMaxWidth < offset) { + offset = offsetMaxWidth; + } this.mapping[key].reference.setOffset(offset); } @@ -148,9 +162,9 @@ setDropdown() { const { lastToken, searchToken } = this.tokenizer - .processTokens(this.filteredSearchInput.value); + .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); - if (this.filteredSearchInput.value.split('').last() === ' ') { + if (this.currentDropdown) { this.updateCurrentDropdownOffset(); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index ffd0d7e9cba..ae19bb68360 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -25,24 +25,32 @@ } bindEvents() { + this.handleFormSubmit = this.handleFormSubmit.bind(this); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.tokenChange = this.tokenChange.bind(this); + this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.addEventListener('click', this.tokenChange); + this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); } unbindEvents() { + this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.removeEventListener('click', this.tokenChange); + this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); } @@ -83,8 +91,14 @@ this.dropdownManager.resetDropdowns(); } + handleFormSubmit(e) { + e.preventDefault(); + this.search(); + } + loadSearchParamsFromURL() { const params = gl.utils.getUrlParamsArray(); + const usernameParams = this.getUsernameParams(); const inputValues = []; params.forEach((p) => { @@ -115,6 +129,16 @@ } inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'assignee_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`assignee:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'author_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`author:@${usernameParams[id]}`); + } } else if (!match && keyParam === 'search') { inputValues.push(sanitizedValue); } @@ -164,6 +188,27 @@ Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); } + + getUsernameParams() { + const usernamesById = {}; + try { + const attribute = this.filteredSearchInput.getAttribute('data-username-params'); + JSON.parse(attribute).forEach((user) => { + usernamesById[user.id] = user.username; + }); + } catch (e) { + // do nothing + } + return usernamesById; + } + + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } } window.gl = window.gl || {}; diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index 5247b2a08f7..10dfd05fe3c 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -2,12 +2,12 @@ (function() { this.GroupAvatar = (function() { function GroupAvatar() { - $('.js-choose-group-avatar-button').bind("click", function() { + $('.js-choose-group-avatar-button').on("click", function() { var form; form = $(this).closest("form"); return form.find(".js-group-avatar-input").click(); }); - $('.js-group-avatar-input').bind("change", function() { + $('.js-group-avatar-input').on("change", function() { var filename, form; form = $(this).closest("form"); filename = $(this).val().replace(/^.*[\\\/]/, ''); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7a315e43667..7cc319e2f4e 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -126,7 +126,9 @@ MergeRequestWidget.prototype.getMergeStatus = function() { return $.get(this.opts.merge_check_url, function(data) { - return $('.mr-state-widget').replaceWith(data); + var $html = $(data); + $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); + $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); }); }; diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 index 2b074994b4a..5969d2ba56b 100644 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -8,31 +8,42 @@ * temporarily. * */ - if ($('.accept-mr-form').length) { - $('.accept-mr-form').on('ajax:send', () => { - $('.accept-mr-form :input').disable(); - }); + $(document) + .off('ajax:send', '.accept-mr-form') + .on('ajax:send', '.accept-mr-form', () => { + $('.accept-mr-form :input').disable(); + }); - $('.accept_merge_request').on('click', () => { - $('.js-merge-button').html(' Merge in progress'); - }); + $(document) + .off('click', '.accept_merge_request') + .on('click', '.accept_merge_request', () => { + $('.js-merge-button').html(' Merge in progress'); + }); - $('.merge_when_build_succeeds').on('click', () => { - $('#merge_when_build_succeeds').val('1'); - }); + $(document) + .off('click', '.merge_when_build_succeeds') + .on('click', '.merge_when_build_succeeds', () => { + $('#merge_when_build_succeeds').val('1'); + }); - $('.js-merge-dropdown a').on('click', (e) => { - e.preventDefault(); - $(this).closest('form').submit(); - }); - } else if ($('.rebase-in-progress').length) { + $(document) + .off('click', '.js-merge-dropdown a') + .on('click', '.js-merge-dropdown a', (e) => { + e.preventDefault(); + $(e.target).closest('form').submit(); + }); + if ($('.rebase-in-progress').length) { merge_request_widget.rebaseInProgress(); } else if ($('.rebase-mr-form').length) { - $('.rebase-mr-form').on('ajax:send', () => { + $(document) + .off('ajax:send', '.rebase-mr-form') + .on('ajax:send', '.rebase-mr-form', () => { $('.rebase-mr-form :input').disable(); }); - $('.js-rebase-button').on('click', () => { + $(document) + .off('click', '.js-rebase-button') + .on('click', '.js-rebase-button', () => { $('.js-rebase-button').html(" Rebase in progress"); }); } else { diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 32973132174..4e85f16ebc5 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -1,5 +1,5 @@ /* global Vue, Flash, gl */ -/* eslint-disable no-param-reassign, no-bitwise */ +/* eslint-disable no-param-reassign */ ((gl) => { gl.VueStage = Vue.extend({ @@ -9,7 +9,20 @@ spinner: '', }; }, - props: ['stage', 'svgs', 'match'], + props: { + stage: { + type: Object, + required: true, + }, + svgs: { + type: DOMStringMap, + required: true, + }, + match: { + type: Function, + required: true, + }, + }, methods: { fetchBuilds(e) { const areaExpanded = e.currentTarget.attributes['aria-expanded']; @@ -24,6 +37,18 @@ return flash; }); }, + keepGraph(e) { + const { target } = e; + + if (target.className.indexOf('js-ci-action-icon') >= 0) return null; + + if ( + target.parentElement && + (target.parentElement.className.indexOf('js-ci-action-icon') >= 0) + ) return null; + + return e.stopPropagation(); + }, }, computed: { buildsOrSpinner() { @@ -64,7 +89,7 @@