Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-03-09 00:09:16 +00:00
parent 9dbde66947
commit 3a8d99989a
41 changed files with 375 additions and 98 deletions

View File

@ -814,12 +814,12 @@ const Api = {
return axios.delete(url, { data });
},
getRawFile(id, path, params = {}) {
getRawFile(id, path, params = {}, options = {}) {
const url = Api.buildUrl(this.rawFilePath)
.replace(':id', encodeURIComponent(id))
.replace(':path', encodeURIComponent(path));
return axios.get(url, { params });
return axios.get(url, { params, ...options });
},
updateIssue(project, issue, data = {}) {

View File

@ -74,7 +74,12 @@ export default class EditBlob {
let blobContent = '';
if (filePath) {
const { data } = await Api.getRawFile(projectId, filePath, { ref });
const { data } = await Api.getRawFile(
projectId,
filePath,
{ ref },
{ responseType: 'text', transformResponse: (x) => x },
);
blobContent = String(data);
}

View File

@ -419,6 +419,7 @@ export default {
fetchLabels: this.fetchLabels,
fetchLatestLabels: this.glFeatures.frontendCaching ? this.fetchLatestLabels : null,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
multiSelect: this.glFeatures.groupMultiSelectTokens,
},
{
type: TOKEN_TYPE_TYPE,

View File

@ -102,7 +102,7 @@ export default {
}[this.imageDetails?.expirationPolicyCleanupStatus];
},
deleteButtonDisabled() {
return this.disabled || !this.imageDetails.canDelete;
return this.disabled || !this.imageDetails.userPermissions.destroyContainerRepository;
},
rootImageTooltip() {
return !this.imageDetails.name ? ROOT_IMAGE_TOOLTIP : '';

View File

@ -103,7 +103,7 @@ export default {
return this.containerRepository?.tags?.nodes || [];
},
hideBulkDelete() {
return !this.containerRepository?.canDelete;
return !this.containerRepository?.userPermissions.destroyContainerRepository;
},
tagsPageInfo() {
return this.containerRepository?.tags?.pageInfo;

View File

@ -139,7 +139,7 @@ export default {
<list-item v-bind="$attrs" :selected="selected" :disabled="disabled">
<template #left-action>
<gl-form-checkbox
v-if="tag.canDelete"
v-if="tag.userPermissions.destroyContainerRepositoryTag"
:disabled="disabled"
class="gl-m-0"
:checked="selected"
@ -197,7 +197,7 @@ export default {
</gl-sprintf>
</span>
</template>
<template v-if="tag.canDelete" #right-action>
<template v-if="tag.userPermissions.destroyContainerRepositoryTag" #right-action>
<gl-disclosure-dropdown
:disabled="disabled"
icon="ellipsis_v"

View File

@ -66,7 +66,9 @@ export default {
},
computed: {
disabledDelete() {
return !this.item.canDelete || this.deleting || this.migrating;
return (
!this.item.userPermissions.destroyContainerRepository || this.deleting || this.migrating
);
},
id() {
return getIdFromGraphQLId(this.item.id);

View File

@ -5,7 +5,6 @@ query getContainerRepositoryDetails($id: ContainerRepositoryID!) {
path
status
location
canDelete
createdAt
expirationPolicyStartedAt
expirationPolicyCleanupStatus
@ -18,5 +17,8 @@ query getContainerRepositoryDetails($id: ContainerRepositoryID!) {
nextRunAt
}
}
userPermissions {
destroyContainerRepository
}
}
}

View File

@ -12,7 +12,9 @@ query getContainerRepositoryTags(
containerRepository(id: $id) {
id
tagsCount
canDelete
userPermissions {
destroyContainerRepository
}
tags(after: $after, before: $before, first: $first, last: $last, name: $name, sort: $sort) {
nodes {
digest
@ -24,7 +26,9 @@ query getContainerRepositoryTags(
createdAt
publishedAt
totalSize
canDelete
userPermissions {
destroyContainerRepositoryTag
}
}
pageInfo {
...PageInfo

View File

@ -1,8 +1,7 @@
<script>
import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { GlIcon, GlIntersperse, GlFilteredSearchSuggestion, GlLabel } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { stripQuotes } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
@ -13,9 +12,12 @@ import BaseToken from './base_token.vue';
export default {
components: {
BaseToken,
GlToken,
GlIcon,
GlFilteredSearchSuggestion,
GlIntersperse,
GlLabel,
},
inject: ['hasScopedLabelsFeature'],
props: {
config: {
type: Object,
@ -33,6 +35,7 @@ export default {
data() {
return {
labels: this.config.initialLabels || [],
allLabels: this.config.initialLabels || [],
loading: false,
};
},
@ -45,6 +48,21 @@ export default {
getActiveLabel(labels, data) {
return labels.find((label) => this.getLabelName(label) === stripQuotes(data));
},
findLabelByName(name) {
return this.allLabels.find((label) => this.getLabelName(label) === name);
},
findLabelById(id) {
return this.allLabels.find((label) => label.id === id);
},
showScopedLabel(labelName) {
const label = this.findLabelByName(labelName);
return isScopedLabel(label) && this.hasScopedLabelsFeature;
},
getLabelBackgroundColor(labelName) {
const label = this.findLabelByName(labelName);
const backgroundColor = label?.color || '#fff0';
return backgroundColor;
},
/**
* There's an inconsistency between private and public API
* for labels where label name is included in a different
@ -60,15 +78,12 @@ export default {
getLabelName(label) {
return label.name || label.title;
},
getContainerStyle(activeLabel) {
if (activeLabel) {
const { color: backgroundColor, textColor: color } = convertObjectPropsToCamelCase(
activeLabel,
);
return { backgroundColor, color };
}
return {};
updateListOfAllLabels() {
this.labels.forEach((label) => {
if (!this.findLabelById(label.id)) {
this.allLabels.push(label);
}
});
},
fetchLabels(searchTerm) {
this.loading = true;
@ -79,6 +94,8 @@ export default {
// labels.json and /groups/:id/labels & /projects/:id/labels
// return response differently.
this.labels = Array.isArray(res) ? res : res.data;
this.updateListOfAllLabels();
if (this.config.fetchLatestLabels) {
this.fetchLatestLabels(searchTerm);
}
@ -100,6 +117,7 @@ export default {
// labels.json and /groups/:id/labels & /projects/:id/labels
// return response differently.
this.labels = Array.isArray(res) ? res : res.data;
this.updateListOfAllLabels();
})
.catch(() =>
createAlert({
@ -124,24 +142,44 @@ export default {
@fetch-suggestions="fetchLabels"
v-on="$listeners"
>
<template
#view-token="{ viewTokenProps: { inputValue, cssClasses, listeners, activeTokenValue } }"
>
<gl-token
variant="search-value"
:class="cssClasses"
:style="getContainerStyle(activeTokenValue)"
v-on="listeners"
>~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue, selectedTokens } }">
<gl-intersperse v-if="selectedTokens.length > 0" separator=", ">
<gl-label
v-for="label in selectedTokens"
:key="label"
class="js-no-trigger"
:background-color="getLabelBackgroundColor(label)"
:scoped="showScopedLabel(label)"
:title="label"
size="sm"
/>
</gl-intersperse>
<template v-else>
<gl-label
class="js-no-trigger"
:background-color="
getLabelBackgroundColor(activeTokenValue ? getLabelName(activeTokenValue) : inputValue)
"
:scoped="showScopedLabel(activeTokenValue ? getLabelName(activeTokenValue) : inputValue)"
:title="activeTokenValue ? getLabelName(activeTokenValue) : inputValue"
size="sm"
/></template>
</template>
<template #suggestions-list="{ suggestions }">
<template #suggestions-list="{ suggestions, selections = [] }">
<gl-filtered-search-suggestion
v-for="label in suggestions"
:key="label.id"
:value="getLabelName(label)"
>
<div class="gl-display-flex gl-align-items-center">
<div
class="gl-display-flex gl-align-items-center"
:class="{ 'gl-pl-6': !selections.includes(label.title) }"
>
<gl-icon
v-if="selections.includes(label.title)"
name="check"
class="gl-mr-3 gl-text-secondary gl-flex-shrink-0"
/>
<span
:style="{ backgroundColor: label.color }"
class="gl-display-inline-block gl-mr-3 gl-p-3"

View File

@ -8,6 +8,10 @@ class Groups::LabelsController < Groups::ApplicationController
before_action :authorize_label_for_admin_label!, only: [:edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
before_action only: :index do
push_frontend_feature_flag(:label_similarity_sort, group)
end
respond_to :html
feature_category :team_planning

View File

@ -12,6 +12,10 @@ class Projects::LabelsController < Projects::ApplicationController
:set_priorities]
before_action :authorize_admin_group_labels!, only: [:promote]
before_action only: :index do
push_frontend_feature_flag(:label_similarity_sort, project)
end
respond_to :js, :html
feature_category :team_planning

View File

@ -66,13 +66,23 @@ class LabelsFinder < UnionFinder
end
# rubocop: enable CodeReuse/ActiveRecord
def similarity_enabled
if project?
Feature.enabled?(:label_similarity_sort, project)
else
Feature.enabled?(:label_similarity_sort, group)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def sort(items)
if params[:sort]
items.order_by(params[:sort])
else
items.reorder(title: :asc)
return items.reorder(title: :asc) unless params[:sort]
if params[:sort] == 'relevance' && params[:search].present? && similarity_enabled
return items.sorted_by_similarity_desc(params[:search])
end
items.order_by(params[:sort])
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -28,7 +28,6 @@ query getProjectContainerRepositories(
path
status
location
canDelete
createdAt
expirationPolicyStartedAt
expirationPolicyCleanupStatus
@ -36,6 +35,9 @@ query getProjectContainerRepositories(
id
path
}
userPermissions {
destroyContainerRepository
}
__typename
}
pageInfo {
@ -75,6 +77,9 @@ query getProjectContainerRepositories(
id
path
}
userPermissions {
destroyContainerRepository
}
__typename
}
pageInfo {

View File

@ -119,15 +119,17 @@ module SortingHelper
}
end
def label_sort_options_hash
{
def label_sort_options_hash(relevance)
options = {}
options[sort_value_relevance] = sort_title_relevance if params[:search].present? && relevance
options.merge({
sort_value_name => sort_title_name,
sort_value_name_desc => sort_title_name_desc,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated
}
})
end
def users_sort_options_hash

View File

@ -190,6 +190,10 @@ module SortingTitlesValuesHelper
s_('SortOptions|Expired date')
end
def sort_title_relevance
s_('SortOptions|Relevance')
end
# Values.
def sort_value_created_date
'created_date'
@ -374,6 +378,10 @@ module SortingTitlesValuesHelper
def sort_value_expire_date
'expired_asc'
end
def sort_value_relevance
'relevance'
end
end
SortingHelper.include_mod_with('SortingTitlesValuesHelper')

View File

@ -75,6 +75,33 @@ class Label < ApplicationRecord
.with_preloaded_container
end
scope :sorted_by_similarity_desc, -> (search) do
order_expression = Gitlab::Database::SimilarityScore.build_expression(
search: search,
rules: [
{ column: arel_table["title"], multiplier: 1 },
{ column: arel_table["description"], multiplier: 0.2 }
])
order = Gitlab::Pagination::Keyset::Order.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'similarity',
column_expression: order_expression,
order_expression: order_expression.desc,
order_direction: :desc,
distinct: false,
add_to_projections: true
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: Label.arel_table[:id].desc
)
])
order.apply_cursor_conditions(reorder(order))
end
def self.pluck_titles
pluck(:title)
end

View File

@ -8,6 +8,8 @@
.nav-controls
= form_tag labels_filter_path, method: :get do
= hidden_field_tag :subscribed, params[:subscribed]
- if Feature.enabled?(:label_similarity_sort, @project) || Feature.enabled?(:label_similarity_sort, @group)
= hidden_field_tag :sort, 'relevance'
.input-group
= search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false, autofocus: true }
%span.input-group-append

View File

@ -1,3 +1,3 @@
- label_sort_options = label_sort_options_hash.map { |value, text| { value: value, text: text, href: page_filter_path(sort: value) } }
- show_relevance = Feature.enabled?(:label_similarity_sort, @project) || Feature.enabled?(:label_similarity_sort, @group)
- label_sort_options = label_sort_options_hash(show_relevance).map { |value, text| { value: value, text: text, href: page_filter_path(sort: value) } }
= gl_redirect_listbox_tag label_sort_options, @sort, data: { placement: 'right' }

View File

@ -0,0 +1,9 @@
---
name: label_similarity_sort
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443244
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145821
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/448455
milestone: '16.10'
group: group::project management
type: gitlab_com_derisk
default_enabled: false

View File

@ -6,8 +6,12 @@
stage: scalability
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/439687
body: |
The [`sidekiq['min_concurrency']` and `sidekiq['max_concurrency']`](https://docs.gitlab.com/ee/administration/sidekiq/extra_sidekiq_processes.html#manage-thread-counts-explicitly) settings are deprecated in GitLab 16.9 and will be removed in GitLab 17.0.
- For Linux package (Omnibus) installations, the [`sidekiq['min_concurrency']` and `sidekiq['max_concurrency']`](https://docs.gitlab.com/ee/administration/sidekiq/extra_sidekiq_processes.html#manage-thread-counts-explicitly) settings are deprecated in GitLab 16.9 and will be removed in GitLab 17.0.
You can use `sidekiq['concurrency']` in GitLab 16.9 and later to set thread counts explicitly in each process.
You can use `sidekiq['concurrency']` in GitLab 16.9 and later to set thread counts explicitly in each process.
This change only applies to Linux package (Omnibus) installations.
The above change only applies to Linux package (Omnibus) installations.
- For GitLab Helm chart installations, passing `SIDEKIQ_CONCURRENCY_MIN` and/or `SIDEKIQ_CONCURRENCY_MAX` as `extraEnv` to the `sidekiq` sub-chart is deprecated in GitLab 16.10 and will be removed in GitLab 17.0.
You can use the `concurrency` option to set thread counts explicitly in each process.

View File

@ -15174,6 +15174,7 @@ Represents the approval policy.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="approvalpolicyallgroupapprovers"></a>`allGroupApprovers` | [`[PolicyApprovalGroup!]`](#policyapprovalgroup) | All potential approvers of the group type, including groups inaccessible to the user. |
| <a id="approvalpolicydeprecatedproperties"></a>`deprecatedProperties` **{warning-solid}** | [`[String!]`](#string) | **Introduced** in GitLab 16.10. **Status**: Experiment. All deprecated properties in the policy. Returns `null` if security_policies_breaking_changes feature flag is disabled. |
| <a id="approvalpolicydescription"></a>`description` | [`String!`](#string) | Description of the policy. |
| <a id="approvalpolicyeditpath"></a>`editPath` | [`String!`](#string) | URL of policy edit page. |
| <a id="approvalpolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. |
@ -28024,6 +28025,7 @@ Represents the scan result policy.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="scanresultpolicyallgroupapprovers"></a>`allGroupApprovers` | [`[PolicyApprovalGroup!]`](#policyapprovalgroup) | All potential approvers of the group type, including groups inaccessible to the user. |
| <a id="scanresultpolicydeprecatedproperties"></a>`deprecatedProperties` **{warning-solid}** | [`[String!]`](#string) | **Introduced** in GitLab 16.10. **Status**: Experiment. All deprecated properties in the policy. Returns `null` if security_policies_breaking_changes feature flag is disabled. |
| <a id="scanresultpolicydescription"></a>`description` | [`String!`](#string) | Description of the policy. |
| <a id="scanresultpolicyeditpath"></a>`editPath` | [`String!`](#string) | URL of policy edit page. |
| <a id="scanresultpolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. |

View File

@ -1406,11 +1406,15 @@ Users are advised to upgrade to 3.8.8 or greater.
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/439687).
</div>
The [`sidekiq['min_concurrency']` and `sidekiq['max_concurrency']`](https://docs.gitlab.com/ee/administration/sidekiq/extra_sidekiq_processes.html#manage-thread-counts-explicitly) settings are deprecated in GitLab 16.9 and will be removed in GitLab 17.0.
- For Linux package (Omnibus) installations, the [`sidekiq['min_concurrency']` and `sidekiq['max_concurrency']`](https://docs.gitlab.com/ee/administration/sidekiq/extra_sidekiq_processes.html#manage-thread-counts-explicitly) settings are deprecated in GitLab 16.9 and will be removed in GitLab 17.0.
You can use `sidekiq['concurrency']` in GitLab 16.9 and later to set thread counts explicitly in each process.
You can use `sidekiq['concurrency']` in GitLab 16.9 and later to set thread counts explicitly in each process.
This change only applies to Linux package (Omnibus) installations.
The above change only applies to Linux package (Omnibus) installations.
- For GitLab Helm chart installations, passing `SIDEKIQ_CONCURRENCY_MIN` and/or `SIDEKIQ_CONCURRENCY_MAX` as `extraEnv` to the `sidekiq` sub-chart is deprecated in GitLab 16.10 and will be removed in GitLab 17.0.
You can use the `concurrency` option to set thread counts explicitly in each process.
</div>

View File

@ -120,7 +120,16 @@ planned for release in 16.9.1.
| 16.5 | All | None |
| 16.6 | All | None |
| 16.7 | All | None |
| 16.8 | 16.8.0 - 18.8.3 | 16.8.4 |
| 16.8 | 16.8.0 - 16.8.3 | 16.8.4 |
| 16.9 | 16.9.0 - 16.9.1 | 16.9.2 |
- You might experience verification failures on a subset of container registry images due to checksum mismatch between the primary site and the secondary site. [Issue 442667](https://gitlab.com/gitlab-org/gitlab/-/issues/442667) describes the details. While there is no direct risk of data loss as the data is being correctly replicated to the secondary sites, it is not being successfully verified. There are no known workarounds at this time.
**Affected releases**:
| Affected minor releases | Affected patch releases | Fixed in |
| ----------------------- | ----------------------- | -------- |
| 16.8 | 16.8.0 - 16.8.3 | 16.8.4 |
| 16.9 | 16.9.0 - 16.9.1 | 16.9.2 |
## 16.8.0
@ -170,7 +179,16 @@ you must take one of the following actions based on your configuration:
| 16.5 | All | None |
| 16.6 | All | None |
| 16.7 | All | None |
| 16.8 | 16.8.0 - 18.8.3 | 16.8.4 |
| 16.8 | 16.8.0 - 16.8.3 | 16.8.4 |
| 16.9 | 16.9.0 - 16.9.1 | 16.9.2 |
- You might experience verification failures on a subset of container registry images due to checksum mismatch between the primary site and the secondary site. [Issue 442667](https://gitlab.com/gitlab-org/gitlab/-/issues/442667) describes the details. While there is no direct risk of data loss as the data is being correctly replicated to the secondary sites, it is not being successfully verified. There are no known workarounds at this time.
**Affected releases**:
| Affected minor releases | Affected patch releases | Fixed in |
| ----------------------- | ----------------------- | -------- |
| 16.8 | 16.8.0 - 16.8.3 | 16.8.4 |
| 16.9 | 16.9.0 - 16.9.1 | 16.9.2 |
## 16.7.0
@ -240,7 +258,7 @@ take one of the following actions based on your configuration:
| 16.5 | All | None |
| 16.6 | All | None |
| 16.7 | All | None |
| 16.8 | 16.8.0 - 18.8.3 | 16.8.4 |
| 16.8 | 16.8.0 - 16.8.3 | 16.8.4 |
| 16.9 | 16.9.0 - 16.9.1 | 16.9.2 |
## 16.6.0
@ -286,7 +304,7 @@ take one of the following actions based on your configuration:
| 16.5 | All | None |
| 16.6 | All | None |
| 16.7 | All | None |
| 16.8 | 16.8.0 - 18.8.3 | 16.8.4 |
| 16.8 | 16.8.0 - 16.8.3 | 16.8.4 |
| 16.9 | 16.9.0 - 16.9.1 | 16.9.2 |
## 16.5.0
@ -426,7 +444,7 @@ Specific information applies to installations using Geo:
| 16.5 | All | None |
| 16.6 | All | None |
| 16.7 | All | None |
| 16.8 | 16.8.0 - 18.8.3 | 16.8.4 |
| 16.8 | 16.8.0 - 16.8.3 | 16.8.4 |
| 16.9 | 16.9.0 - 16.9.1 | 16.9.2 |
## 16.4.0

View File

@ -48439,6 +48439,9 @@ msgstr ""
msgid "SortOptions|Recently starred"
msgstr ""
msgid "SortOptions|Relevance"
msgstr ""
msgid "SortOptions|Size"
msgstr ""

View File

@ -45,4 +45,30 @@ RSpec.describe 'Search for labels', :js, feature_category: :team_planning do
expect(page).not_to have_content(label2.title)
expect(page).not_to have_content(label2.description)
end
context 'when label_similarity_sort is enabled' do
before do
stub_feature_flags(label_similarity_sort: true)
end
it 'sorts by relevance when searching' do
find('#label-search').fill_in(with: 'Bar')
find('#label-search').native.send_keys(:enter)
expect(page).to have_button('Relevance')
end
end
context 'when label_similarity_sort is disabled' do
before do
stub_feature_flags(label_similarity_sort: false)
end
it 'sorts by relevance when searching' do
find('#label-search').fill_in(with: 'Bar')
find('#label-search').native.send_keys(:enter)
expect(page).to have_button('Name')
end
end
end

View File

@ -159,7 +159,8 @@ RSpec.describe 'Filter issues', :js, feature_category: :team_planning do
end
it 'filters issues by multiple labels with not operator' do
select_tokens 'Label', '!=', bug_label.title, 'Label', '=', caps_sensitive_label.title, submit: true
select_tokens 'Label', '!=', bug_label.title, submit: true
select_tokens 'Label', '=', caps_sensitive_label.title, submit: true
expect_negated_label_token(bug_label.title)
expect_label_token(caps_sensitive_label.title)

View File

@ -6,12 +6,16 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
include Features::SourceEditorSpecHelpers
include ProjectForksHelper
include Features::BlobSpecHelpers
include TreeHelper
let_it_be(:json_text) { '{"name":"Best package ever!"}' }
let_it_be(:project_with_json) { create(:project, :custom_repo, name: 'Project with json', files: { 'package.json' => json_text }) }
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'Shop') }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
let(:user) { create(:user) }
before do
stub_feature_flags(vscode_web_ide: false)
@ -248,4 +252,16 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
end
end
end
context 'when editing a json file', :js do
before_all do
project_with_json.add_maintainer(user)
end
it 'loads the content as text' do
visit(project_edit_blob_path(project_with_json, tree_join(project_with_json.default_branch, 'package.json')))
wait_for_requests
expect(find('.monaco-editor')).to have_content(json_text)
end
end
end

View File

@ -1202,6 +1202,18 @@ describe('Api', () => {
expect(axios.get).toHaveBeenCalledWith(expectedUrl, { params });
});
});
describe('when the method is called with options', () => {
it('sets the params and options on the request', () => {
const options = { responseType: 'text', transformRequest: (x) => x };
const params = { ref: 'main' };
jest.spyOn(axios, 'get');
Api.getRawFile(dummyProjectPath, dummyFilePath, params, options);
expect(axios.get).toHaveBeenCalledWith(expectedUrl, { params, ...options });
});
});
});
describe('when an error occurs while getting a raw file', () => {

View File

@ -99,7 +99,12 @@ describe('Blob Editing', () => {
describe('file content', () => {
beforeEach(() => initEditor());
it('requests raw file content', () => {
expect(Api.getRawFile).toHaveBeenCalledWith(projectId, filePath, { ref: 'main' });
expect(Api.getRawFile).toHaveBeenCalledWith(
projectId,
filePath,
{ ref: 'main' },
{ responseType: 'text', transformResponse: expect.any(Function) },
);
});
it('creates an editor instance with the raw content', () => {

View File

@ -117,6 +117,9 @@ describe('IssuesDashboardApp component', () => {
propsData: {
eeTypeTokenOptions,
},
stubs: {
GlIntersperse: true,
},
});
};

View File

@ -264,10 +264,23 @@ const makeFilteredTokens = ({ grouped }) => [
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'comedy', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'sitcom', operator: OPERATOR_OR } },
...(grouped
? [
{
type: TOKEN_TYPE_LABEL,
value: { data: ['live action', 'drama'], operator: OPERATOR_NOT },
},
]
: [
{ type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
]),
...(grouped
? [{ type: TOKEN_TYPE_LABEL, value: { data: ['comedy', 'sitcom'], operator: OPERATOR_OR } }]
: [
{ type: TOKEN_TYPE_LABEL, value: { data: 'comedy', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'sitcom', operator: OPERATOR_OR } },
]),
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },

View File

@ -183,6 +183,7 @@ describe('groupMultiSelectFilterTokens', () => {
groupMultiSelectFilterTokens(filteredTokens, [
{ type: 'assignee', multiSelect: true },
{ type: 'author', multiSelect: true },
{ type: 'label', multiSelect: true },
]),
).toEqual(groupedFilteredTokens);
});

View File

@ -108,15 +108,20 @@ describe('Details Header', () => {
describe('menu', () => {
it.each`
canDelete | disabled | isVisible
${true} | ${false} | ${true}
${true} | ${true} | ${false}
${false} | ${false} | ${false}
${false} | ${true} | ${false}
destroyContainerRepository | disabled | isVisible
${true} | ${false} | ${true}
${true} | ${true} | ${false}
${false} | ${false} | ${false}
${false} | ${true} | ${false}
`(
'when canDelete is $canDelete and disabled is $disabled is $isVisible that the menu is visible',
({ canDelete, disabled, isVisible }) => {
mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
'when userPermissions.destroyContainerRepository is $destroyContainerRepository and disabled is $disabled is $isVisible that the menu is visible',
({ destroyContainerRepository, disabled, isVisible }) => {
mountComponent({
propsData: {
image: { ...defaultImage, userPermissions: { destroyContainerRepository } },
disabled,
},
});
expect(findMenu().exists()).toBe(isVisible);
},

View File

@ -70,7 +70,7 @@ describe('tags list row', () => {
});
it("does not exist when the row can't be deleted", () => {
const customTag = { ...tag, canDelete: false };
const customTag = { ...tag, userPermissions: { destroyContainerRepositoryTag: false } };
mountComponent({ ...defaultProps, tag: customTag });
@ -306,8 +306,11 @@ describe('tags list row', () => {
expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(false);
});
it('is not rendered when tag.canDelete is false', () => {
mountComponent({ ...defaultProps, tag: { ...tag, canDelete: false } });
it('is not rendered when tag.userPermissions.destroyContainerRegistryTag is false', () => {
mountComponent({
...defaultProps,
tag: { ...tag, userPermissions: { destroyContainerRepositoryTag: false } },
});
expect(findAdditionalActionsMenu().exists()).toBe(false);
});

View File

@ -314,7 +314,11 @@ describe('Tags List', () => {
describe('when user does not have permission to delete list rows', () => {
it('sets registry list hiddenDelete prop to true', async () => {
resolver = jest.fn().mockResolvedValue(imageTagsMock({ canDelete: false }));
resolver = jest
.fn()
.mockResolvedValue(
imageTagsMock({ userPermissions: { destroyContainerRepository: false } }),
);
mountComponent();
await waitForApolloRequestRender();

View File

@ -161,7 +161,7 @@ describe('Image List Row', () => {
expect(findDeleteBtn().props()).toMatchObject({
title: REMOVE_REPOSITORY_LABEL,
tooltipDisabled: item.canDelete,
tooltipDisabled: item.userPermissions.destroyContainerRepository,
tooltipTitle: LIST_DELETE_BUTTON_DISABLED,
});
});
@ -174,15 +174,23 @@ describe('Image List Row', () => {
});
it.each`
canDelete | status | state
${false} | ${''} | ${true}
${false} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${true} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${true} | ${''} | ${false}
destroyContainerRepository | status | state
${false} | ${''} | ${true}
${false} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${true} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${true} | ${''} | ${false}
`(
'disabled is $state when canDelete is $canDelete and status is $status',
({ canDelete, status, state }) => {
mountComponent({ item: { ...item, canDelete, status } });
'disabled is $state when userPermissions.destroyContainerRepository is $destroyContainerRepository and status is $status',
({ destroyContainerRepository, status, state }) => {
mountComponent({
item: {
...item,
userPermissions: {
destroyContainerRepository,
},
status,
},
});
expect(findDeleteBtn().props('disabled')).toBe(state);
},

View File

@ -1,3 +1,9 @@
const userPermissionsData = {
userPermissions: {
destroyContainerRepository: true,
},
};
export const imagesListResponse = [
{
__typename: 'ContainerRepository',
@ -7,7 +13,6 @@ export const imagesListResponse = [
status: null,
migrationState: 'default',
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
@ -15,6 +20,7 @@ export const imagesListResponse = [
id: 'gid://gitlab/Project/22',
path: 'GITLAB-TEST',
},
...userPermissionsData,
},
{
__typename: 'ContainerRepository',
@ -24,7 +30,6 @@ export const imagesListResponse = [
status: null,
migrationState: 'default',
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572',
canDelete: true,
createdAt: '2020-09-21T06:57:43Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
@ -32,6 +37,7 @@ export const imagesListResponse = [
id: 'gid://gitlab/Project/22',
path: 'gitlab-test',
},
...userPermissionsData,
},
];
@ -125,7 +131,6 @@ export const containerRepositoryMock = {
path: 'gitlab-org/gitlab-test/rails-12009',
status: null,
location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
@ -139,6 +144,7 @@ export const containerRepositoryMock = {
},
__typename: 'Project',
},
...userPermissionsData,
};
export const tagsPageInfo = {
@ -160,7 +166,9 @@ export const tagsMock = [
createdAt: '2020-11-03T13:29:38+00:00',
publishedAt: '2020-11-05T13:29:38+00:00',
totalSize: '1099511627776',
canDelete: true,
userPermissions: {
destroyContainerRepositoryTag: true,
},
__typename: 'ContainerRepositoryTag',
},
{
@ -173,22 +181,27 @@ export const tagsMock = [
createdAt: '2020-11-03T13:29:32+00:00',
publishedAt: '2020-11-05T13:29:32+00:00',
totalSize: '536870912000',
canDelete: true,
userPermissions: {
destroyContainerRepositoryTag: true,
},
__typename: 'ContainerRepositoryTag',
},
];
export const imageTagsMock = ({ nodes = tagsMock, canDelete = true } = {}) => ({
export const imageTagsMock = ({ nodes = tagsMock, userPermissions = {} } = {}) => ({
data: {
containerRepository: {
id: containerRepositoryMock.id,
tagsCount: nodes.length,
canDelete,
tags: {
nodes,
pageInfo: { ...tagsPageInfo },
__typename: 'ContainerRepositoryTagConnection',
},
userPermissions: {
...userPermissionsData.userPermissions,
...userPermissions,
},
__typename: 'ContainerRepositoryDetails',
},
},

View File

@ -2,6 +2,7 @@ import {
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
GlLabel,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@ -52,6 +53,7 @@ function createComponent(options = {}) {
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
termsAsTokens: () => false,
hasScopedLabelsFeature: false,
},
stubs,
listeners,
@ -192,9 +194,9 @@ describe('LabelToken', () => {
const tokenSegments = findTokenSegments();
expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label"
expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label"
expect(tokenSegments.at(2).find('.gl-token').attributes('style')).toBe(
'background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);',
expect(tokenSegments.at(2).text()).toBe(`${mockRegularLabel.title}`); // "Foo Label"
expect(tokenSegments.at(2).findComponent(GlLabel).props('backgroundColor')).toBe(
mockRegularLabel.color,
);
});

View File

@ -417,6 +417,17 @@ RSpec.describe Label, feature_category: :team_planning do
end
end
describe '.sorted_by_similarity_desc' do
context 'when sorted by similarity' do
it 'returns most relevant labels first' do
label1 = create(:label, title: 'exact')
label2 = create(:label, title: 'less exact')
label3 = create(:label, title: 'other', description: 'mentions exact')
expect(described_class.sorted_by_similarity_desc('exact')).to eq([label1, label2, label3])
end
end
end
describe '.optionally_subscribed_by' do
let!(:user) { create(:user) }
let!(:label) { create(:label) }

View File

@ -141,7 +141,7 @@ module FilteredSearchHelpers
create_token('Release', release_tag)
end
def label_token(label_name = nil, has_symbol = true)
def label_token(label_name = nil, has_symbol = false)
symbol = has_symbol ? '~' : nil
create_token('Label', label_name, symbol)
end
@ -264,11 +264,11 @@ module FilteredSearchHelpers
end
def expect_label_token(value)
expect(page).to have_css '.gl-filtered-search-token', text: "Label = ~#{value}"
expect(page).to have_css '.gl-filtered-search-token', text: "Label = #{value}"
end
def expect_negated_label_token(value)
expect(page).to have_css '.gl-filtered-search-token', text: "Label != ~#{value}"
expect(page).to have_css '.gl-filtered-search-token', text: "Label != #{value}"
end
def expect_milestone_token(value)