From cdd71cf36a45b72d8207fe4fcfc4e44a405d3607 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 10 Jul 2020 09:09:01 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- app/assets/javascripts/global_search_input.js | 425 ------------------ .../issuables_list/components/issuable.vue | 2 +- .../components/issuables_list_app.vue | 149 ++++-- app/assets/javascripts/issuables_list/constants.js | 21 + app/assets/javascripts/main.js | 4 +- app/assets/javascripts/notes/stores/actions.js | 11 +- .../components/details_page/tags_list_row.vue | 31 +- .../components/list_page/image_list_row.vue | 2 +- .../registry/explorer/constants/details.js | 8 +- app/assets/javascripts/search_autocomplete.js | 500 +++++++++++++++++++++ app/controllers/search_controller.rb | 15 + app/helpers/export_helper.rb | 2 +- app/helpers/search_helper.rb | 107 +++++ app/services/jira/requests/issues/list_service.rb | 4 +- app/views/layouts/_search.html.haml | 5 +- app/views/search/results/_note.html.haml | 2 +- app/views/shared/_issuable_meta_data.html.haml | 2 +- app/views/shared/snippets/_snippet.html.haml | 2 +- ...shows-error-something-went-wrong-while-fetc.yml | 5 + .../220342-remove-services-from-import-export.yml | 5 + ...-comment-icons-with-gitlab-svg-comment-icon.yml | 5 + ...-digest-revision-and-short-revision-in-tags.yml | 5 + .../unreleased/feature-secure-eslint-to-core.yml | 5 + changelogs/unreleased/revert-bc8546a9.yml | 5 + config/routes.rb | 1 + crowdin.yml | 4 +- doc/administration/gitaly/praefect.md | 32 +- doc/user/application_security/sast/index.md | 9 +- doc/user/project/settings/import_export.md | 2 +- .../ci/templates/Security/SAST.gitlab-ci.yml | 1 - lib/gitlab/import_export/project/import_export.yml | 9 - .../import_export/project/relation_factory.rb | 11 +- locale/gitlab.pot | 47 +- spec/controllers/search_controller_spec.rb | 5 + spec/features/issuables/issuable_list_spec.rb | 2 +- .../import_export/test_project_export.tar.gz | Bin 3360 -> 3176 bytes spec/features/signed_commits_spec.rb | 7 + .../import_export/corrupted_project_export.tar.gz | Bin 3973 -> 3846 bytes .../lightweight_project_export.tar.gz | Bin 3778 -> 3647 bytes .../lib/gitlab/import_export/complex/project.json | 370 --------------- .../lib/gitlab/import_export/designs/project.json | 3 - .../lib/gitlab/import_export/light/project.json | 42 -- .../with_invalid_records/project.json | 1 - .../components/blob_header_default_actions_spec.js | 5 +- spec/frontend/environment.js | 7 +- .../fixtures/static/global_search_input.html | 15 - .../fixtures/static/search_autocomplete.html | 15 + spec/frontend/global_search_input_spec.js | 215 --------- spec/frontend/helpers/test_constants.js | 22 +- .../components/issuables_list_app_spec.js | 54 +++ .../monitoring/components/dashboard_spec.js | 3 +- spec/frontend/monitoring/utils_spec.js | 2 +- spec/frontend/notes/stores/actions_spec.js | 57 ++- .../components/details_page/tags_list_row_spec.js | 119 +++-- spec/frontend/repository/utils/dom_spec.js | 3 +- spec/frontend/search_autocomplete_spec.js | 283 ++++++++++++ .../components/self_monitor_form_spec.js | 3 +- spec/helpers/search_helper_spec.rb | 93 ++++ .../import_export/fast_hash_serializer_spec.rb | 8 - .../import_export/project/tree_restorer_spec.rb | 21 +- .../import_export/project/tree_saver_spec.rb | 13 - .../gitlab/import_export/safe_model_attributes.yml | 30 -- spec/services/ci/create_pipeline_service_spec.rb | 2 +- .../jira/requests/issues/list_service_spec.rb | 10 + 64 files changed, 1560 insertions(+), 1288 deletions(-) delete mode 100644 app/assets/javascripts/global_search_input.js create mode 100644 app/assets/javascripts/search_autocomplete.js create mode 100644 changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml create mode 100644 changelogs/unreleased/220342-remove-services-from-import-export.yml create mode 100644 changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml create mode 100644 changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml create mode 100644 changelogs/unreleased/feature-secure-eslint-to-core.yml create mode 100644 changelogs/unreleased/revert-bc8546a9.yml delete mode 100644 spec/frontend/fixtures/static/global_search_input.html create mode 100644 spec/frontend/fixtures/static/search_autocomplete.html delete mode 100644 spec/frontend/global_search_input_spec.js create mode 100644 spec/frontend/search_autocomplete_spec.js diff --git a/app/assets/javascripts/global_search_input.js b/app/assets/javascripts/global_search_input.js deleted file mode 100644 index a7c121259d4..00000000000 --- a/app/assets/javascripts/global_search_input.js +++ /dev/null @@ -1,425 +0,0 @@ -/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */ - -import $ from 'jquery'; -import { throttle } from 'lodash'; -import { s__, __, sprintf } from '~/locale'; -import { - isInGroupsPage, - isInProjectPage, - getGroupSlug, - getProjectSlug, - spriteIcon, -} from './lib/utils/common_utils'; - -/** - * Search input in top navigation bar. - * On click, opens a dropdown - * As the user types it filters the results - * When the user clicks `x` button it cleans the input and closes the dropdown. - */ - -const KEYCODE = { - ESCAPE: 27, - BACKSPACE: 8, - ENTER: 13, - UP: 38, - DOWN: 40, -}; - -function setSearchOptions() { - const $projectOptionsDataEl = $('.js-search-project-options'); - const $groupOptionsDataEl = $('.js-search-group-options'); - const $dashboardOptionsDataEl = $('.js-search-dashboard-options'); - - if ($projectOptionsDataEl.length) { - gl.projectOptions = gl.projectOptions || {}; - - const projectPath = $projectOptionsDataEl.data('projectPath'); - - gl.projectOptions[projectPath] = { - name: $projectOptionsDataEl.data('name'), - issuesPath: $projectOptionsDataEl.data('issuesPath'), - issuesDisabled: $projectOptionsDataEl.data('issuesDisabled'), - mrPath: $projectOptionsDataEl.data('mrPath'), - }; - } - - if ($groupOptionsDataEl.length) { - gl.groupOptions = gl.groupOptions || {}; - - const groupPath = $groupOptionsDataEl.data('groupPath'); - - gl.groupOptions[groupPath] = { - name: $groupOptionsDataEl.data('name'), - issuesPath: $groupOptionsDataEl.data('issuesPath'), - mrPath: $groupOptionsDataEl.data('mrPath'), - }; - } - - if ($dashboardOptionsDataEl.length) { - gl.dashboardOptions = { - name: s__('SearchAutocomplete|All GitLab'), - issuesPath: $dashboardOptionsDataEl.data('issuesPath'), - mrPath: $dashboardOptionsDataEl.data('mrPath'), - }; - } -} - -export class GlobalSearchInput { - constructor({ wrap } = {}) { - setSearchOptions(); - this.bindEventContext(); - this.wrap = wrap || $('.search'); - this.dropdown = this.wrap.find('.dropdown'); - this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); - this.dropdownMenu = this.dropdown.find('.dropdown-menu'); - this.dropdownContent = this.dropdown.find('.dropdown-content'); - this.scopeInputEl = this.getElement('#scope'); - this.searchInput = this.getElement('.search-input'); - this.projectInputEl = this.getElement('#search_project_id'); - this.groupInputEl = this.getElement('#group_id'); - this.searchCodeInputEl = this.getElement('#search_code'); - this.repositoryInputEl = this.getElement('#repository_ref'); - this.clearInput = this.getElement('.js-clear-input'); - this.scrollFadeInitialized = false; - this.saveOriginalState(); - - // Only when user is logged in - if (gon.current_user_id) { - this.createGlobalSearchInput(); - } - - this.bindEvents(); - this.dropdownToggle.dropdown(); - this.searchInput.addClass('js-autocomplete-disabled'); - } - - // Finds an element inside wrapper element - bindEventContext() { - this.onSearchInputBlur = this.onSearchInputBlur.bind(this); - this.onClearInputClick = this.onClearInputClick.bind(this); - this.onSearchInputFocus = this.onSearchInputFocus.bind(this); - this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); - this.onSearchInputChange = this.onSearchInputChange.bind(this); - this.setScrollFade = this.setScrollFade.bind(this); - } - getElement(selector) { - return this.wrap.find(selector); - } - - saveOriginalState() { - return (this.originalState = this.serializeState()); - } - - createGlobalSearchInput() { - return this.searchInput.glDropdown({ - filterInputBlur: false, - filterable: true, - filterRemote: true, - highlight: true, - icon: true, - enterCallback: false, - filterInput: 'input#search', - search: { - fields: ['text'], - }, - id: this.getSearchText, - data: this.getData.bind(this), - selectable: true, - clicked: this.onClick.bind(this), - }); - } - - getSearchText(selectedObject) { - return selectedObject.id ? selectedObject.text : ''; - } - - getData(term, callback) { - if (!term) { - const contents = this.getCategoryContents(); - if (contents) { - const glDropdownInstance = this.searchInput.data('glDropdown'); - - if (glDropdownInstance) { - glDropdownInstance.filter.options.callback(contents); - } - this.enableDropdown(); - } - return; - } - - const options = this.scopedSearchOptions(term); - - callback(options); - - this.highlightFirstRow(); - this.setScrollFade(); - } - - // Add option to proceed with the search for each - // scope that is currently available, namely: - // - // - Search in this project - // - Search in this group (or project's group) - // - Search in all GitLab - scopedSearchOptions(term) { - const icon = spriteIcon('search', 's16 inline-search-icon'); - const projectId = this.projectInputEl.val(); - const groupId = this.groupInputEl.val(); - const options = []; - - if (projectId) { - const projectOptions = gl.projectOptions[getProjectSlug()]; - const url = groupId - ? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}` - : `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}`; - - options.push({ - icon, - text: term, - template: sprintf( - s__(`SearchAutocomplete|in project %{projectName}`), - { - projectName: `${projectOptions.name}`, - }, - false, - ), - url, - }); - } - - if (groupId) { - const groupOptions = gl.groupOptions[getGroupSlug()]; - options.push({ - icon, - text: term, - template: sprintf( - s__(`SearchAutocomplete|in group %{groupName}`), - { - groupName: `${groupOptions.name}`, - }, - false, - ), - url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}`, - }); - } - - options.push({ - icon, - text: term, - template: s__('SearchAutocomplete|in all GitLab'), - url: `${gon.relative_url_root}/search?search=${term}`, - }); - - return options; - } - - serializeState() { - return { - // Search Criteria - search_project_id: this.projectInputEl.val(), - group_id: this.groupInputEl.val(), - search_code: this.searchCodeInputEl.val(), - repository_ref: this.repositoryInputEl.val(), - scope: this.scopeInputEl.val(), - }; - } - - bindEvents() { - this.searchInput.on('input', this.onSearchInputChange); - this.searchInput.on('keyup', this.onSearchInputKeyUp); - this.searchInput.on('focus', this.onSearchInputFocus); - this.searchInput.on('blur', this.onSearchInputBlur); - this.clearInput.on('click', this.onClearInputClick); - this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250)); - - this.searchInput.on('click', e => { - e.stopPropagation(); - }); - } - - enableDropdown() { - this.setScrollFade(); - - // No need to enable anything if user is not logged in - if (!gon.current_user_id) { - return; - } - - // If the dropdown is closed, we'll open it - if (!this.dropdown.hasClass('show')) { - this.loadingSuggestions = false; - this.dropdownToggle.dropdown('toggle'); - return this.searchInput.removeClass('js-autocomplete-disabled'); - } - } - - onSearchInputChange() { - this.enableDropdown(); - } - - onSearchInputKeyUp(e) { - switch (e.keyCode) { - case KEYCODE.ESCAPE: - this.restoreOriginalState(); - break; - case KEYCODE.ENTER: - this.disableDropdown(); - break; - default: - } - this.wrap.toggleClass('has-value', Boolean(e.target.value)); - } - - onSearchInputFocus() { - this.isFocused = true; - this.wrap.addClass('search-active'); - if (this.getValue() === '') { - return this.getData(); - } - } - - getValue() { - return this.searchInput.val(); - } - - onClearInputClick(e) { - e.preventDefault(); - this.wrap.toggleClass('has-value', Boolean(e.target.value)); - return this.searchInput.val('').focus(); - } - - onSearchInputBlur() { - this.isFocused = false; - this.wrap.removeClass('search-active'); - // If input is blank then restore state - if (this.searchInput.val() === '') { - this.restoreOriginalState(); - } - this.dropdownMenu.removeClass('show'); - } - - restoreOriginalState() { - const inputs = Object.keys(this.originalState); - for (let i = 0, len = inputs.length; i < len; i += 1) { - const input = inputs[i]; - this.getElement(`#${input}`).val(this.originalState[input]); - } - } - - resetSearchState() { - const inputs = Object.keys(this.originalState); - const results = []; - for (let i = 0, len = inputs.length; i < len; i += 1) { - const input = inputs[i]; - results.push(this.getElement(`#${input}`).val('')); - } - return results; - } - - disableDropdown() { - if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) { - this.searchInput.addClass('js-autocomplete-disabled'); - this.dropdownToggle.dropdown('toggle'); - this.restoreMenu(); - } - } - - restoreMenu() { - const html = ``; - return this.dropdownContent.html(html); - } - - onClick(item, $el, e) { - if (window.location.pathname.indexOf(item.url) !== -1) { - if (!e.metaKey) e.preventDefault(); - $el.removeClass('is-active'); - this.disableDropdown(); - return this.searchInput.val('').focus(); - } - } - - highlightFirstRow() { - this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0); - } - - getCategoryContents() { - const userName = gon.current_username; - const { projectOptions, groupOptions, dashboardOptions } = gl; - - // Get options - let options; - if (isInProjectPage() && projectOptions) { - options = projectOptions[getProjectSlug()]; - } else if (isInGroupsPage() && groupOptions) { - options = groupOptions[getGroupSlug()]; - } else if (dashboardOptions) { - options = dashboardOptions; - } - - const { issuesPath, mrPath, name, issuesDisabled } = options; - const baseItems = []; - - if (name) { - baseItems.push({ - type: 'header', - content: `${name}`, - }); - } - - const issueItems = [ - { - text: s__('SearchAutocomplete|Issues assigned to me'), - url: `${issuesPath}/?assignee_username=${userName}`, - }, - { - text: s__("SearchAutocomplete|Issues I've created"), - url: `${issuesPath}/?author_username=${userName}`, - }, - ]; - const mergeRequestItems = [ - { - text: s__('SearchAutocomplete|Merge requests assigned to me'), - url: `${mrPath}/?assignee_username=${userName}`, - }, - { - text: s__("SearchAutocomplete|Merge requests I've created"), - url: `${mrPath}/?author_username=${userName}`, - }, - ]; - - let items; - if (issuesDisabled) { - items = baseItems.concat(mergeRequestItems); - } else { - items = baseItems.concat(...issueItems, ...mergeRequestItems); - } - return items; - } - - isScrolledUp() { - const el = this.dropdownContent[0]; - const currentPosition = this.contentClientHeight + el.scrollTop; - - return currentPosition < this.maxPosition; - } - - initScrollFade() { - const el = this.dropdownContent[0]; - this.scrollFadeInitialized = true; - - this.contentClientHeight = el.clientHeight; - this.maxPosition = el.scrollHeight; - this.dropdownMenu.addClass('dropdown-content-faded-mask'); - } - - setScrollFade() { - this.initScrollFade(); - - this.dropdownMenu.toggleClass('fade-out', !this.isScrolledUp()); - } -} - -export default function initGlobalSearchInput(opts) { - return new GlobalSearchInput(opts); -} diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index 9af1887ef12..04991a8d374 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -365,7 +365,7 @@ export default { :title="__('Comments')" :class="{ 'no-comments': hasNoComments }" > - + {{ userNotesCount }} diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index db18bcbce09..e1a40323f5d 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -12,8 +12,10 @@ import { import { __ } from '~/locale'; import initManualOrdering from '~/manual_ordering'; import Issuable from './issuable.vue'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import { sortOrderMap, + availableSortOptionsJira, RELATIVE_POSITION, PAGE_SIZE, PAGE_SIZE_MANUAL, @@ -29,6 +31,7 @@ export default { GlPagination, GlSkeletonLoading, Issuable, + FilteredSearchBar, }, props: { canBulkEdit: { @@ -50,14 +53,25 @@ export default { type: String, required: true, }, + projectPath: { + type: String, + required: false, + default: '', + }, sortKey: { type: String, required: false, default: '', }, + type: { + type: String, + required: false, + default: '', + }, }, data() { return { + availableSortOptionsJira, filters: {}, isBulkEditing: false, issuables: [], @@ -141,6 +155,22 @@ export default { nextPage: this.paginationNext, }; }, + isJira() { + return this.type === 'jira'; + }, + initialFilterValue() { + const value = []; + const { search } = this.getQueryObject(); + + if (search) { + value.push(search); + } + return value; + }, + initialSortBy() { + const { sort } = this.getQueryObject(); + return sort || 'created_desc'; + }, }, watch: { selection() { @@ -262,51 +292,92 @@ export default { this.filters = filters; }, + refetchIssuables() { + const ignored = ['utf8', 'state']; + const params = omit(this.filters, ignored); + + historyPushState(setUrlParams(params, window.location.href, true)); + this.fetchIssuables(); + }, + handleFilter(filters) { + let search = null; + + filters.forEach(filter => { + if (typeof filter === 'string') { + search = filter; + } + }); + + this.filters.search = search; + this.page = 1; + + this.refetchIssuables(); + }, + handleSort(sort) { + this.filters.sort = sort; + this.page = 1; + + this.refetchIssuables(); + }, }, }; diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issuables_list/constants.js index 71b9c52c703..e240efd2804 100644 --- a/app/assets/javascripts/issuables_list/constants.js +++ b/app/assets/javascripts/issuables_list/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + // Maps sort order as it appears in the URL query to API `order_by` and `sort` params. const PRIORITY = 'priority'; const ASC = 'asc'; @@ -31,3 +33,22 @@ export const sortOrderMap = { weight_desc: { order_by: WEIGHT, sort: DESC }, weight: { order_by: WEIGHT, sort: ASC }, }; + +export const availableSortOptionsJira = [ + { + id: 1, + title: __('Created date'), + sortDirection: { + descending: 'created_desc', + ascending: 'created_asc', + }, + }, + { + id: 2, + title: __('Last updated'), + sortDirection: { + descending: 'updated_desc', + ascending: 'updated_asc', + }, + }, +]; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index b742fe42024..3f85295a5ed 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -33,7 +33,7 @@ import initFrequentItemDropdowns from './frequent_items'; import initBreadcrumbs from './breadcrumb'; import initUsagePingConsent from './usage_ping_consent'; import initPerformanceBar from './performance_bar'; -import initGlobalSearchInput from './global_search_input'; +import initSearchAutocomplete from './search_autocomplete'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import initBroadcastNotifications from './broadcast_notification'; @@ -113,7 +113,7 @@ function deferredInitialisation() { initFrequentItemDropdowns(); initPersistentUserCallouts(); - if (document.querySelector('.search')) initGlobalSearchInput(); + if (document.querySelector('.search')) initSearchAutocomplete(); addSelectOnFocusBehaviour('.js-select-on-focus'); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 32af62fe6f1..8a7734f4d31 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -402,9 +402,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }; const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { - if (resp.notes && resp.notes.length) { - updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes); - + if (resp.notes?.length) { + dispatch('updateOrCreateNotes', resp.notes); dispatch('startTaskList'); } @@ -424,12 +423,12 @@ const getFetchDataParams = state => { return { endpoint, options }; }; -export const fetchData = ({ commit, state, getters }) => { +export const fetchData = ({ commit, state, getters, dispatch }) => { const { endpoint, options } = getFetchDataParams(state); axios .get(endpoint, options) - .then(({ data }) => pollSuccessCallBack(data, commit, state, getters)) + .then(({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch)) .catch(() => Flash(__('Something went wrong while fetching latest comments.'))); }; @@ -449,7 +448,7 @@ export const poll = ({ commit, state, getters, dispatch }) => { if (!Visibility.hidden()) { eTagPoll.makeRequest(); } else { - fetchData({ commit, state, getters }); + dispatch('fetchData'); } Visibility.change(() => { diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index d9816dc5102..51ba2337db6 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -1,5 +1,5 @@ @@ -94,6 +102,7 @@ export default { @@ -146,7 +162,7 @@ export default { -