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'
}
]