From 7ec4725142f51181ceae434e1d785b8fe34d248e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 13 May 2024 03:16:30 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../gitlab-com/danger-review.gitlab-ci.yml | 2 +- .../ci/package-and-test/main.gitlab-ci.yml | 46 ++++++ GITALY_SERVER_VERSION | 2 +- .../components/filtered_search_and_sort.vue | 16 +- .../pages/explore/projects/index.js | 4 +- .../components/filtered_search_and_sort.vue | 138 +++++++++++++++++ .../javascripts/projects/explore/constants.js | 33 ++++ .../javascripts/projects/explore/index.js | 29 ++++ .../filtered_search_utils.js | 14 +- app/helpers/projects_helper.rb | 9 ++ app/helpers/sidebars_helper.rb | 2 +- app/views/dashboard/_projects_head.html.haml | 2 +- app/views/explore/projects/_nav.html.haml | 9 +- app/views/shared/projects/_list.html.haml | 2 +- doc/ci/components/index.md | 10 +- doc/ci/secrets/gcp_secret_manager.md | 24 +++ lib/sidebars/explore/menus/projects_menu.rb | 12 +- .../project_archive_compare_spec.rb | 7 +- spec/features/dashboard/projects_spec.rb | 2 +- spec/features/dashboard/root_explore_spec.rb | 7 +- .../explore/user_explores_projects_spec.rb | 21 ++- .../projects/user_sorts_projects_spec.rb | 19 ++- .../filtered_search_and_sort_spec.js | 10 ++ .../filtered_search_and_sort_spec.js | 145 ++++++++++++++++++ .../filtered_search_utils_spec.js | 29 ++++ spec/helpers/projects_helper_spec.rb | 13 ++ spec/helpers/sidebars_helper_spec.rb | 2 +- 27 files changed, 563 insertions(+), 46 deletions(-) create mode 100644 app/assets/javascripts/projects/explore/components/filtered_search_and_sort.vue create mode 100644 app/assets/javascripts/projects/explore/constants.js create mode 100644 app/assets/javascripts/projects/explore/index.js create mode 100644 spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js diff --git a/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml b/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml index 19567ea45ae..66360fa1b7a 100644 --- a/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml +++ b/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml @@ -1,6 +1,6 @@ include: - project: gitlab-org/quality/pipeline-common - ref: 8.10.0 + ref: 8.11.0 file: - /ci/danger-review.yml diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 5bc715ea302..626e57cd9d9 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -115,6 +115,7 @@ praefect: variables: QA_SCENARIO: Test::Integration::Praefect QA_CAN_TEST_PRAEFECT: "true" + QA_GITALY_TRANSACTIONS_ENABLED: "false" KNAPSACK_TEST_FILE_PATTERN: "qa/specs/features/**/3_create/**/*_spec.rb" rules: - !reference [.rules:test:smoke-for-omnibus-mr, rules] @@ -126,6 +127,7 @@ praefect-selective: variables: QA_SCENARIO: Test::Integration::Praefect QA_CAN_TEST_PRAEFECT: "true" + QA_GITALY_TRANSACTIONS_ENABLED: "false" rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::All/ @@ -138,6 +140,50 @@ praefect-selective-parallel: variables: QA_SCENARIO: Test::Integration::Praefect QA_CAN_TEST_PRAEFECT: "true" + QA_GITALY_TRANSACTIONS_ENABLED: "false" + KNAPSACK_TEST_FILE_PATTERN: "qa/specs/features/**/3_create/**/*_spec.rb" + rules: + - !reference [.rules:test:qa-selective-parallel, rules] + - if: $QA_SUITES =~ /Test::Instance::All/ + variables: + QA_TESTS: "" + +# ========== gitaly transactions enabled =========== +# https://docs.gitlab.com/ee/architecture/blueprints/gitaly_transaction_management/ +gitaly-transactions: + extends: + - .parallel + - .qa + parallel: 2 + variables: + QA_SCENARIO: Test::Integration::Praefect + QA_CAN_TEST_PRAEFECT: "true" + KNAPSACK_TEST_FILE_PATTERN: "qa/specs/features/**/3_create/**/*_spec.rb" + QA_GITALY_TRANSACTIONS_ENABLED: "true" + rules: + - !reference [.rules:test:smoke-for-omnibus-mr, rules] + - !reference [.rules:test:qa-parallel, rules] + - if: $QA_SUITES =~ /Test::Instance::All/ + +gitaly-transactions-selective: + extends: .qa + variables: + QA_SCENARIO: Test::Integration::Praefect + QA_CAN_TEST_PRAEFECT: "true" + QA_GITALY_TRANSACTIONS_ENABLED: "true" + rules: + - !reference [.rules:test:qa-selective, rules] + - if: $QA_SUITES =~ /Test::Instance::All/ + +gitaly-transactions-selective-parallel: + extends: + - .qa + - .parallel + parallel: 2 + variables: + QA_SCENARIO: Test::Integration::Praefect + QA_CAN_TEST_PRAEFECT: "true" + QA_GITALY_TRANSACTIONS_ENABLED: "true" KNAPSACK_TEST_FILE_PATTERN: "qa/specs/features/**/3_create/**/*_spec.rb" rules: - !reference [.rules:test:qa-selective-parallel, rules] diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0ca8d83c64a..89792d2a0fd 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1f796d76e13237a957abccde4a07fb5b2d177241 +677e0ab263cf525c6e9557d33908cf55f0a13bd2 diff --git a/app/assets/javascripts/groups_projects/components/filtered_search_and_sort.vue b/app/assets/javascripts/groups_projects/components/filtered_search_and_sort.vue index ac10e60d1b7..1c803d24009 100644 --- a/app/assets/javascripts/groups_projects/components/filtered_search_and_sort.vue +++ b/app/assets/javascripts/groups_projects/components/filtered_search_and_sort.vue @@ -64,7 +64,10 @@ export default { }, sortOptions: { type: Array, - required: true, + required: false, + default() { + return []; + }, }, activeSortOption: { type: Object, @@ -80,12 +83,18 @@ export default { const tokens = prepareTokens( urlQueryToFilter(this.filteredSearchQuery, { filteredSearchTermKey: this.filteredSearchTermKey, - filterNamesAllowList: [FILTERED_SEARCH_TERM], + filterNamesAllowList: [ + FILTERED_SEARCH_TERM, + ...this.filteredSearchTokens.map(({ type }) => type), + ], }), ); return tokens.length ? tokens : [TOKEN_EMPTY_SEARCH_TERM]; }, + shouldShowSort() { + return this.sortOptions.length; + }, }, methods: { onFilter(filters) { @@ -93,6 +102,7 @@ export default { 'filter', filterToQueryObject(processFilters(filters), { filteredSearchTermKey: this.filteredSearchTermKey, + shouldExcludeEmpty: true, }), ); }, @@ -118,7 +128,7 @@ export default {
-
+
+import { GlFilteredSearchToken } from '@gitlab/ui'; +import { __ } from '~/locale'; +import FilteredSearchAndSort from '~/groups_projects/components/filtered_search_and_sort.vue'; +import { RECENT_SEARCHES_STORAGE_KEY_PROJECTS } from '~/filtered_search/recent_searches_storage_keys'; +import { queryToObject, objectToQuery, visitUrl } from '~/lib/utils/url_utility'; +import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + SORT_OPTIONS, + SORT_DIRECTION_ASC, + SORT_DIRECTION_DESC, + SORT_OPTION_UPDATED, + FILTERED_SEARCH_TERM_KEY, + FILTERED_SEARCH_NAMESPACE, +} from '../constants'; + +export default { + name: 'ProjectsExploreFilteredSearchAndSort', + filteredSearch: { + namespace: FILTERED_SEARCH_NAMESPACE, + recentSearchesStorageKey: RECENT_SEARCHES_STORAGE_KEY_PROJECTS, + searchTermKey: FILTERED_SEARCH_TERM_KEY, + }, + components: { + FilteredSearchAndSort, + }, + inject: ['initialSort', 'programmingLanguages', 'starredExploreProjectsPath', 'exploreRootPath'], + computed: { + filteredSearchTokens() { + return [ + { + type: 'language', + icon: 'lock', + title: __('Language'), + token: GlFilteredSearchToken, + unique: true, + operators: OPERATORS_IS, + options: this.programmingLanguages.map(({ id, name }) => ({ + // Cast to string so it matches value from query string + value: id.toString(), + title: name, + })), + }, + ]; + }, + queryAsObject() { + return queryToObject(document.location.search); + }, + queryAsObjectWithoutPagination() { + const { page, ...queryAsObject } = this.queryAsObject; + + return queryAsObject; + }, + sortByQuery() { + return this.queryAsObject?.sort; + }, + sortBy() { + if (this.sortByQuery) { + return this.sortByQuery; + } + + return this.initialSort; + }, + search() { + return this.queryAsObject?.[FILTERED_SEARCH_TERM_KEY] || ''; + }, + sortOptions() { + const mostStarredPathnames = [this.starredExploreProjectsPath, this.exploreRootPath]; + if (mostStarredPathnames.includes(window.location.pathname)) { + return []; + } + + return SORT_OPTIONS; + }, + activeSortOption() { + return ( + SORT_OPTIONS.find((sortItem) => this.sortBy.includes(sortItem.value)) || SORT_OPTION_UPDATED + ); + }, + isAscending() { + if (!this.sortBy) { + return true; + } + + return this.sortBy.endsWith(SORT_DIRECTION_ASC); + }, + }, + methods: { + visitUrlWithQueryObject(queryObject) { + return visitUrl(`?${objectToQuery(queryObject)}`); + }, + onSortDirectionChange(isAscending) { + const sort = `${this.activeSortOption.value}_${ + isAscending ? SORT_DIRECTION_ASC : SORT_DIRECTION_DESC + }`; + + this.visitUrlWithQueryObject({ + ...this.queryAsObjectWithoutPagination, + sort, + }); + }, + onSortByChange(sortBy) { + const sort = `${sortBy}_${this.isAscending ? SORT_DIRECTION_ASC : SORT_DIRECTION_DESC}`; + + this.visitUrlWithQueryObject({ ...this.queryAsObjectWithoutPagination, sort }); + }, + onFilter(filtersQuery) { + const queryObject = { ...filtersQuery }; + + if (this.sortByQuery) { + queryObject.sort = this.sortByQuery; + } + + if (this.queryAsObject.archived) { + queryObject.archived = this.queryAsObject.archived; + } + + this.visitUrlWithQueryObject(queryObject); + }, + }, +}; + + + diff --git a/app/assets/javascripts/projects/explore/constants.js b/app/assets/javascripts/projects/explore/constants.js new file mode 100644 index 00000000000..67fd189831a --- /dev/null +++ b/app/assets/javascripts/projects/explore/constants.js @@ -0,0 +1,33 @@ +import { __ } from '~/locale'; + +export const SORT_OPTION_NAME = { + value: 'name', + text: __('Name'), +}; + +export const SORT_OPTION_CREATED = { + value: 'created', + text: __('Created'), +}; + +export const SORT_OPTION_UPDATED = { + value: 'latest_activity', + text: __('Updated'), +}; + +export const SORT_OPTION_STARS = { + value: 'stars', + text: __('Stars'), +}; + +export const SORT_DIRECTION_ASC = 'asc'; +export const SORT_DIRECTION_DESC = 'desc'; + +export const FILTERED_SEARCH_TERM_KEY = 'name'; +export const FILTERED_SEARCH_NAMESPACE = 'explore'; +export const SORT_OPTIONS = [ + SORT_OPTION_NAME, + SORT_OPTION_CREATED, + SORT_OPTION_UPDATED, + SORT_OPTION_STARS, +]; diff --git a/app/assets/javascripts/projects/explore/index.js b/app/assets/javascripts/projects/explore/index.js new file mode 100644 index 00000000000..d7b5b83f675 --- /dev/null +++ b/app/assets/javascripts/projects/explore/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import FilteredSearchAndSort from './components/filtered_search_and_sort.vue'; + +export const initProjectsExploreFilteredSearchAndSort = () => { + const el = document.getElementById('js-projects-explore-filtered-search-and-sort'); + + if (!el) return false; + + const { + dataset: { appData }, + } = el; + + const { + initialSort, + programmingLanguages, + starredExploreProjectsPath, + exploreRootPath, + } = convertObjectPropsToCamelCase(JSON.parse(appData)); + + return new Vue({ + el, + name: 'ProjectsExploreFilteredSearchAndSortRoot', + provide: { initialSort, programmingLanguages, starredExploreProjectsPath, exploreRootPath }, + render(createElement) { + return createElement(FilteredSearchAndSort); + }, + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js index ce4a46fe3dd..9e5c7a90655 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js @@ -116,12 +116,17 @@ function filteredSearchQueryParam(filter) { * @return {Object} query object with both filter name and not-name with values */ export function filterToQueryObject(filters = {}, options = {}) { - const { filteredSearchTermKey, customOperators } = options; + const { filteredSearchTermKey, customOperators, shouldExcludeEmpty = false } = options; return Object.keys(filters).reduce((memo, key) => { const filter = filters[key]; if (typeof filteredSearchTermKey === 'string' && key === FILTERED_SEARCH_TERM && filter) { + const combinedFilteredSearchTerm = filteredSearchQueryParam(filter); + if (combinedFilteredSearchTerm === '' && shouldExcludeEmpty) { + return memo; + } + return { ...memo, [filteredSearchTermKey]: filteredSearchQueryParam(filter) }; } @@ -143,9 +148,16 @@ export function filterToQueryObject(filters = {}, options = {}) { } else { value = filter?.operator === operator ? filter.value : null; } + if (isEmpty(value)) { value = null; } + + if (shouldExcludeEmpty && (value?.[0] === '' || value === '' || value === null)) { + // eslint-disable-next-line no-continue + continue; + } + if (prefix) { result[`${prefix}[${key}]`] = value; } else { diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index beba8ffd2d6..d938635cecb 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -631,6 +631,15 @@ module ProjectsHelper 'manual-ordering' end + def projects_explore_filtered_search_and_sort_app_data + { + initial_sort: project_list_sort_by, + programming_languages: programming_languages, + starred_explore_projects_path: starred_explore_projects_path, + explore_root_path: explore_root_path + }.to_json + end + private def can_admin_project_clusters?(project) diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 24adc5e721b..bbe6d9577ae 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -399,7 +399,7 @@ module SidebarsHelper }, { title: _('Projects'), - href: explore_projects_path, + href: starred_explore_projects_path, css_class: 'dashboard-shortcuts-projects' } ] diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 7527f32274a..e7847269a5e 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -6,7 +6,7 @@ %h1.page-title.gl-font-size-h-display= _('Projects') .page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5 - = link_to _("Explore projects"), explore_projects_path + = link_to _("Explore projects"), starred_explore_projects_path - if current_user.can_create_project? = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { testid: 'new-project-button' } }) do = _("New project") diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml index ab565279238..2bf652b3d10 100644 --- a/app/views/explore/projects/_nav.html.haml +++ b/app/views/explore/projects/_nav.html.haml @@ -1,8 +1,9 @@ .top-area = gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-none'}) do - = gl_tab_link_to _('All'), explore_projects_path, { item_active: current_page?(explore_projects_path) || current_page?(explore_root_path) } - = gl_tab_link_to _('Most starred'), starred_explore_projects_path + = gl_tab_link_to _('Most starred'), starred_explore_projects_path, { item_active: current_page?(starred_explore_projects_path) || current_page?(explore_root_path) } = gl_tab_link_to _('Trending'), trending_explore_projects_path + = gl_tab_link_to _('Active'), explore_projects_path, { item_active: current_page?(explore_projects_path) && !params['archived'] } + = gl_tab_link_to _('Inactive'), explore_projects_path({ archived: 'only' }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'only' } + = gl_tab_link_to _('All'), explore_projects_path({ archived: true }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'true' } - .nav-controls - = render 'shared/projects/search_form' +#js-projects-explore-filtered-search-and-sort{ data: { app_data: projects_explore_filtered_search_and_sort_app_data } } diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index b0926bf63b2..1941e688fb2 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -26,7 +26,7 @@ - new_project_button_label = _('New project') - new_project_button_link = new_project_path - explore_projects_button_label = _('Explore projects') -- explore_projects_button_link = explore_projects_path +- explore_projects_button_link = starred_explore_projects_path - explore_groups_button_label = _('Explore groups') - explore_groups_button_link = explore_groups_path diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md index 5b970a26756..658bc39979a 100644 --- a/doc/ci/components/index.md +++ b/doc/ci/components/index.md @@ -9,12 +9,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w DETAILS: **Tier:** Free, Premium, Ultimate **Offering:** GitLab.com, Self-managed, GitLab Dedicated -**Status:** Beta -> - Introduced as an [experimental feature](../../policy/experiment-beta-support.md) in GitLab 16.0, [with a flag](../../administration/feature_flags.md) named `ci_namespace_catalog_experimental`. Disabled by default. +> - Introduced as an [experimental feature](../../policy/experiment-beta-support.md#experiment) in GitLab 16.0, [with a flag](../../administration/feature_flags.md) named `ci_namespace_catalog_experimental`. Disabled by default. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/groups/gitlab-org/-/epics/9897) in GitLab 16.2. > - [Feature flag `ci_namespace_catalog_experimental` removed](https://gitlab.com/gitlab-org/gitlab/-/issues/394772) in GitLab 16.3. -> - [Moved](https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/130824) to [Beta status](../../policy/experiment-beta-support.md) in GitLab 16.6. +> - [Moved](https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/130824) to [Beta](../../policy/experiment-beta-support.md#beta) in GitLab 16.6. +> - [Made Generally Available](https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/134062) in GitLab 17.0. A CI/CD component is a reusable single pipeline configuration unit. Use components to create a small part of a larger pipeline, or even to compose a complete pipeline configuration. @@ -210,10 +210,10 @@ In this example, referencing the component with: DETAILS: **Tier:** Free, Premium, Ultimate **Offering:** GitLab.com, Self-managed, GitLab Dedicated -**Status:** Beta -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/407249) in GitLab 16.1 as an [experiment](../../policy/experiment-beta-support.md#experiment). +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/407249) as an [experiment](../../policy/experiment-beta-support.md#experiment) in GitLab 16.1. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/432045) to [beta](../../policy/experiment-beta-support.md#beta) in GitLab 16.7. +> - [Made Generally Available](https://gitlab.com/gitlab-org/gitlab/-/issues/454306) in GitLab 17.0. The CI/CD Catalog is a list of projects with published CI/CD components you can use to extend your CI/CD workflow. diff --git a/doc/ci/secrets/gcp_secret_manager.md b/doc/ci/secrets/gcp_secret_manager.md index 692c1d64f25..f64aad99694 100644 --- a/doc/ci/secrets/gcp_secret_manager.md +++ b/doc/ci/secrets/gcp_secret_manager.md @@ -113,6 +113,30 @@ job_using_gcp_sm: token: $GCP_ID_TOKEN ``` +### Use secrets from a different GCP project + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/37487) in GitLab 17.0. + +Secret names in GCP are per-project. By default the secret named in `gcp_secret_manager:name` +is read from the project specified in `GCP_PROJECT_NUMBER`. + +To read a secret from a different project than the project containing the WIF pool, use the +fully-qualified secret name formatted as `projects//secrets/`. + +For example, if `my-project-secret` is in the GCP project number `123456789`, +then you can access the secret with: + +```yaml +job_using_gcp_sm: + # ... configured as above ... + secrets: + DATABASE_PASSWORD: + gcp_secret_manager: + name: projects/123456789/secrets/my-project-secret # fully-qualified name of the secret defined in GCP Secret Manager + version: 1 # optional: defaults to `latest`. + token: $GCP_ID_TOKEN +``` + ## Troubleshooting ### `The size of mapped attribute google.subject exceeds the 127 bytes limit` error diff --git a/lib/sidebars/explore/menus/projects_menu.rb b/lib/sidebars/explore/menus/projects_menu.rb index c34bd7ed3da..40bca0cd984 100644 --- a/lib/sidebars/explore/menus/projects_menu.rb +++ b/lib/sidebars/explore/menus/projects_menu.rb @@ -6,7 +6,7 @@ module Sidebars class ProjectsMenu < ::Sidebars::Menu override :link def link - explore_projects_path + starred_explore_projects_path end override :title @@ -26,7 +26,15 @@ module Sidebars override :active_routes def active_routes - { page: [link, explore_root_path, starred_explore_projects_path, trending_explore_projects_path] } + { + page: [ + link, + explore_root_path, + starred_explore_projects_path, + trending_explore_projects_path, + explore_projects_path + ] + } end end end diff --git a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb index 6aece8096ff..6c140322fde 100644 --- a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb @@ -29,12 +29,7 @@ module QA end it 'download archives of each user project then check they are different', :blocking, - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347748', - quarantine: { - type: :test_environment, - issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/457099", - only: :production - } do + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347748' do archive_checksums = {} users.each do |user_key, user_info| diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 3c02171bf43..a1b9cc46fb3 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -23,7 +23,7 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects it 'links to the "Explore projects" page' do visit dashboard_projects_path - expect(page).to have_link("Explore projects", href: explore_projects_path) + expect(page).to have_link("Explore projects", href: starred_explore_projects_path) end context 'when user has access to the project' do diff --git a/spec/features/dashboard/root_explore_spec.rb b/spec/features/dashboard/root_explore_spec.rb index 9e844f81a29..cfa98c52c5f 100644 --- a/spec/features/dashboard/root_explore_spec.rb +++ b/spec/features/dashboard/root_explore_spec.rb @@ -27,13 +27,12 @@ RSpec.describe 'Root explore', :saas, feature_category: :shared do include_examples 'shows public projects' end - describe 'project language dropdown' do - let(:has_language_dropdown?) { page.has_selector?('[data-testid="project-language-dropdown"]') } - + describe 'project language dropdown', :js do it 'is conditionally rendered' do visit explore_projects_path + find_by_testid('filtered-search-term-input').click - expect(has_language_dropdown?).to eq(true) + expect(page).to have_link('Language') end end end diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb index 617638ad102..704918aed8e 100644 --- a/spec/features/explore/user_explores_projects_spec.rb +++ b/spec/features/explore/user_explores_projects_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe 'User explores projects', feature_category: :user_profile do - shared_examples 'an "Explore > Projects" page with sidebar and breadcrumbs' do |page_path| + shared_examples 'an "Explore > Projects" page with sidebar and breadcrumbs' do |page_path, params| before do - visit send(page_path) + visit send(page_path, params) end describe "sidebar", :js do @@ -33,7 +33,11 @@ RSpec.describe 'User explores projects', feature_category: :user_profile do end describe '"All" tab' do - it_behaves_like 'an "Explore > Projects" page with sidebar and breadcrumbs', :explore_projects_path + it_behaves_like( + 'an "Explore > Projects" page with sidebar and breadcrumbs', + :explore_projects_path, + { archived: 'true' } + ) end describe '"Most starred" tab' do @@ -80,7 +84,7 @@ RSpec.describe 'User explores projects', feature_category: :user_profile do shared_examples 'empty search results' do it 'shows correct empty state message', :js do - fill_in 'name', with: 'zzzzzzzzzzzzzzzzzzz' + search('zzzzzzzzzzzzzzzzzzz') expect(page).to have_content('Explore public groups to find projects to contribute to') end @@ -88,7 +92,7 @@ RSpec.describe 'User explores projects', feature_category: :user_profile do shared_examples 'minimum search length' do it 'shows a prompt to enter a longer search term', :js do - fill_in 'name', with: 'z' + search('z') expect(page).to have_content('Enter at least three characters to search') end @@ -161,4 +165,11 @@ RSpec.describe 'User explores projects', feature_category: :user_profile do it_behaves_like 'explore page empty state' end end + + def search(term) + filter_input = find_by_testid('filtered-search-term-input') + filter_input.click + filter_input.set(term) + click_button 'Search' + end end diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb index 3576225a417..3f8d6e5a283 100644 --- a/spec/features/projects/user_sorts_projects_spec.rb +++ b/spec/features/projects/user_sorts_projects_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'User sorts projects and order persists', feature_category: :grou find('button[data-testid=base-dropdown-toggle]') end - shared_examples_for "sort order persists across all views" do |project_paths_label, group_paths_label| + shared_examples_for "sort order persists across all views" do |project_paths_label, vue_sort_label| it "is set on the dashboard_projects_path" do visit(dashboard_projects_path) @@ -24,14 +24,16 @@ RSpec.describe 'User sorts projects and order persists', feature_category: :grou it "is set on the explore_projects_path" do visit(explore_projects_path) - expect(find('#sort-projects-dropdown')).to have_content(project_paths_label) + within '[data-testid=groups-projects-sort]' do + expect(find_dropdown_toggle).to have_content(vue_sort_label) + end end it "is set on the group_canonical_path" do visit(group_canonical_path(group)) within '[data-testid=group_sort_by_dropdown]' do - expect(find_dropdown_toggle).to have_content(group_paths_label) + expect(find_dropdown_toggle).to have_content(vue_sort_label) end end @@ -39,7 +41,7 @@ RSpec.describe 'User sorts projects and order persists', feature_category: :grou visit(details_group_path(group)) within '[data-testid=group_sort_by_dropdown]' do - expect(find_dropdown_toggle).to have_content(group_paths_label) + expect(find_dropdown_toggle).to have_content(vue_sort_label) end end end @@ -48,11 +50,14 @@ RSpec.describe 'User sorts projects and order persists', feature_category: :grou before do sign_in(user) visit(explore_projects_path) - find('#sort-projects-dropdown').click - first(:link, 'Updated date').click + within '[data-testid=groups-projects-sort]' do + find_dropdown_toggle.click + find('li', text: 'Name').click + wait_for_requests + end end - it_behaves_like "sort order persists across all views", 'Updated date', 'Updated' + it_behaves_like "sort order persists across all views", 'Name', 'Name' end context 'from dashboard projects', :js do diff --git a/spec/frontend/groups_projects/components/filtered_search_and_sort_spec.js b/spec/frontend/groups_projects/components/filtered_search_and_sort_spec.js index 730f3fb15c6..67eb9d39283 100644 --- a/spec/frontend/groups_projects/components/filtered_search_and_sort_spec.js +++ b/spec/frontend/groups_projects/components/filtered_search_and_sort_spec.js @@ -107,4 +107,14 @@ describe('FilteredSearchAndSort', () => { expect(wrapper.emitted('sort-by-change')).toEqual([[defaultPropsData.sortOptions[1].value]]); }); }); + + describe('when `sortOptions` prop is not passed', () => { + beforeEach(() => { + createComponent({ propsData: { sortOptions: undefined } }); + }); + + it('does not show sort dropdown', () => { + expect(findGlSorting().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js b/spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js new file mode 100644 index 00000000000..2dc767710a9 --- /dev/null +++ b/spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js @@ -0,0 +1,145 @@ +import { GlFilteredSearchToken } from '@gitlab/ui'; +import { + SORT_OPTION_NAME, + SORT_OPTION_CREATED, + SORT_OPTION_UPDATED, + FILTERED_SEARCH_TERM_KEY, + FILTERED_SEARCH_NAMESPACE, + SORT_OPTIONS, + SORT_DIRECTION_ASC, + SORT_DIRECTION_DESC, +} from '~/projects/explore/constants'; +import { RECENT_SEARCHES_STORAGE_KEY_PROJECTS } from '~/filtered_search/recent_searches_storage_keys'; +import FilteredSearchAndSort from '~/groups_projects/components/filtered_search_and_sort.vue'; +import ProjectsExploreFilteredSearchAndSort from '~/projects/explore/components/filtered_search_and_sort.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { visitUrl } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn(), +})); + +describe('ProjectsExploreFilteredSearchAndSort', () => { + let wrapper; + + const defaultProvide = { + initialSort: `${SORT_OPTION_NAME.value}_${SORT_DIRECTION_ASC}`, + programmingLanguages: [ + { id: 5, name: 'CSS', color: '#563d7c', created_at: '2023-09-19T14:41:37.601Z' }, + { id: 8, name: 'CoffeeScript', color: '#244776', created_at: '2023-09-19T14:42:01.494Z' }, + { id: 1, name: 'HTML', color: '#e34c26', created_at: '2023-09-19T14:41:37.597Z' }, + { id: 7, name: 'JavaScript', color: '#f1e05a', created_at: '2023-09-19T14:42:01.494Z' }, + { id: 10, name: 'Makefile', color: '#427819', created_at: '2023-09-19T14:42:11.922Z' }, + { id: 6, name: 'Ruby', color: '#701516', created_at: '2023-09-19T14:42:01.493Z' }, + { id: 11, name: 'Shell', color: '#89e051', created_at: '2023-09-19T14:42:11.923Z' }, + ], + starredExploreProjectsPath: '/explore/projects/starred', + exploreRootPath: '/explore', + }; + + const createComponent = ({ + pathname = '/explore/projects', + queryString = `?archived=only&${FILTERED_SEARCH_TERM_KEY}=foo&sort=${SORT_OPTION_CREATED.value}_${SORT_DIRECTION_ASC}&page=2`, + } = {}) => { + setWindowLocation(pathname + queryString); + + wrapper = shallowMountExtended(ProjectsExploreFilteredSearchAndSort, { + provide: defaultProvide, + }); + }; + + const findFilteredSearchAndSort = () => wrapper.findComponent(FilteredSearchAndSort); + + it('renders filtered search bar with correct props', () => { + createComponent(); + + expect(findFilteredSearchAndSort().props()).toMatchObject({ + filteredSearchTokens: [ + { + type: 'language', + icon: 'lock', + title: 'Language', + token: GlFilteredSearchToken, + unique: true, + operators: [{ value: '=', description: 'is' }], + options: [ + { value: '5', title: 'CSS' }, + { value: '8', title: 'CoffeeScript' }, + { value: '1', title: 'HTML' }, + { value: '7', title: 'JavaScript' }, + { value: '10', title: 'Makefile' }, + { value: '6', title: 'Ruby' }, + { value: '11', title: 'Shell' }, + ], + }, + ], + filteredSearchQuery: { [FILTERED_SEARCH_TERM_KEY]: 'foo' }, + filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY, + filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE, + filteredSearchRecentSearchesStorageKey: RECENT_SEARCHES_STORAGE_KEY_PROJECTS, + sortOptions: SORT_OPTIONS, + activeSortOption: SORT_OPTION_CREATED, + isAscending: true, + }); + }); + + describe('when filtered search bar is submitted', () => { + const searchTerm = 'foo bar'; + + beforeEach(() => { + createComponent(); + + findFilteredSearchAndSort().vm.$emit('filter', { + [FILTERED_SEARCH_TERM_KEY]: searchTerm, + language: '5', + }); + }); + + it('visits URL with correct query string', () => { + expect(visitUrl).toHaveBeenCalledWith( + `?${FILTERED_SEARCH_TERM_KEY}=foo%20bar&language=5&sort=${SORT_OPTION_CREATED.value}_${SORT_DIRECTION_ASC}&archived=only`, + ); + }); + }); + + describe('when sort item is changed', () => { + beforeEach(() => { + createComponent(); + + findFilteredSearchAndSort().vm.$emit('sort-by-change', SORT_OPTION_UPDATED.value); + }); + + it('visits URL with correct query string', () => { + expect(visitUrl).toHaveBeenCalledWith( + `?archived=only&${FILTERED_SEARCH_TERM_KEY}=foo&sort=${SORT_OPTION_UPDATED.value}_${SORT_DIRECTION_ASC}`, + ); + }); + }); + + describe('when sort direction is changed', () => { + beforeEach(() => { + createComponent(); + + findFilteredSearchAndSort().vm.$emit('sort-direction-change', false); + }); + + it('visits URL with correct query string', () => { + expect(visitUrl).toHaveBeenCalledWith( + `?archived=only&${FILTERED_SEARCH_TERM_KEY}=foo&sort=${SORT_OPTION_CREATED.value}_${SORT_DIRECTION_DESC}`, + ); + }); + }); + + describe('when on the "Most starred" tab', () => { + it.each([defaultProvide.starredExploreProjectsPath, defaultProvide.exploreRootPath])( + 'does not show sort dropdown', + (pathname) => { + createComponent({ pathname }); + + expect(findFilteredSearchAndSort().props('sortOptions')).toEqual([]); + }, + ); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js index ce8897027a4..0a5e6f441f0 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js @@ -275,6 +275,35 @@ describe('filterToQueryObject', () => { 'not[foo]': null, }); }); + + describe('when `shouldExcludeEmpty` is set to `true`', () => { + it('excludes empty filters', () => { + expect( + filterToQueryObject( + { + language: [ + { + value: '5', + operator: '=', + }, + ], + FILTERED_SEARCH_TERM: [ + { + value: '', + }, + ], + fooBar: [ + { + value: '', + operator: '=', + }, + ], + }, + { shouldExcludeEmpty: true }, + ), + ).toEqual({ language: ['5'] }); + }); + }); }); describe('urlQueryToFilter', () => { diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index c92a0dcfb2d..0c0c7079491 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1847,4 +1847,17 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end end end + + describe '#projects_explore_filtered_search_and_sort_app_data' do + it 'returns expected json' do + expect(Gitlab::Json.parse(helper.projects_explore_filtered_search_and_sort_app_data)).to eq( + { + 'initial_sort' => 'created_desc', + 'programming_languages' => ProgrammingLanguage.most_popular, + 'starred_explore_projects_path' => starred_explore_projects_path, + 'explore_root_path' => explore_root_path + } + ) + end + end end diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index 27fe1bb3677..8788a552941 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -209,7 +209,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do }, { title: _('Projects'), - href: explore_projects_path, + href: starred_explore_projects_path, css_class: 'dashboard-shortcuts-projects' } ]