Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-13 03:16:30 +00:00
parent 5d89d483f8
commit 7ec4725142
27 changed files with 563 additions and 46 deletions

View File

@ -1,6 +1,6 @@
include:
- project: gitlab-org/quality/pipeline-common
ref: 8.10.0
ref: 8.11.0
file:
- /ci/danger-review.yml

View File

@ -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]

View File

@ -1 +1 @@
1f796d76e13237a957abccde4a07fb5b2d177241
677e0ab263cf525c6e9557d33908cf55f0a13bd2

View File

@ -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 {
<div v-if="$scopedSlots.default">
<slot></slot>
</div>
<div>
<div v-if="shouldShowSort" data-testid="groups-projects-sort">
<gl-sorting
class="gl-display-flex"
dropdown-class="gl-w-full"

View File

@ -1,3 +1,3 @@
import ProjectsList from '~/projects_list';
import { initProjectsExploreFilteredSearchAndSort } from '~/projects/explore';
new ProjectsList(); // eslint-disable-line no-new
initProjectsExploreFilteredSearchAndSort();

View File

@ -0,0 +1,138 @@
<script>
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);
},
},
};
</script>
<template>
<filtered-search-and-sort
:filtered-search-namespace="$options.filteredSearch.namespace"
:filtered-search-tokens="filteredSearchTokens"
:filtered-search-term-key="$options.filteredSearch.searchTermKey"
:filtered-search-recent-searches-storage-key="$options.filteredSearch.recentSearchesStorageKey"
:sort-options="sortOptions"
:filtered-search-query="queryAsObject"
:is-ascending="isAscending"
:active-sort-option="activeSortOption"
@filter="onFilter"
@sort-direction-change="onSortDirectionChange"
@sort-by-change="onSortByChange"
/>
</template>

View File

@ -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,
];

View File

@ -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);
},
});
};

View File

@ -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 {

View File

@ -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)

View File

@ -399,7 +399,7 @@ module SidebarsHelper
},
{
title: _('Projects'),
href: explore_projects_path,
href: starred_explore_projects_path,
css_class: 'dashboard-shortcuts-projects'
}
]

View File

@ -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")

View File

@ -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 } }

View File

@ -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

View File

@ -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.

View File

@ -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/<project-number>/secrets/<secret-name>`.
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

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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([]);
},
);
});
});

View File

@ -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', () => {

View File

@ -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

View File

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