diff --git a/app/assets/javascripts/pages/search/show/highlight_blob_search_result.js b/app/assets/javascripts/pages/search/show/highlight_blob_search_result.js new file mode 100644 index 00000000000..e17c87735b4 --- /dev/null +++ b/app/assets/javascripts/pages/search/show/highlight_blob_search_result.js @@ -0,0 +1,15 @@ +export default () => { + const highlightLineClass = 'hll'; + const contentBody = document.getElementById('content-body'); + const searchTerm = contentBody.querySelector('.js-search-input').value.toLowerCase(); + const blobs = contentBody.querySelectorAll('.blob-result'); + + blobs.forEach(blob => { + const lines = blob.querySelectorAll('.line'); + lines.forEach(line => { + if (line.textContent.toLowerCase().includes(searchTerm)) { + line.classList.add(highlightLineClass); + } + }); + }); +}; diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index dff9d855b67..4050f2f13f1 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -5,9 +5,11 @@ import Api from '~/api'; import { __ } from '~/locale'; import Project from '~/pages/projects/project'; import refreshCounts from './refresh_counts'; +import setHighlightClass from './highlight_blob_search_result'; export default class Search { constructor() { + setHighlightClass(); const $groupDropdown = $('.js-search-group-dropdown'); const $projectDropdown = $('.js-search-project-dropdown'); diff --git a/app/controllers/admin/serverless/domains_controller.rb b/app/controllers/admin/serverless/domains_controller.rb index 9741a0716f2..1d4f10e033f 100644 --- a/app/controllers/admin/serverless/domains_controller.rb +++ b/app/controllers/admin/serverless/domains_controller.rb @@ -9,7 +9,7 @@ class Admin::Serverless::DomainsController < Admin::ApplicationController end def create - if PagesDomain.instance_serverless.count > 0 + if PagesDomain.instance_serverless.exists? return redirect_to admin_serverless_domains_path, notice: _('An instance-level serverless domain already exists.') end @@ -31,7 +31,7 @@ class Admin::Serverless::DomainsController < Admin::ApplicationController end def destroy - if domain.serverless_domain_clusters.count > 0 + if domain.serverless_domain_clusters.exists? return redirect_to admin_serverless_domains_path, status: :conflict, notice: _('Domain cannot be deleted while associated to one or more clusters.') diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index e73e6476c12..cc91fd4b4d2 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -13,13 +13,8 @@ module Labels new_label = clone_label_to_group_label(label) label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids| - update_issuables(new_label, batched_ids) - update_resource_label_events(new_label, batched_ids) - update_issue_board_lists(new_label, batched_ids) - update_priorities(new_label, batched_ids) - subscribe_users(new_label, batched_ids) - # Order is important, project labels need to be last - update_project_labels(batched_ids) + update_old_label_relations(new_label, batched_ids) + destroy_project_labels(batched_ids) end # We skipped validations during creation. Let's run them now, after deleting conflicting labels @@ -32,6 +27,14 @@ module Labels private + def update_old_label_relations(new_label, old_label_ids) + update_issuables(new_label, old_label_ids) + update_resource_label_events(new_label, old_label_ids) + update_issue_board_lists(new_label, old_label_ids) + update_priorities(new_label, old_label_ids) + subscribe_users(new_label, old_label_ids) + end + # rubocop: disable CodeReuse/ActiveRecord def subscribe_users(new_label, label_ids) # users can be subscribed to multiple labels that will be merged into the group one @@ -86,7 +89,7 @@ module Labels # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord - def update_project_labels(label_ids) + def destroy_project_labels(label_ids) Label.where(id: label_ids).destroy_all # rubocop: disable DestroyAll end # rubocop: enable CodeReuse/ActiveRecord @@ -105,3 +108,5 @@ module Labels end end end + +Labels::PromoteService.prepend_if_ee('EE::Labels::PromoteService') diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index fae7d6526e8..902a6e9b363 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -17,7 +17,7 @@ -# Populated by app/assets/javascripts/dropzone_input.js %span.uploading-progress 0% %span.uploading-spinner - = icon('spinner spin', class: 'toolbar-button-icon') + .toolbar-button-icon.spinner.align-text-top %span.uploading-error-container.hide %span.uploading-error-icon diff --git a/changelogs/unreleased/196384-highlight-code-search-result-line.yml b/changelogs/unreleased/196384-highlight-code-search-result-line.yml new file mode 100644 index 00000000000..68de400ec18 --- /dev/null +++ b/changelogs/unreleased/196384-highlight-code-search-result-line.yml @@ -0,0 +1,5 @@ +--- +title: Highlight line which includes search term is code search results +merge_request: 22914 +author: Alex Terekhov (terales) +type: added diff --git a/changelogs/unreleased/23206-board-lists-lose-their-filter-label-when-said-label-becomes-a-group.yml b/changelogs/unreleased/23206-board-lists-lose-their-filter-label-when-said-label-becomes-a-group.yml new file mode 100644 index 00000000000..d79ec320e95 --- /dev/null +++ b/changelogs/unreleased/23206-board-lists-lose-their-filter-label-when-said-label-becomes-a-group.yml @@ -0,0 +1,5 @@ +--- +title: Update board scopes when promoting a label +merge_request: 27662 +author: +type: fixed diff --git a/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-shared-notes.yml b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-shared-notes.yml new file mode 100644 index 00000000000..c370e26c316 --- /dev/null +++ b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-shared-notes.yml @@ -0,0 +1,5 @@ +--- +title: Migrate .fa-spinner to .spinner for app/views/shared/notes +merge_request: 25028 +author: nuwe1 +type: other diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 6d02b2905e2..2d78174d669 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2548,7 +2548,7 @@ type EpicIssue implements Noteable { epicIssueId: ID! """ - Current health status. Available only when feature flag `save_issuable_health_status` is enabled + Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. """ healthStatus: HealthStatus @@ -3539,7 +3539,7 @@ type Issue implements Noteable { epic: Epic """ - Current health status. Available only when feature flag `save_issuable_health_status` is enabled + Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. """ healthStatus: HealthStatus diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 40053199d72..2be4573cafe 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -7397,7 +7397,7 @@ }, { "name": "healthStatus", - "description": "Current health status. Available only when feature flag `save_issuable_health_status` is enabled", + "description": "Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.", "args": [ ], @@ -10125,7 +10125,7 @@ }, { "name": "healthStatus", - "description": "Current health status. Available only when feature flag `save_issuable_health_status` is enabled", + "description": "Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.", "args": [ ], diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index cce8603a54f..6a79a407dac 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -419,7 +419,7 @@ Relationship between an epic and an issue | `dueDate` | Time | Due date of the issue | | `epic` | Epic | Epic to which this issue belongs | | `epicIssueId` | ID! | ID of the epic-issue relation | -| `healthStatus` | HealthStatus | Current health status. Available only when feature flag `save_issuable_health_status` is enabled | +| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. | | `id` | ID | Global ID of the epic-issue relation | | `iid` | ID! | Internal ID of the issue | | `milestone` | Milestone | Milestone of the issue | @@ -540,7 +540,7 @@ Autogenerated return type of EpicTreeReorder | `downvotes` | Int! | Number of downvotes the issue has received | | `dueDate` | Time | Due date of the issue | | `epic` | Epic | Epic to which this issue belongs | -| `healthStatus` | HealthStatus | Current health status. Available only when feature flag `save_issuable_health_status` is enabled | +| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. | | `iid` | ID! | Internal ID of the issue | | `milestone` | Milestone | Milestone of the issue | | `reference` | String! | Internal reference of the issue. Returned in shortened format by default | diff --git a/lib/api/users.rb b/lib/api/users.rb index 1ca222b4ed5..1694f3fe3fb 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -206,11 +206,11 @@ module API conflict!('Email has already been taken') if params[:email] && User.by_any_email(params[:email].downcase) - .where.not(id: user.id).count > 0 + .where.not(id: user.id).exists? conflict!('Username has already been taken') if params[:username] && User.by_username(params[:username]) - .where.not(id: user.id).count > 0 + .where.not(id: user.id).exists? user_params = declared_params(include_missing: false) diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb index 025cc53c745..cbe3e373986 100644 --- a/spec/frontend/fixtures/search.rb +++ b/spec/frontend/fixtures/search.rb @@ -16,4 +16,19 @@ describe SearchController, '(JavaScript fixtures)', type: :controller do expect(response).to be_successful end + + context 'search within a project' do + let(:namespace) { create(:namespace, name: 'frontend-fixtures') } + let(:project) { create(:project, :public, :repository, namespace: namespace, path: 'search-project') } + + it 'search/blob_search_result.html' do + get :show, params: { + search: 'Send', + project_id: project.id, + scope: :blobs + } + + expect(response).to be_successful + end + end end diff --git a/spec/frontend/pages/search/show/highlight_blob_search_result_spec.js b/spec/frontend/pages/search/show/highlight_blob_search_result_spec.js new file mode 100644 index 00000000000..4083a65df75 --- /dev/null +++ b/spec/frontend/pages/search/show/highlight_blob_search_result_spec.js @@ -0,0 +1,15 @@ +import setHighlightClass from '~/pages/search/show/highlight_blob_search_result'; + +const fixture = 'search/blob_search_result.html'; + +describe('pages/search/show/highlight_blob_search_result', () => { + preloadFixtures(fixture); + + beforeEach(() => loadFixtures(fixture)); + + it('highlights lines with search term occurrence', () => { + setHighlightClass(); + + expect(document.querySelectorAll('.blob-result .hll').length).toBe(11); + }); +}); diff --git a/spec/frontend/search_spec.js b/spec/frontend/search_spec.js index af93fa88f72..1573365538c 100644 --- a/spec/frontend/search_spec.js +++ b/spec/frontend/search_spec.js @@ -1,8 +1,10 @@ import $ from 'jquery'; import Api from '~/api'; import Search from '~/pages/search/show/search'; +import setHighlightClass from '~/pages/search/show/highlight_blob_search_result'; jest.mock('~/api'); +jest.mock('~/pages/search/show/highlight_blob_search_result'); describe('Search', () => { const fixturePath = 'search/show.html'; @@ -16,27 +18,41 @@ describe('Search', () => { preloadFixtures(fixturePath); - beforeEach(() => { - loadFixtures(fixturePath); - new Search(); // eslint-disable-line no-new - }); - - it('requests groups from backend when filtering', () => { - jest.spyOn(Api, 'groups').mockImplementation(term => { - expect(term).toBe(searchTerm); + describe('constructor side effects', () => { + afterEach(() => { + jest.restoreAllMocks(); }); - const inputElement = fillDropdownInput('.js-search-group-dropdown'); + it('highlights lines with search terms in blob search results', () => { + new Search(); // eslint-disable-line no-new - $(inputElement).trigger('input'); + expect(setHighlightClass).toHaveBeenCalled(); + }); }); - it('requests projects from backend when filtering', () => { - jest.spyOn(Api, 'projects').mockImplementation(term => { - expect(term).toBe(searchTerm); + describe('dropdown behavior', () => { + beforeEach(() => { + loadFixtures(fixturePath); + new Search(); // eslint-disable-line no-new }); - const inputElement = fillDropdownInput('.js-search-project-dropdown'); - $(inputElement).trigger('input'); + it('requests groups from backend when filtering', () => { + jest.spyOn(Api, 'groups').mockImplementation(term => { + expect(term).toBe(searchTerm); + }); + + const inputElement = fillDropdownInput('.js-search-group-dropdown'); + + $(inputElement).trigger('input'); + }); + + it('requests projects from backend when filtering', () => { + jest.spyOn(Api, 'projects').mockImplementation(term => { + expect(term).toBe(searchTerm); + }); + const inputElement = fillDropdownInput('.js-search-project-dropdown'); + + $(inputElement).trigger('input'); + }); }); });