diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 50d85f58d69..00000000000 --- a/.babelrc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "presets": [["latest", { "es2015": { "modules": false } }], "stage-2"], - "env": { - "karma": { - "plugins": ["rewire"] - }, - "coverage": { - "plugins": [ - [ - "istanbul", - { - "exclude": ["spec/javascripts/**/*", "app/assets/javascripts/locale/**/app.js"] - } - ], - [ - "transform-define", - { - "process.env.BABEL_ENV": "coverage" - } - ], - "rewire" - ] - } - } -} diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 00000000000..27caf378b99 --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,38 @@ +const BABEL_ENV = process.env.BABEL_ENV || process.env.NODE_ENV || null; + +const presets = [ + [ + '@babel/preset-env', + { + modules: false, + targets: { + ie: '11', + }, + }, + ], +]; + +// include stage 3 proposals +const plugins = [ + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-syntax-import-meta', + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-json-strings', +]; + +// add code coverage tooling if necessary +if (BABEL_ENV === 'coverage') { + plugins.push([ + 'babel-plugin-istanbul', + { + exclude: ['spec/javascripts/**/*', 'app/assets/javascripts/locale/**/app.js'], + }, + ]); +} + +// add rewire support when running tests +if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') { + plugins.push('babel-plugin-rewire'); +} + +module.exports = { presets, plugins }; diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 7fd32563696..96a157648fb 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -6,7 +6,8 @@ /doc/ @axil @marcia # Frontend maintainers should see everything in `app/assets/` -app/assets/ @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann +app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann +*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann # Someone from the database team should review changes in `db/` db/ @abrandl @NikolayS diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d8c4e965190..d4f7615c80e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -445,7 +445,6 @@ Style/Dir: # Cop supports --auto-correct. Style/EachWithObject: Exclude: - - 'config/initializers/gollum.rb' - 'lib/expand_variables.rb' - 'lib/gitlab/ci/ansi2html.rb' - 'lib/gitlab/ee_compat_check.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ab8599d99..be204a76645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.3.2 (2018-10-03) + +### Fixed (4 changes) + +- Fix NULL pipeline import problem and pipeline user mapping issue. !21875 +- Fix migration to avoid an exception during upgrade. !22055 +- Fixes admin runners table not wrapping content. +- Fix Error 500 when forking projects with Gravatar disabled. + +### Other (1 change) + +- Removes the 'required' attribute from the 'project name' field. !21770 + + ## 11.3.1 (2018-09-26) ### Security (6 changes) diff --git a/Gemfile b/Gemfile index ecf5e6392c0..52de588deb3 100644 --- a/Gemfile +++ b/Gemfile @@ -80,11 +80,9 @@ gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap' gem 'net-ldap' # Git Wiki -# Required manually in config/initializers/gollum.rb to control load order +# Only used to compute wiki page slugs gem 'gitlab-gollum-lib', '~> 4.2', require: false -gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false - # Language detection gem 'github-linguist', '~> 5.3.3', require: 'linguist' @@ -134,6 +132,7 @@ gem 'seed-fu', '~> 2.3.7' gem 'html-pipeline', '~> 2.8' gem 'deckar01-task_list', '2.0.0' gem 'gitlab-markup', '~> 1.6.4' +gem 'github-markup', '~> 1.7.0', require: 'github/markup' gem 'redcarpet', '~> 3.4' gem 'commonmarker', '~> 0.17' gem 'RedCloth', '~> 4.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 69a1c899cc5..301f4132476 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,7 +86,7 @@ GEM bindata (2.4.3) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootsnap (1.3.1) + bootsnap (1.3.2) msgpack (~> 1.0) bootstrap_form (2.7.0) brakeman (4.2.1) @@ -140,7 +140,7 @@ GEM creole (0.5.0) css_parser (1.5.0) addressable - daemons (1.2.3) + daemons (1.2.6) database_cleaner (1.5.3) debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) @@ -187,7 +187,7 @@ GEM escape_utils (1.1.1) et-orbi (1.0.3) tzinfo - eventmachine (1.0.8) + eventmachine (1.2.7) excon (0.62.0) execjs (2.6.0) expression_parser (0.9.0) @@ -295,9 +295,6 @@ GEM rouge (~> 3.1) sanitize (~> 4.6.4) stringex (~> 2.6) - gitlab-gollum-rugged_adapter (0.4.4.1) - mime-types (>= 1.15) - rugged (~> 0.25) gitlab-grit (2.8.2) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) @@ -491,7 +488,7 @@ GEM mime-types-data (3.2016.0521) mimemagic (0.3.0) mini_magick (4.8.0) - mini_mime (1.0.0) + mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.7.0) mousetrap-rails (1.4.6) @@ -627,9 +624,9 @@ GEM pry-byebug (3.4.3) byebug (>= 9.0, < 9.1) pry (~> 0.10) - pry-rails (0.3.5) - pry (>= 0.9.10) - public_suffix (3.0.2) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (3.0.3) pyu-ruby-sasl (0.0.3.3) rack (1.6.10) rack-accept (0.4.5) @@ -856,7 +853,7 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slack-notifier (1.5.1) - spring (2.0.1) + spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) @@ -886,7 +883,7 @@ GEM test_after_commit (1.1.0) activerecord (>= 3.2) text (1.3.1) - thin (1.7.0) + thin (1.7.2) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) @@ -945,7 +942,7 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - webpack-rails (0.9.10) + webpack-rails (0.9.11) railties (>= 3.2.0) wikicloth (0.8.1) builder @@ -1030,9 +1027,9 @@ DEPENDENCIES gettext_i18n_rails_js (~> 1.3) gitaly-proto (~> 0.118.1) github-linguist (~> 5.3.3) + github-markup (~> 1.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) - gitlab-gollum-rugged_adapter (~> 0.4.4) gitlab-markup (~> 1.6.4) gitlab-styles (~> 2.4) gitlab_omniauth-ldap (~> 2.0.4) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 30605e460e6..1e129bc2b54 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -89,7 +89,7 @@ GEM bindata (2.4.3) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootsnap (1.3.1) + bootsnap (1.3.2) msgpack (~> 1.0) bootstrap_form (2.7.0) brakeman (4.2.1) @@ -143,7 +143,7 @@ GEM creole (0.5.0) css_parser (1.5.0) addressable - daemons (1.2.3) + daemons (1.2.6) database_cleaner (1.5.3) debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) @@ -190,7 +190,7 @@ GEM escape_utils (1.1.1) et-orbi (1.0.3) tzinfo - eventmachine (1.0.8) + eventmachine (1.2.7) excon (0.62.0) execjs (2.6.0) expression_parser (0.9.0) @@ -298,9 +298,6 @@ GEM rouge (~> 3.1) sanitize (~> 4.6.4) stringex (~> 2.6) - gitlab-gollum-rugged_adapter (0.4.4.1) - mime-types (>= 1.15) - rugged (~> 0.25) gitlab-grit (2.8.2) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) @@ -410,7 +407,7 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.0.1) + i18n (1.1.0) concurrent-ruby (~> 1.0) icalendar (2.4.1) ice_nine (0.11.2) @@ -494,7 +491,7 @@ GEM mime-types-data (3.2016.0521) mimemagic (0.3.0) mini_magick (4.8.0) - mini_mime (1.0.0) + mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.7.0) mousetrap-rails (1.4.6) @@ -631,9 +628,9 @@ GEM pry-byebug (3.4.3) byebug (>= 9.0, < 9.1) pry (~> 0.10) - pry-rails (0.3.5) - pry (>= 0.9.10) - public_suffix (3.0.2) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (3.0.3) pyu-ruby-sasl (0.0.3.3) rack (2.0.5) rack-accept (0.4.5) @@ -864,7 +861,7 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slack-notifier (1.5.1) - spring (2.0.1) + spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) @@ -892,7 +889,7 @@ GEM temple (0.8.0) test-prof (0.2.5) text (1.3.1) - thin (1.7.0) + thin (1.7.2) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) @@ -951,7 +948,7 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - webpack-rails (0.9.10) + webpack-rails (0.9.11) railties (>= 3.2.0) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) @@ -1039,9 +1036,9 @@ DEPENDENCIES gettext_i18n_rails_js (~> 1.3) gitaly-proto (~> 0.118.1) github-linguist (~> 5.3.3) + github-markup (~> 1.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) - gitlab-gollum-rugged_adapter (~> 0.4.4) gitlab-markup (~> 1.6.4) gitlab-styles (~> 2.4) gitlab_omniauth-ldap (~> 2.0.4) diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index f248f53fa51..f7ce5128964 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,5 +1,6 @@ - - diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue deleted file mode 100644 index 0ec6b8b7f21..00000000000 --- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 1c9ad8e77f1..9bbf62c0eb6 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -1,9 +1,18 @@ diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index 96cccb49378..c3acc352d5e 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -108,7 +108,7 @@ export default { + + diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index bcbe374a90c..4e04e50c52a 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -1,5 +1,5 @@ + + + + diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue new file mode 100644 index 00000000000..cfe4273742f --- /dev/null +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -0,0 +1,101 @@ + + + diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 2795dddfc48..6a50d2c1426 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -29,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17; export const LINES_TO_BE_RENDERED_DIRECTLY = 100; export const MAX_LINES_TO_BE_RENDERED = 2000; + +export const MR_TREE_SHOW_KEY = 'mr_tree_show'; diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js deleted file mode 100644 index da1339f0ffa..00000000000 --- a/app/assets/javascripts/diffs/mixins/changed_files.js +++ /dev/null @@ -1,38 +0,0 @@ -export default { - props: { - diffFiles: { - type: Array, - required: true, - }, - }, - methods: { - fileChangedIcon(diffFile) { - if (diffFile.deletedFile) { - return 'file-deletion'; - } else if (diffFile.newFile) { - return 'file-addition'; - } - return 'file-modified'; - }, - fileChangedClass(diffFile) { - if (diffFile.deletedFile) { - return 'cred'; - } else if (diffFile.newFile) { - return 'cgreen'; - } - - return ''; - }, - truncatedDiffPath(path) { - const maxLength = 60; - - if (path.length > maxLength) { - const start = path.length - maxLength; - const end = start + maxLength; - return `...${path.slice(start, end)}`; - } - - return path; - }, - }, -}; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 98d8d5943f9..1e0b27b538d 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -12,6 +12,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, + MR_TREE_SHOW_KEY, } from '../constants'; export const setBaseConfig = ({ commit }, options) => { @@ -195,5 +196,23 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); }; +export const toggleTreeOpen = ({ commit }, path) => { + commit(types.TOGGLE_FOLDER_OPEN, path); +}; + +export const scrollToFile = ({ state, commit }, path) => { + const { fileHash } = state.treeEntries[path]; + document.location.hash = fileHash; + + commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); + + setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000); +}; + +export const toggleShowTreeList = ({ commit, state }) => { + commit(types.TOGGLE_SHOW_TREE_LIST); + localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 968ba3c5e13..d4c205882ff 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -110,5 +110,9 @@ export const shouldRenderInlineCommentRow = state => line => { export const getDiffFileByHash = state => fileHash => state.diffFiles.find(file => file.fileHash === fileHash); +export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob'); + +export const diffFilesLength = state => state.diffFiles.length; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index eb596b251c1..ae8930c8968 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -1,10 +1,11 @@ import Cookies from 'js-cookie'; import { getParameterValues } from '~/lib/utils/url_utility'; -import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants'; +import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants'; const viewTypeFromQueryString = getParameterValues('view')[0]; const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME); const defaultViewType = INLINE_DIFF_VIEW_TYPE; +const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); export default () => ({ isLoading: true, @@ -17,4 +18,8 @@ export default () => ({ mergeRequestDiff: null, diffLineCommentForms: {}, diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, + tree: [], + treeEntries: {}, + showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true', + currentDiffFileId: '', }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index f61efbe6e1e..6474ee628e2 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -11,3 +11,6 @@ export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; export const RENDER_FILE = 'RENDER_FILE'; export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE'; export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE'; +export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN'; +export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST'; +export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 59a2c09e54f..0b4485ecdb5 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { sortTree } from '~/ide/stores/utils'; import { findDiffFile, addLineReferences, @@ -7,6 +8,7 @@ import { addContextLines, prepareDiffData, isDiscussionApplicableToLine, + generateTreeList, } from './utils'; import * as types from './mutation_types'; @@ -23,9 +25,12 @@ export default { [types.SET_DIFF_DATA](state, data) { const diffData = convertObjectPropsToCamelCase(data, { deep: true }); prepareDiffData(diffData); + const { tree, treeEntries } = generateTreeList(diffData.diffFiles); Object.assign(state, { ...diffData, + tree: sortTree(tree), + treeEntries, }); }, @@ -163,4 +168,13 @@ export default { } } }, + [types.TOGGLE_FOLDER_OPEN](state, path) { + state.treeEntries[path].opened = !state.treeEntries[path].opened; + }, + [types.TOGGLE_SHOW_TREE_LIST](state) { + state.showTreeList = !state.showTreeList; + }, + [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) { + state.currentDiffFileId = fileId; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 631e3de311e..4ae588042e4 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -267,3 +267,49 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD return latestDiff && discussion.active && lineCode === discussion.line_code; } + +export const generateTreeList = files => + files.reduce( + (acc, file) => { + const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file; + const split = newPath.split('/'); + + split.forEach((name, i) => { + const parent = acc.treeEntries[split.slice(0, i).join('/')]; + const path = `${parent ? `${parent.path}/` : ''}${name}`; + + if (!acc.treeEntries[path]) { + const type = path === newPath ? 'blob' : 'tree'; + acc.treeEntries[path] = { + key: path, + path, + name, + type, + tree: [], + }; + + const entry = acc.treeEntries[path]; + + if (type === 'blob') { + Object.assign(entry, { + changed: true, + tempFile: newFile, + deleted: deletedFile, + fileHash, + addedLines, + removedLines, + }); + } else { + Object.assign(entry, { + opened: true, + }); + } + + (parent ? parent.tree : acc.tree).push(entry); + } + }); + + return acc; + }, + { treeEntries: {}, tree: [] }, + ); diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index ccc8419ca6d..a0797b594cb 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -2,12 +2,14 @@ /** * Renders the Monitoring (Metrics) link in environments table. */ +import { Button } from '@gitlab-org/gitlab-ui'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { components: { Icon, + 'gl-button': Button, }, directives: { tooltip, @@ -26,15 +28,16 @@ export default { }; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 8aecf9725e6..c568f4e4ebf 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -51,7 +51,11 @@ export default class DropdownHint extends FilteredSearchDropdown { FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); } - FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); + const key = token.replace(':', ''); + const { uppercaseTokenName } = this.tokenKeys.searchByKey(key); + FilteredSearchDropdownManager.addWordToInput(key, '', false, { + uppercaseTokenName, + }); } this.dismissDropdown(); this.dispatchInputEvent(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 27fff488603..6da6ca10008 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -143,7 +143,9 @@ export default class DropdownUtils { const dataValue = selected.getAttribute('data-value'); if (dataValue) { - FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); + FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true, { + capitalizeTokenValue: selected.hasAttribute('data-capitalize'), + }); } // Return boolean based on whether it was set diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 207616b9de2..cd3d532c958 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -91,6 +91,11 @@ export default class FilteredSearchDropdownManager { gl: DropdownEmoji, element: this.container.querySelector('#js-dropdown-my-reaction'), }, + wip: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-wip'), + }, status: { reference: null, gl: NullDropdown, @@ -136,10 +141,16 @@ export default class FilteredSearchDropdownManager { return endpoint; } - static addWordToInput(tokenName, tokenValue = '', clicked = false) { + static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { + const { + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = options; const input = FilteredSearchContainer.container.querySelector('.filtered-search'); - - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { + uppercaseTokenName, + capitalizeTokenValue, + }); input.value = ''; if (clicked) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index d25f6f95b22..54533ebb70d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -405,7 +405,10 @@ export default class FilteredSearchManager { if (isLastVisualTokenValid) { tokens.forEach((t) => { input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); - FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); + FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, { + uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key), + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(t.key), + }); }); const fragments = searchToken.split(':'); @@ -421,7 +424,10 @@ export default class FilteredSearchManager { FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); } - FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); + FilteredSearchVisualTokens.addFilterVisualToken(tokenKey, null, { + uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(tokenKey), + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey), + }); input.value = input.value.replace(`${tokenKey}:`, ''); } } else { @@ -429,7 +435,10 @@ export default class FilteredSearchManager { const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { - FilteredSearchVisualTokens.addFilterVisualToken(searchToken); + const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial(); + FilteredSearchVisualTokens.addFilterVisualToken(searchToken, null, { + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey), + }); // Trim the last space as seen in the if statement above input.value = input.value.replace(searchToken, '').trim(); @@ -480,7 +489,7 @@ export default class FilteredSearchManager { FilteredSearchVisualTokens.addFilterVisualToken( condition.tokenKey, condition.value, - canEdit, + { canEdit }, ); } else { // Sanitize value since URL converts spaces into + @@ -506,10 +515,15 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); + const { uppercaseTokenName, capitalizeTokenValue } = match; FilteredSearchVisualTokens.addFilterVisualToken( sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, - canEdit, + { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }, ); } else if (!match && keyParam === 'assignee_id') { const id = parseInt(value, 10); @@ -517,7 +531,7 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'assignee'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); } } else if (!match && keyParam === 'author_id') { const id = parseInt(value, 10); @@ -525,7 +539,7 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'author'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); } } else if (!match && keyParam === 'search') { hasFilteredSearch = true; @@ -561,15 +575,17 @@ export default class FilteredSearchManager { this.saveCurrentSearchQuery(); - const { tokens, searchToken } - = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); + const tokenKeys = this.filteredSearchTokenKeys.getKeys(); + const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, tokenKeys); const currentState = state || getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { const condition = this.filteredSearchTokenKeys .searchByConditionKeyValue(token.key, token.value.toLowerCase()); - const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const { param } = tokenConfig; + // Replace hyphen with underscore to use as request parameter // e.g. 'my-reaction' => 'my_reaction' const underscoredKey = token.key.replace('-', '_'); @@ -581,6 +597,10 @@ export default class FilteredSearchManager { } else { let tokenValue = token.value; + if (tokenConfig.lowercaseValueOnSubmit) { + tokenValue = tokenValue.toLowerCase(); + } + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { tokenValue = tokenValue.slice(1, tokenValue.length - 1); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 5d131b396a0..a09ad3e4758 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -23,6 +23,16 @@ export default class FilteredSearchTokenKeys { return this.conditions; } + shouldUppercaseTokenName(tokenKey) { + const token = this.searchByKey(tokenKey.toLowerCase()); + return token && token.uppercaseTokenName; + } + + shouldCapitalizeTokenValue(tokenKey) { + const token = this.searchByKey(tokenKey.toLowerCase()); + return token && token.capitalizeTokenValue; + } + searchByKey(key) { return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null; } @@ -55,4 +65,21 @@ export default class FilteredSearchTokenKeys { return this.conditions .find(condition => condition.tokenKey === key && condition.value === value) || null; } + + addExtraTokensForMergeRequests() { + const wipToken = { + key: 'wip', + type: 'string', + param: '', + symbol: '', + icon: 'admin', + tag: 'Yes or No', + lowercaseValueOnSubmit: true, + uppercaseTokenName: true, + capitalizeTokenValue: true, + }; + + this.tokenKeys.push(wipToken); + this.tokenKeysWithAlternative.push(wipToken); + } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 56fe1ab4e90..0854c1822fb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -55,12 +55,18 @@ export default class FilteredSearchVisualTokens { } } - static createVisualTokenElementHTML(canEdit = true) { + static createVisualTokenElementHTML(options = {}) { + const { + canEdit = true, + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = options; + return `
-
+
-
+
@@ -182,16 +188,26 @@ export default class FilteredSearchVisualTokens { } } - static addVisualTokenElement(name, value, isSearchTerm, canEdit) { + static addVisualTokenElement(name, value, options = {}) { + const { + isSearchTerm = false, + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + } = options; const li = document.createElement('li'); li.classList.add('js-visual-token'); li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); if (value) { - li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit); + li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value); } else { - li.innerHTML = '
'; + li.innerHTML = `
`; } li.querySelector('.name').innerText = name; @@ -212,20 +228,32 @@ export default class FilteredSearchVisualTokens { } } - static addFilterVisualToken(tokenName, tokenValue, canEdit) { + static addFilterVisualToken(tokenName, tokenValue, { + canEdit, + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = {}) { const { lastVisualToken, isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { addVisualTokenElement } = FilteredSearchVisualTokens; if (isLastVisualTokenValid) { - addVisualTokenElement(tokenName, tokenValue, false, canEdit); + addVisualTokenElement(tokenName, tokenValue, { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); } else { const previousTokenName = lastVisualToken.querySelector('.name').innerText; const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); tokensContainer.removeChild(lastVisualToken); const value = tokenValue || tokenName; - addVisualTokenElement(previousTokenName, value, false, canEdit); + addVisualTokenElement(previousTokenName, value, { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); } } @@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens { if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) { lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`; } else { - FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true); + FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, { + isSearchTerm: true, + }); } } @@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens { let value; if (token.classList.contains('filtered-search-token')) { - FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText); + FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText, null, { + uppercaseTokenName: nameElement.classList.contains('text-uppercase'), + }); const valueContainerElement = token.querySelector('.value-container'); value = valueContainerElement.dataset.originalValue; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index 3aca38399fb..b0e60edcbe5 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -3,7 +3,7 @@ import $ from 'jquery'; import { mapActions } from 'vuex'; import { __ } from '~/locale'; import FileIcon from '~/vue_shared/components/file_icon.vue'; -import ChangedFileIcon from '../changed_file_icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue index a612739d641..72ce37be63a 100644 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -1,7 +1,7 @@ - diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue new file mode 100644 index 00000000000..bac8bd71d64 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -0,0 +1,99 @@ + + diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue index 93e2292ff84..271b7790d75 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -1,4 +1,5 @@