Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5d89d483f8
commit
7ec4725142
|
|
@ -1,6 +1,6 @@
|
|||
include:
|
||||
- project: gitlab-org/quality/pipeline-common
|
||||
ref: 8.10.0
|
||||
ref: 8.11.0
|
||||
file:
|
||||
- /ci/danger-review.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]
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1f796d76e13237a957abccde4a07fb5b2d177241
|
||||
677e0ab263cf525c6e9557d33908cf55f0a13bd2
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
import ProjectsList from '~/projects_list';
|
||||
import { initProjectsExploreFilteredSearchAndSort } from '~/projects/explore';
|
||||
|
||||
new ProjectsList(); // eslint-disable-line no-new
|
||||
initProjectsExploreFilteredSearchAndSort();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
];
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ module SidebarsHelper
|
|||
},
|
||||
{
|
||||
title: _('Projects'),
|
||||
href: explore_projects_path,
|
||||
href: starred_explore_projects_path,
|
||||
css_class: 'dashboard-shortcuts-projects'
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue