Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_row.vue4
-rw-r--r--app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue16
-rw-r--r--app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue66
-rw-r--r--app/assets/javascripts/members/constants.js53
-rw-r--r--app/assets/javascripts/members/utils.js52
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/helpers/members_helper.rb6
-rw-r--r--app/presenters/alert_management/alert_presenter.rb3
-rw-r--r--app/services/issues/base_service.rb16
-rw-r--r--app/services/issues/create_service.rb16
-rw-r--r--app/services/issues/update_service.rb1
-rw-r--r--app/services/members/invite_service.rb16
-rw-r--r--app/services/packages/create_event_service.rb4
-rw-r--r--changelogs/unreleased/205578-trigger-pkg-guest-events.yml5
-rw-r--r--changelogs/unreleased/268282-remove-feature-flag.yml5
-rw-r--r--changelogs/unreleased/285076-400-bad-request-during-authentication-due-to-password-format-lengt.yml5
-rw-r--r--changelogs/unreleased/290006-error-500-on-members-page-after-invitation-sent-via-api.yml5
-rw-r--r--changelogs/unreleased/sy-allow-to-unlabel-incidents.yml5
-rw-r--r--config/feature_flags/development/highlight_current_diff_row.yml8
-rw-r--r--doc/api/README.md6
-rw-r--r--doc/api/audit_events.md4
-rw-r--r--doc/api/commits.md2
-rw-r--r--doc/api/feature_flags.md2
-rw-r--r--doc/api/feature_flags_legacy.md8
-rw-r--r--doc/api/freeze_periods.md2
-rw-r--r--doc/api/graphql/getting_started.md2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql90
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json247
-rw-r--r--doc/api/graphql/reference/index.md11
-rw-r--r--doc/api/import.md2
-rw-r--r--doc/api/invitations.md2
-rw-r--r--doc/api/issues.md4
-rw-r--r--doc/api/jobs.md2
-rw-r--r--doc/api/lint.md4
-rw-r--r--doc/api/releases/index.md2
-rw-r--r--doc/api/scim.md2
-rw-r--r--doc/api/templates/dockerfiles.md4
-rw-r--r--doc/api/templates/gitignores.md4
-rw-r--r--doc/api/templates/licenses.md4
-rw-r--r--doc/api/vulnerability_exports.md4
-rw-r--r--doc/development/testing_guide/frontend_testing.md39
-rw-r--r--doc/user/markdown.md22
-rw-r--r--lib/api/entities/invitation.rb2
-rw-r--r--lib/gitlab/middleware/handle_malformed_strings.rb3
-rw-r--r--lib/gitlab/usage_data_counters/guest_package_events.yml34
-rw-r--r--locale/gitlab.pot24
-rw-r--r--spec/features/groups/members/sort_members_spec.rb192
-rw-r--r--spec/frontend/diffs/components/diff_file_row_spec.js38
-rw-r--r--spec/frontend/members/components/filter_sort/filter_sort_container_spec.js15
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js135
-rw-r--r--spec/frontend/members/utils_spec.js110
-rw-r--r--spec/helpers/members_helper_spec.rb10
-rw-r--r--spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb7
-rw-r--r--spec/mailers/emails/projects_spec.rb6
-rw-r--r--spec/requests/api/invitations_spec.rb8
-rw-r--r--spec/requests/git_http_spec.rb14
-rw-r--r--spec/services/incident_management/incidents/create_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb1
-rw-r--r--spec/services/issues/update_service_spec.rb43
-rw-r--r--spec/services/members/invite_service_spec.rb11
-rw-r--r--spec/services/packages/create_event_service_spec.rb24
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb6
-rw-r--r--spec/tasks/gitlab/packages/events_rake_spec.rb8
63 files changed, 1222 insertions, 227 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_file_row.vue b/app/assets/javascripts/diffs/components/diff_file_row.vue
index 3888eb781fb..6c5d9170c9e 100644
--- a/app/assets/javascripts/diffs/components/diff_file_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_row.vue
@@ -41,10 +41,6 @@ export default {
return !this.hideFileStats && this.file.type === 'blob';
},
fileClasses() {
- if (!this.glFeatures.highlightCurrentDiffRow) {
- return '';
- }
-
return this.file.type === 'blob' && !this.viewedFiles[this.file.fileHash]
? 'gl-font-weight-bold'
: '';
diff --git a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
index f2acc3215cd..f869ecd392f 100644
--- a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
+++ b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
@@ -1,18 +1,26 @@
<script>
import { mapState } from 'vuex';
import MembersFilteredSearchBar from './members_filtered_search_bar.vue';
+import SortDropdown from './sort_dropdown.vue';
export default {
name: 'FilterSortContainer',
- components: { MembersFilteredSearchBar },
+ components: { MembersFilteredSearchBar, SortDropdown },
computed: {
- ...mapState(['filteredSearchBar']),
+ ...mapState(['filteredSearchBar', 'tableSortableFields']),
+ showContainer() {
+ return this.filteredSearchBar.show || this.showSortDropdown;
+ },
+ showSortDropdown() {
+ return this.tableSortableFields.length;
+ },
},
};
</script>
<template>
- <div v-if="filteredSearchBar.show" class="gl-bg-gray-10 gl-p-5">
- <members-filtered-search-bar />
+ <div v-if="showContainer" class="gl-bg-gray-10 gl-p-3 gl-display-md-flex">
+ <members-filtered-search-bar v-if="filteredSearchBar.show" class="gl-p-3 gl-flex-grow-1" />
+ <sort-dropdown v-if="showSortDropdown" class="gl-p-3 gl-flex-shrink-0" />
</div>
</template>
diff --git a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
new file mode 100644
index 00000000000..e2fbb074fcd
--- /dev/null
+++ b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
@@ -0,0 +1,66 @@
+<script>
+import { mapState } from 'vuex';
+import { GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
+import { parseSortParam, buildSortUrl } from '~/members/utils';
+import { FIELDS } from '~/members/constants';
+
+export default {
+ name: 'SortDropdown',
+ components: { GlDropdown, GlDropdownItem, GlFormGroup },
+ computed: {
+ ...mapState(['tableSortableFields', 'filteredSearchBar']),
+ sort() {
+ return parseSortParam(this.tableSortableFields);
+ },
+ filteredOptions() {
+ const buildOption = (field, sortDesc) => ({
+ ...(sortDesc ? field.sort.desc : field.sort.asc),
+ key: field.key,
+ sortDesc,
+ url: buildSortUrl({
+ sortBy: field.key,
+ sortDesc,
+ filteredSearchBarTokens: this.filteredSearchBar.tokens,
+ filteredSearchBarSearchParam: this.filteredSearchBar.searchParam,
+ }),
+ });
+
+ return FIELDS.filter(
+ field => this.tableSortableFields.includes(field.key) && field.sort,
+ ).flatMap(field => [buildOption(field, false), buildOption(field, true)]);
+ },
+ },
+ methods: {
+ isChecked(key, sortDesc) {
+ return this.sort?.sortBy === key && this.sort?.sortDesc === sortDesc;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ :label="__('Sort by')"
+ class="gl-mb-0"
+ label-cols="auto"
+ label-class="gl-align-self-center gl-pb-0!"
+ >
+ <gl-dropdown
+ :text="sort.sortByLabel"
+ block
+ toggle-class="gl-mb-0"
+ data-testid="members-sort-dropdown"
+ right
+ >
+ <gl-dropdown-item
+ v-for="option in filteredOptions"
+ :key="option.param"
+ :href="option.url"
+ is-check-item
+ :is-checked="isChecked(option.key, option.sortDesc)"
+ >
+ {{ option.label }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index a23e9b942ef..874e934e5b0 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -1,9 +1,21 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+
+const ACCOUNT_SORT_ASC_LABEL = s__('Members|Account, ascending');
export const FIELDS = [
{
key: 'account',
label: __('Account'),
+ sort: {
+ asc: {
+ param: 'name_asc',
+ label: ACCOUNT_SORT_ASC_LABEL,
+ },
+ desc: {
+ param: 'name_desc',
+ label: s__('Members|Account, descending'),
+ },
+ },
},
{
key: 'source',
@@ -16,6 +28,16 @@ export const FIELDS = [
label: __('Access granted'),
thClass: 'col-meta',
tdClass: 'col-meta',
+ sort: {
+ asc: {
+ param: 'last_joined',
+ label: s__('Members|Access granted, ascending'),
+ },
+ desc: {
+ param: 'oldest_joined',
+ label: s__('Members|Access granted, descending'),
+ },
+ },
},
{
key: 'invited',
@@ -40,6 +62,16 @@ export const FIELDS = [
label: __('Max role'),
thClass: 'col-max-role',
tdClass: 'col-max-role',
+ sort: {
+ asc: {
+ param: 'access_level_asc',
+ label: s__('Members|Max role, ascending'),
+ },
+ desc: {
+ param: 'access_level_desc',
+ label: s__('Members|Max role, descending'),
+ },
+ },
},
{
key: 'expiration',
@@ -48,6 +80,19 @@ export const FIELDS = [
tdClass: 'col-expiration',
},
{
+ key: 'lastSignIn',
+ sort: {
+ asc: {
+ param: 'recent_sign_in',
+ label: s__('Members|Last sign-in, ascending'),
+ },
+ desc: {
+ param: 'oldest_sign_in',
+ label: s__('Members|Last sign-in, descending'),
+ },
+ },
+ },
+ {
key: 'actions',
thClass: 'col-actions',
tdClass: 'col-actions',
@@ -55,6 +100,12 @@ export const FIELDS = [
},
];
+export const DEFAULT_SORT = {
+ sortBy: 'account',
+ sortDesc: false,
+ sortByLabel: ACCOUNT_SORT_ASC_LABEL,
+};
+
export const AVATAR_SIZE = 48;
export const MEMBER_TYPES = {
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 4229a62c0a7..5c58c4a9f6c 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,4 +1,7 @@
import { __ } from '~/locale';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { FIELDS, DEFAULT_SORT } from './constants';
export const generateBadges = (member, isCurrentUser) => [
{
@@ -44,5 +47,54 @@ export const canUpdate = (member, currentUserId, sourceId) => {
);
};
+export const parseSortParam = sortableFields => {
+ const sortParam = getParameterByName('sort');
+
+ const sortedField = FIELDS.filter(field => sortableFields.includes(field.key)).find(
+ field => field.sort?.asc?.param === sortParam || field.sort?.desc?.param === sortParam,
+ );
+
+ if (!sortedField) {
+ return DEFAULT_SORT;
+ }
+
+ const isDesc = sortedField?.sort?.desc?.param === sortParam;
+
+ return {
+ sortBy: sortedField.key,
+ sortDesc: isDesc,
+ sortByLabel: isDesc ? sortedField?.sort?.desc?.label : sortedField?.sort?.asc?.label,
+ };
+};
+
+export const buildSortUrl = ({
+ sortBy,
+ sortDesc,
+ filteredSearchBarTokens,
+ filteredSearchBarSearchParam,
+}) => {
+ const sortDefinition = FIELDS.find(field => field.key === sortBy)?.sort;
+
+ if (!sortDefinition) {
+ return '';
+ }
+
+ const sortParam = sortDesc ? sortDefinition.desc.param : sortDefinition.asc.param;
+
+ const filterParams =
+ filteredSearchBarTokens?.reduce((accumulator, token) => {
+ return {
+ ...accumulator,
+ [token]: getParameterByName(token),
+ };
+ }, {}) || {};
+
+ if (filteredSearchBarSearchParam) {
+ filterParams[filteredSearchBarSearchParam] = getParameterByName(filteredSearchBarSearchParam);
+ }
+
+ return setUrlParams({ ...filterParams, sort: sortParam }, window.location.href, true);
+};
+
// Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js`
export const canOverride = () => false;
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 69aec044863..838b1925f34 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -36,7 +36,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:unified_diff_components, @project)
- push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index d66f67fbb60..5dc636ad996 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -6,14 +6,14 @@ module MembersHelper
text = 'Are you sure you want to'
action =
- if member.request?
+ if member.invite?
+ "revoke the invitation for #{member.invite_email} to join"
+ elsif member.request?
if member.user == user
'withdraw your access request for'
else
"deny #{member.user.name}'s request to join"
end
- elsif member.invite?
- "revoke the invitation for #{member.invite_email} to join"
else
if member.user
"remove #{member.user.name} from"
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index 4bfa3dc9a13..1cebf5c561a 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -8,7 +8,6 @@ module AlertManagement
MARKDOWN_LINE_BREAK = " \n"
HORIZONTAL_LINE = "\n\n---\n\n"
- INCIDENT_LABEL_NAME = ::IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title]
delegate :metrics_dashboard_url, :runbook, to: :parsed_payload
@@ -48,7 +47,7 @@ module AlertManagement
end
def incident_issues_link
- project_issues_url(project, label_name: INCIDENT_LABEL_NAME)
+ project_incidents_url(project)
end
def performance_dashboard_link
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 978ea6fe9bc..25f319da03b 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -73,22 +73,6 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache
end
-
- # Applies label "incident" (creates it if missing) to incident issues.
- # Please use in "after" hooks only to ensure we are not appyling
- # labels prematurely.
- def add_incident_label(issue)
- return unless issue.incident?
-
- label = ::IncidentManagement::CreateIncidentLabelService
- .new(project, current_user)
- .execute
- .payload[:label]
-
- return if issue.label_ids.include?(label.id)
-
- issue.labels << label
- end
end
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index fb7683f940d..44de8eb6389 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -49,6 +49,22 @@ module Issues
def user_agent_detail_service
UserAgentDetailService.new(@issue, @request)
end
+
+ # Applies label "incident" (creates it if missing) to incident issues.
+ # For use in "after" hooks only to ensure we are not appyling
+ # labels prematurely.
+ def add_incident_label(issue)
+ return unless issue.incident?
+
+ label = ::IncidentManagement::CreateIncidentLabelService
+ .new(project, current_user)
+ .execute
+ .payload[:label]
+
+ return if issue.label_ids.include?(label.id)
+
+ issue.labels << label
+ end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 4f40ff5f535..127ed04cf51 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -34,7 +34,6 @@ module Issues
end
def after_update(issue)
- add_incident_label(issue)
IssuesChannel.broadcast_to(issue, event: 'updated') if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
end
diff --git a/app/services/members/invite_service.rb b/app/services/members/invite_service.rb
index cfab5c3ef9d..60ebbaface2 100644
--- a/app/services/members/invite_service.rb
+++ b/app/services/members/invite_service.rb
@@ -20,8 +20,8 @@ module Members
emails.each do |email|
next if existing_member?(source, email)
-
next if existing_invite?(source, email)
+ next if existing_request?(source, email)
if existing_user?(email)
add_existing_user_as_member(current_user, source, params, email)
@@ -44,8 +44,7 @@ module Members
access_level: params[:access_level],
invite_email: email,
created_by_id: current_user.id,
- expires_at: params[:expires_at],
- requested_at: Time.current.utc)
+ expires_at: params[:expires_at])
unless new_member.valid? && new_member.persisted?
errors[params[:email]] = new_member.errors.full_messages.to_sentence
@@ -92,6 +91,17 @@ module Members
false
end
+ def existing_request?(source, email)
+ existing_request = source.requesters.with_user_by_email(email).exists?
+
+ if existing_request
+ errors[email] = "Member cannot be invited because they already requested to join #{source.name}"
+ return true
+ end
+
+ false
+ end
+
def existing_user(email)
User.find_by_email(email)
end
diff --git a/app/services/packages/create_event_service.rb b/app/services/packages/create_event_service.rb
index 74d57f2ad98..f0328ceb08a 100644
--- a/app/services/packages/create_event_service.rb
+++ b/app/services/packages/create_event_service.rb
@@ -4,7 +4,9 @@ module Packages
class CreateEventService < BaseService
def execute
if Feature.enabled?(:collect_package_events_redis) && redis_event_name
- unless guest?
+ if guest?
+ ::Gitlab::UsageDataCounters::GuestPackageEventCounter.count(redis_event_name)
+ else
::Gitlab::UsageDataCounters::HLLRedisCounter.track_event(current_user.id, redis_event_name)
end
end
diff --git a/changelogs/unreleased/205578-trigger-pkg-guest-events.yml b/changelogs/unreleased/205578-trigger-pkg-guest-events.yml
new file mode 100644
index 00000000000..2e92b37c2f7
--- /dev/null
+++ b/changelogs/unreleased/205578-trigger-pkg-guest-events.yml
@@ -0,0 +1,5 @@
+---
+title: Tracks guest package events
+merge_request: 48547
+author:
+type: added
diff --git a/changelogs/unreleased/268282-remove-feature-flag.yml b/changelogs/unreleased/268282-remove-feature-flag.yml
new file mode 100644
index 00000000000..fc08aad3e2c
--- /dev/null
+++ b/changelogs/unreleased/268282-remove-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Enable file tree highlighting by default
+merge_request: 49356
+author:
+type: changed
diff --git a/changelogs/unreleased/285076-400-bad-request-during-authentication-due-to-password-format-lengt.yml b/changelogs/unreleased/285076-400-bad-request-during-authentication-due-to-password-format-lengt.yml
new file mode 100644
index 00000000000..a2dfc320f49
--- /dev/null
+++ b/changelogs/unreleased/285076-400-bad-request-during-authentication-due-to-password-format-lengt.yml
@@ -0,0 +1,5 @@
+---
+title: Add different string encoding method in rack middleware
+merge_request: 49044
+author:
+type: fixed
diff --git a/changelogs/unreleased/290006-error-500-on-members-page-after-invitation-sent-via-api.yml b/changelogs/unreleased/290006-error-500-on-members-page-after-invitation-sent-via-api.yml
new file mode 100644
index 00000000000..8d393e47a61
--- /dev/null
+++ b/changelogs/unreleased/290006-error-500-on-members-page-after-invitation-sent-via-api.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Members page 500 error after Invitation sent via API
+merge_request: 48937
+author:
+type: fixed
diff --git a/changelogs/unreleased/sy-allow-to-unlabel-incidents.yml b/changelogs/unreleased/sy-allow-to-unlabel-incidents.yml
new file mode 100644
index 00000000000..68eeffccf63
--- /dev/null
+++ b/changelogs/unreleased/sy-allow-to-unlabel-incidents.yml
@@ -0,0 +1,5 @@
+---
+title: Do not automatically reapply incident label after user removes it
+merge_request: 49188
+author:
+type: fixed
diff --git a/config/feature_flags/development/highlight_current_diff_row.yml b/config/feature_flags/development/highlight_current_diff_row.yml
deleted file mode 100644
index fc872ea47fc..00000000000
--- a/config/feature_flags/development/highlight_current_diff_row.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: highlight_current_diff_row
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27937
-rollout_issue_url:
-milestone: '13.4'
-type: development
-group: group::source code
-default_enabled: false
diff --git a/doc/api/README.md b/doc/api/README.md
index 63bccbddb50..dced721b018 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -107,7 +107,7 @@ curl: (22) The requested URL returned error: 404
```
The HTTP exit code can help you diagnose the success or failure of your REST call.
-
+
## Authentication
Most API requests require authentication, or only return public data when
@@ -591,7 +591,7 @@ We can call the API with `array` and `hash` types parameters as follows:
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
-d "import_sources[]=github" \
-d "import_sources[]=bitbucket" \
-https://gitlab.example.com/api/v4/some_endpoint
+"https://gitlab.example.com/api/v4/some_endpoint"
```
### `hash`
@@ -605,7 +605,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--form "file=@/path/to/somefile.txt"
--form "override_params[visibility]=private" \
--form "override_params[some_other_param]=some_value" \
-https://gitlab.example.com/api/v4/projects/import
+"https://gitlab.example.com/api/v4/projects/import"
```
### Array of hashes
diff --git a/doc/api/audit_events.md b/doc/api/audit_events.md
index 315059fa87a..83b244ccb8d 100644
--- a/doc/api/audit_events.md
+++ b/doc/api/audit_events.md
@@ -258,7 +258,7 @@ are paginated.
Read more on [pagination](README.md#pagination).
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/api/v4/projects/7/audit_events"
```
Example response:
@@ -318,7 +318,7 @@ GET /projects/:id/audit_events/:audit_event_id
| `audit_event_id` | integer | yes | The ID of the audit event |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events/5
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/api/v4/projects/7/audit_events/5"
```
Example response:
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 8987704c8b2..81014956fc5 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -635,7 +635,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| `all` | boolean | no | Return all statuses, not only the latest ones
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses"
```
Example response:
diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md
index 45028408491..e8d6911463d 100644
--- a/doc/api/feature_flags.md
+++ b/doc/api/feature_flags.md
@@ -103,7 +103,7 @@ GET /projects/:id/feature_flags/:feature_flag_name
| `feature_flag_name` | string | yes | The name of the feature flag. |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature"
```
Example response:
diff --git a/doc/api/feature_flags_legacy.md b/doc/api/feature_flags_legacy.md
index eae4dbc36e1..8a5af39a37f 100644
--- a/doc/api/feature_flags_legacy.md
+++ b/doc/api/feature_flags_legacy.md
@@ -36,7 +36,7 @@ GET /projects/:id/feature_flags
| `scope` | string | no | The condition of feature flags, one of: `enabled`, `disabled`. |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags"
```
Example response:
@@ -174,7 +174,7 @@ POST /projects/:id/feature_flags
| `scopes:strategies` | JSON | no | The [strategies](../operations/feature_flags.md#feature-flag-strategies) of the feature flag spec. |
```shell
-curl https://gitlab.example.com/api/v4/projects/1/feature_flags \
+curl "https://gitlab.example.com/api/v4/projects/1/feature_flags" \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-type: application/json" \
--data @- << EOF
@@ -244,7 +244,7 @@ GET /projects/:id/feature_flags/:name
| `name` | string | yes | The name of the feature flag. |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace"
```
Example response:
@@ -320,5 +320,5 @@ DELETE /projects/:id/feature_flags/:name
| `name` | string | yes | The name of the feature flag. |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature
+curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature"
```
diff --git a/doc/api/freeze_periods.md b/doc/api/freeze_periods.md
index d26c9248084..ae640e254d1 100644
--- a/doc/api/freeze_periods.md
+++ b/doc/api/freeze_periods.md
@@ -101,7 +101,7 @@ Example request:
```shell
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: <your_access_token>" \
--data '{ "freeze_start": "0 23 * * 5", "freeze_end": "0 7 * * 1", "cron_timezone": "UTC" }' \
- --request POST https://gitlab.example.com/api/v4/projects/19/freeze_periods
+ --request POST "https://gitlab.example.com/api/v4/projects/19/freeze_periods"
```
Example response:
diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md
index 49886ec8595..e5f2dde325b 100644
--- a/doc/api/graphql/getting_started.md
+++ b/doc/api/graphql/getting_started.md
@@ -29,7 +29,7 @@ Example:
```shell
GRAPHQL_TOKEN=<your-token>
-curl 'https://gitlab.com/api/graphql' --header "Authorization: Bearer $GRAPHQL_TOKEN" --header "Content-Type: application/json" --request POST --data "{\"query\": \"query {currentUser {name}}\"}"
+curl "https://gitlab.com/api/graphql" --header "Authorization: Bearer $GRAPHQL_TOKEN" --header "Content-Type: application/json" --request POST --data "{\"query\": \"query {currentUser {name}}\"}"
```
### GraphiQL
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index bbb2f483a3c..e9924bd52d7 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2195,6 +2195,11 @@ type BoardListUpdateLimitMetricsPayload {
list: BoardList
}
+"""
+Identifier of Boards::EpicBoard
+"""
+scalar BoardsEpicBoardID
+
type Branch {
"""
Commit for the branch
@@ -7944,6 +7949,56 @@ type EpicAddIssuePayload {
}
"""
+Represents an epic board
+"""
+type EpicBoard {
+ """
+ Global ID of the board
+ """
+ id: BoardsEpicBoardID!
+
+ """
+ Name of the board
+ """
+ name: String
+}
+
+"""
+The connection type for EpicBoard.
+"""
+type EpicBoardConnection {
+ """
+ A list of edges.
+ """
+ edges: [EpicBoardEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [EpicBoard]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type EpicBoardEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: EpicBoard
+}
+
+"""
The connection type for Epic.
"""
type EpicConnection {
@@ -9238,6 +9293,41 @@ type Group {
): Epic
"""
+ Find a single epic board
+ """
+ epicBoard(
+ """
+ Find an epic board by ID
+ """
+ id: BoardsEpicBoardID!
+ ): EpicBoard
+
+ """
+ Find epic boards
+ """
+ epicBoards(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): EpicBoardConnection
+
+ """
Find epics
"""
epics(
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 97b19afa6d5..3782960aece 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -5844,6 +5844,16 @@
},
{
"kind": "SCALAR",
+ "name": "BoardsEpicBoardID",
+ "description": "Identifier of Boards::EpicBoard",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "SCALAR",
"name": "Boolean",
"description": "Represents `true` or `false` values.",
"fields": null,
@@ -22239,6 +22249,163 @@
},
{
"kind": "OBJECT",
+ "name": "EpicBoard",
+ "description": "Represents an epic board",
+ "fields": [
+ {
+ "name": "id",
+ "description": "Global ID of the board",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "BoardsEpicBoardID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the board",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicBoardConnection",
+ "description": "The connection type for EpicBoard.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "EpicBoardEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "EpicBoard",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicBoardEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicBoard",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "EpicConnection",
"description": "The connection type for Epic.",
"fields": [
@@ -25699,6 +25866,86 @@
"deprecationReason": null
},
{
+ "name": "epicBoard",
+ "description": "Find a single epic board",
+ "args": [
+ {
+ "name": "id",
+ "description": "Find an epic board by ID",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "BoardsEpicBoardID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicBoard",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epicBoards",
+ "description": "Find epic boards",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicBoardConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "epics",
"description": "Find epics",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 316476f290f..4380152d79e 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1352,6 +1352,15 @@ Autogenerated return type of EpicAddIssue.
| `epicIssue` | EpicIssue | The epic-issue relation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+### EpicBoard
+
+Represents an epic board.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `id` | BoardsEpicBoardID! | Global ID of the board |
+| `name` | String | Name of the board |
+
### EpicDescendantCount
Counts of descendent epics.
@@ -1532,6 +1541,8 @@ Autogenerated return type of EpicTreeReorder.
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled |
| `epic` | Epic | Find a single epic |
+| `epicBoard` | EpicBoard | Find a single epic board |
+| `epicBoards` | EpicBoardConnection | Find epic boards |
| `epics` | EpicConnection | Find epics |
| `epicsEnabled` | Boolean | Indicates if Epics are enabled for namespace |
| `fullName` | String! | Full name of the namespace |
diff --git a/doc/api/import.md b/doc/api/import.md
index f1292be5d6e..3a1edcb732d 100644
--- a/doc/api/import.md
+++ b/doc/api/import.md
@@ -71,7 +71,7 @@ POST /import/bitbucket_server
```shell
curl --request POST \
- --url https://gitlab.example.com/api/v4/import/bitbucket_server \
+ --url "https://gitlab.example.com/api/v4/import/bitbucket_server" \
--header "content-type: application/json" \
--header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" \
--data '{
diff --git a/doc/api/invitations.md b/doc/api/invitations.md
index d241c56dc9e..e5663f6efe1 100644
--- a/doc/api/invitations.md
+++ b/doc/api/invitations.md
@@ -97,7 +97,7 @@ Example response:
{
"id": 1,
"invite_email": "member@example.org",
- "invited_at": "2020-10-22T14:13:35Z",
+ "created_at": "2020-10-22T14:13:35Z",
"access_level": 30,
"expires_at": "2020-11-22T14:13:35Z",
"user_name": "Raymond Smith",
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 3157acc5d08..cbb0dad9841 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -1652,13 +1652,13 @@ Supported attributes:
Example request:
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=Lets%20promote%20this%20to%20an%20epic%0A%0A%2Fpromote
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=Lets%20promote%20this%20to%20an%20epic%0A%0A%2Fpromote"
```
Example response:
```json
-{
+{
"id":699,
"type":null,
"body":"Lets promote this to an epic",
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index d1286c98e2f..b7b742ebe66 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -293,7 +293,7 @@ GET /projects/:id/pipelines/:pipeline_id/bridges
| `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/bridges?scope[]=pending&scope[]=running'
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/6/bridges?scope[]=pending&scope[]=running"
```
Example of response
diff --git a/doc/api/lint.md b/doc/api/lint.md
index bd84e1dfcf8..48a7be13a28 100644
--- a/doc/api/lint.md
+++ b/doc/api/lint.md
@@ -246,7 +246,7 @@ GitLab API using `curl` and `jq` in a one-line command:
```shell
jq --null-input --arg yaml "$(<example-gitlab-ci.yml)" '.content=$yaml' \
-| curl 'https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true' \
+| curl "https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true" \
--header 'Content-Type: application/json' \
--data @-
```
@@ -293,7 +293,7 @@ With a one-line command, you can:
```shell
jq --null-input --arg yaml "$(<example-gitlab-ci.yml)" '.content=$yaml' \
-| curl 'https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true' \
+| curl "https://gitlab.com/api/v4/ci/lint?include_merged_yaml=true" \
--header 'Content-Type: application/json' --data @- \
| jq --raw-output '.merged_yaml | fromjson'
```
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index 62e08eab7db..af89cd206a7 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -376,7 +376,7 @@ Example request:
```shell
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \
--data '{ "name": "New release", "tag_name": "v0.3", "description": "Super nice release", "milestones": ["v1.0", "v1.0-rc"], "assets": { "links": [{ "name": "hoge", "url": "https://google.com", "filepath": "/binaries/linux-amd64", "link_type":"other" }] } }' \
- --request POST https://gitlab.example.com/api/v4/projects/24/releases
+ --request POST "https://gitlab.example.com/api/v4/projects/24/releases"
```
Example response:
diff --git a/doc/api/scim.md b/doc/api/scim.md
index fe344ea112f..a9622525478 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -39,7 +39,7 @@ Pagination follows the [SCIM spec](https://tools.ietf.org/html/rfc7644#section-3
Example request:
```shell
-curl 'https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20"0b1d561c-21ff-4092-beab-8154b17f82f2"' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
+curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20%220b1d561c-21ff-4092-beab-8154b17f82f2%22" --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
```
Example response:
diff --git a/doc/api/templates/dockerfiles.md b/doc/api/templates/dockerfiles.md
index 14451cae892..a86dc36e141 100644
--- a/doc/api/templates/dockerfiles.md
+++ b/doc/api/templates/dockerfiles.md
@@ -20,7 +20,7 @@ GET /templates/dockerfiles
```
```shell
-curl https://gitlab.example.com/api/v4/templates/dockerfiles
+curl "https://gitlab.example.com/api/v4/templates/dockerfiles"
```
Example response:
@@ -119,7 +119,7 @@ GET /templates/dockerfiles/:key
| `key` | string | yes | The key of the Dockerfile template |
```shell
-curl https://gitlab.example.com/api/v4/templates/dockerfiles/Binary
+curl "https://gitlab.example.com/api/v4/templates/dockerfiles/Binary"
```
Example response:
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
index 2f489b2d6a7..6f2e5a83903 100644
--- a/doc/api/templates/gitignores.md
+++ b/doc/api/templates/gitignores.md
@@ -21,7 +21,7 @@ GET /templates/gitignores
Example request:
```shell
-curl https://gitlab.example.com/api/v4/templates/gitignores
+curl "https://gitlab.example.com/api/v4/templates/gitignores"
```
Example response:
@@ -126,7 +126,7 @@ GET /templates/gitignores/:key
Example request:
```shell
-curl https://gitlab.example.com/api/v4/templates/gitignores/Ruby
+curl "https://gitlab.example.com/api/v4/templates/gitignores/Ruby"
```
Example response:
diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md
index 29c7f69c7f7..fb76bd81e50 100644
--- a/doc/api/templates/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -25,7 +25,7 @@ GET /templates/licenses
| `popular` | boolean | no | If passed, returns only popular licenses |
```shell
-curl https://gitlab.example.com/api/v4/templates/licenses?popular=1
+curl "https://gitlab.example.com/api/v4/templates/licenses?popular=1"
```
Example response:
@@ -128,7 +128,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of
the authenticated user replaces the copyright holder placeholder.
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/templates/licenses/mit?project=My+Cool+Project
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/templates/licenses/mit?project=My+Cool+Project"
```
Example response:
diff --git a/doc/api/vulnerability_exports.md b/doc/api/vulnerability_exports.md
index 98e349d6235..e4853920cab 100644
--- a/doc/api/vulnerability_exports.md
+++ b/doc/api/vulnerability_exports.md
@@ -83,7 +83,7 @@ POST /security/groups/:id/vulnerability_exports
| `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the group which the authenticated user is a member of |
```shell
-curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/groups/1/vulnerability_exports
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/security/groups/1/vulnerability_exports"
```
The created vulnerability export is automatically deleted after 1 hour.
@@ -116,7 +116,7 @@ POST /security/vulnerability_exports
```
```shell
-curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/security/vulnerability_exports"
```
The created vulnerability export is automatically deleted after one hour.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index b098b8418b4..cc7222426a7 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -961,6 +961,45 @@ describe "Admin::AbuseReports", :js do
end
```
+### Jest test timeout due to async imports
+
+If a module asynchronously imports some other modules at runtime, these modules will need to be
+transpiled by the Jest loaders at runtime. It's possible that this will cause [Jest to timeout](https://gitlab.com/gitlab-org/gitlab/-/issues/280809).
+
+If you run into this issue, consider eager importing the module so that Jest will compile
+and cache it at compile-time, fixing the runtime timeout.
+
+Consider the following example:
+
+```javascript
+// the_subject.js
+
+export default {
+ components: {
+ // Async import Thing because it is large and isn't always needed.
+ Thing: () => import(/* webpackChunkName: 'thing' */ './path/to/thing.vue'),
+ }
+};
+```
+
+Jest will not automatically transpile the `thing.vue` module, and depending on it's size, could
+cause Jest to timeout. We can force Jest to transpile and cache this module by eagerly importing
+it like so:
+
+```javascript
+// the_subject_spec.js
+
+import Subject from '~/feature/the_subject.vue';
+
+// Force Jest to transpile and cache
+// eslint-disable-next-line import/order, no-unused-vars
+import _Thing from '~/feature/path/to/thing.vue';
+```
+
+**PLEASE NOTE:** Do not simply disregard test timeouts. This could be a sign that there's
+actually a production problem. Use this opportunity to analyze the production webpack bundles and
+chunks and confirm that there is not a production issue with the async imports.
+
## Overview of Frontend Testing Levels
Main information on frontend testing levels can be found in the [Testing Levels page](testing_levels.md).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index f2633749cd3..15bb77efa17 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -431,7 +431,7 @@ GFM recognizes the following:
| merge request | `!123` | `namespace/project!123` | `project!123` |
| snippet | `$123` | `namespace/project$123` | `project$123` |
| epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | |
-| vulnerability **(ULTIMATE)** *(1)* | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
+| vulnerability **(ULTIMATE)** | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
| label by ID | `~123` | `namespace/project~123` | `project~123` |
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
@@ -445,26 +445,6 @@ GFM recognizes the following:
| repository file line references | `[README](doc/README#L13)` | | |
| [alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` |
-1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/281035) in GitLab 13.6.
-
- The Vulnerability special references feature is under development but ready for production use.
- It is deployed behind a feature flag that is **disabled by default**.
- [GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
- can opt to enable it for your instance.
- It's disabled on GitLab.com.
-
- To disable it:
-
- ```ruby
- Feature.disable(:vulnerability_special_references)
- ```
-
- To enable it:
-
- ```ruby
- Feature.enable(:vulnerability_special_references)
- ```
-
For example, referencing an issue by using `#123` will format the output as a link
to issue number 123 with text `#123`. Likewise, a link to issue number 123 will be
recognized and formatted with text `#123`.
diff --git a/lib/api/entities/invitation.rb b/lib/api/entities/invitation.rb
index 342f4804cf3..517b89529d7 100644
--- a/lib/api/entities/invitation.rb
+++ b/lib/api/entities/invitation.rb
@@ -4,7 +4,7 @@ module API
module Entities
class Invitation < Grape::Entity
expose :access_level
- expose :requested_at
+ expose :created_at
expose :expires_at
expose :invite_email
expose :invite_token
diff --git a/lib/gitlab/middleware/handle_malformed_strings.rb b/lib/gitlab/middleware/handle_malformed_strings.rb
index 84f7e2e1b14..b966395ee32 100644
--- a/lib/gitlab/middleware/handle_malformed_strings.rb
+++ b/lib/gitlab/middleware/handle_malformed_strings.rb
@@ -93,7 +93,8 @@ module Gitlab
# We try to encode the string from ASCII-8BIT to UTF8. If we failed to do
# so for certain characters in the string, those chars are probably incomplete
# multibyte characters.
- string.encode(Encoding::UTF_8).match?(NULL_BYTE_REGEX)
+ string.dup.force_encoding(Encoding::UTF_8).match?(NULL_BYTE_REGEX)
+
rescue ArgumentError, Encoding::UndefinedConversionError
# If we're here, we caught a malformed string. Return true
true
diff --git a/lib/gitlab/usage_data_counters/guest_package_events.yml b/lib/gitlab/usage_data_counters/guest_package_events.yml
new file mode 100644
index 00000000000..a9b9f8ea235
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/guest_package_events.yml
@@ -0,0 +1,34 @@
+---
+- i_package_composer_guest_delete
+- i_package_composer_guest_pull
+- i_package_composer_guest_push
+- i_package_conan_guest_delete
+- i_package_conan_guest_pull
+- i_package_conan_guest_push
+- i_package_container_guest_delete
+- i_package_container_guest_pull
+- i_package_container_guest_push
+- i_package_debian_guest_delete
+- i_package_debian_guest_pull
+- i_package_debian_guest_push
+- i_package_generic_guest_delete
+- i_package_generic_guest_pull
+- i_package_generic_guest_push
+- i_package_golang_guest_delete
+- i_package_golang_guest_pull
+- i_package_golang_guest_push
+- i_package_maven_guest_delete
+- i_package_maven_guest_pull
+- i_package_maven_guest_push
+- i_package_npm_guest_delete
+- i_package_npm_guest_pull
+- i_package_npm_guest_push
+- i_package_nuget_guest_delete
+- i_package_nuget_guest_pull
+- i_package_nuget_guest_push
+- i_package_pypi_guest_delete
+- i_package_pypi_guest_pull
+- i_package_pypi_guest_push
+- i_package_tag_guest_delete
+- i_package_tag_guest_pull
+- i_package_tag_guest_push
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 17dbe063104..ec87de01384 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16900,6 +16900,18 @@ msgstr ""
msgid "Members|2FA"
msgstr ""
+msgid "Members|Access granted, ascending"
+msgstr ""
+
+msgid "Members|Access granted, descending"
+msgstr ""
+
+msgid "Members|Account, ascending"
+msgstr ""
+
+msgid "Members|Account, descending"
+msgstr ""
+
msgid "Members|An error occurred while trying to enable LDAP override, please try again."
msgstr ""
@@ -16963,9 +16975,21 @@ msgstr ""
msgid "Members|LDAP override enabled."
msgstr ""
+msgid "Members|Last sign-in, ascending"
+msgstr ""
+
+msgid "Members|Last sign-in, descending"
+msgstr ""
+
msgid "Members|Leave \"%{source}\""
msgstr ""
+msgid "Members|Max role, ascending"
+msgstr ""
+
+msgid "Members|Max role, descending"
+msgstr ""
+
msgid "Members|Membership"
msgstr ""
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index 74c736f6e2c..a7a6104a1f7 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -9,87 +9,167 @@ RSpec.describe 'Groups > Members > Sort members', :js do
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
let(:group) { create(:group) }
- dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]'
-
before do
- stub_feature_flags(group_members_filtered_search: false)
-
create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
sign_in(owner)
end
- it 'sorts alphabetically by default' do
- visit_members_list(sort: nil)
+ context 'when `group_members_filtered_search` feature flag is enabled' do
+ dropdown_toggle_selector = '[data-testid="members-sort-dropdown"] > button'
- expect(first_row.text).to include(owner.name)
- expect(second_row.text).to include(developer.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending')
- end
+ it 'sorts account by default' do
+ visit_members_list(sort: nil)
- it 'sorts by access level ascending' do
- visit_members_list(sort: :access_level_asc)
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Account, ascending')
+ end
- expect(first_row.text).to include(developer.name)
- expect(second_row.text).to include(owner.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, ascending')
- end
+ it 'sorts by max role ascending' do
+ visit_members_list(sort: :access_level_asc)
- it 'sorts by access level descending' do
- visit_members_list(sort: :access_level_desc)
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Max role, ascending')
+ end
- expect(first_row.text).to include(owner.name)
- expect(second_row.text).to include(developer.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, descending')
- end
+ it 'sorts by max role descending' do
+ visit_members_list(sort: :access_level_desc)
- it 'sorts by last joined' do
- visit_members_list(sort: :last_joined)
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Max role, descending')
+ end
- expect(first_row.text).to include(developer.name)
- expect(second_row.text).to include(owner.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Last joined')
- end
+ it 'sorts by access granted ascending' do
+ visit_members_list(sort: :last_joined)
- it 'sorts by oldest joined' do
- visit_members_list(sort: :oldest_joined)
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Access granted, ascending')
+ end
- expect(first_row.text).to include(owner.name)
- expect(second_row.text).to include(developer.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest joined')
- end
+ it 'sorts by access granted descending' do
+ visit_members_list(sort: :oldest_joined)
- it 'sorts by name ascending' do
- visit_members_list(sort: :name_asc)
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Access granted, descending')
+ end
- expect(first_row.text).to include(owner.name)
- expect(second_row.text).to include(developer.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending')
- end
+ it 'sorts by account ascending' do
+ visit_members_list(sort: :name_asc)
- it 'sorts by name descending' do
- visit_members_list(sort: :name_desc)
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Account, ascending')
+ end
- expect(first_row.text).to include(developer.name)
- expect(second_row.text).to include(owner.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Name, descending')
- end
+ it 'sorts by account descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Account, descending')
+ end
+
+ it 'sorts by last sign-in ascending', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Last sign-in, ascending')
+ end
- it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
- visit_members_list(sort: :recent_sign_in)
+ it 'sorts by last sign-in descending', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :oldest_sign_in)
- expect(first_row.text).to include(owner.name)
- expect(second_row.text).to include(developer.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Recent sign in')
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Last sign-in, descending')
+ end
end
- it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
- visit_members_list(sort: :oldest_sign_in)
+ context 'when `group_members_filtered_search` feature flag is disabled' do
+ dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]'
+
+ before do
+ stub_feature_flags(group_members_filtered_search: false)
+ end
+
+ it 'sorts alphabetically by default' do
+ visit_members_list(sort: nil)
+
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending')
+ end
+
+ it 'sorts by access level ascending' do
+ visit_members_list(sort: :access_level_asc)
+
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, ascending')
+ end
+
+ it 'sorts by access level descending' do
+ visit_members_list(sort: :access_level_desc)
+
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Access level, descending')
+ end
+
+ it 'sorts by last joined' do
+ visit_members_list(sort: :last_joined)
+
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Last joined')
+ end
+
+ it 'sorts by oldest joined' do
+ visit_members_list(sort: :oldest_joined)
+
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest joined')
+ end
+
+ it 'sorts by name ascending' do
+ visit_members_list(sort: :name_asc)
+
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Name, ascending')
+ end
+
+ it 'sorts by name descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Name, descending')
+ end
+
+ it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_row.text).to include(owner.name)
+ expect(second_row.text).to include(developer.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Recent sign in')
+ end
+
+ it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :oldest_sign_in)
- expect(first_row.text).to include(developer.name)
- expect(second_row.text).to include(owner.name)
- expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest sign in')
+ expect(first_row.text).to include(developer.name)
+ expect(second_row.text).to include(owner.name)
+ expect(page).to have_css(dropdown_toggle_selector, text: 'Oldest sign in')
+ end
end
def visit_members_list(sort:)
diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js
index 23adc8f9da4..7403a7918a9 100644
--- a/spec/frontend/diffs/components/diff_file_row_spec.js
+++ b/spec/frontend/diffs/components/diff_file_row_spec.js
@@ -7,12 +7,9 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
describe('Diff File Row component', () => {
let wrapper;
- const createComponent = (props = {}, highlightCurrentDiffRow = false) => {
+ const createComponent = (props = {}) => {
wrapper = shallowMount(DiffFileRow, {
propsData: { ...props },
- provide: {
- glFeatures: { highlightCurrentDiffRow },
- },
});
};
@@ -60,26 +57,23 @@ describe('Diff File Row component', () => {
});
it.each`
- features | fileType | isViewed | expected
- ${{ highlightCurrentDiffRow: true }} | ${'blob'} | ${false} | ${'gl-font-weight-bold'}
- ${{}} | ${'blob'} | ${true} | ${''}
- ${{}} | ${'tree'} | ${false} | ${''}
- ${{}} | ${'tree'} | ${true} | ${''}
+ fileType | isViewed | expected
+ ${'blob'} | ${false} | ${'gl-font-weight-bold'}
+ ${'blob'} | ${true} | ${''}
+ ${'tree'} | ${false} | ${''}
+ ${'tree'} | ${true} | ${''}
`(
- 'with (features="$features", fileType="$fileType", isViewed=$isViewed), sets fileClasses="$expected"',
- ({ features, fileType, isViewed, expected }) => {
- createComponent(
- {
- file: {
- type: fileType,
- fileHash: '#123456789',
- },
- level: 0,
- hideFileStats: false,
- viewedFiles: isViewed ? { '#123456789': true } : {},
+ 'with (fileType="$fileType", isViewed=$isViewed), sets fileClasses="$expected"',
+ ({ fileType, isViewed, expected }) => {
+ createComponent({
+ file: {
+ type: fileType,
+ fileHash: '#123456789',
},
- features.highlightCurrentDiffRow,
- );
+ level: 0,
+ hideFileStats: false,
+ viewedFiles: isViewed ? { '#123456789': true } : {},
+ });
expect(wrapper.find(FileRow).props('fileClasses')).toBe(expected);
},
);
diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
index 4abf9f50959..91277ae6d03 100644
--- a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
+++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
+import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -19,6 +20,7 @@ describe('FilterSortContainer', () => {
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
+ tableSortableFields: ['account'],
...state,
},
});
@@ -29,12 +31,13 @@ describe('FilterSortContainer', () => {
});
};
- describe('when `filteredSearchBar.show` is `false`', () => {
+ describe('when `filteredSearchBar.show` is `false` and `tableSortableFields` is empty', () => {
it('renders nothing', () => {
createComponent({
filteredSearchBar: {
show: false,
},
+ tableSortableFields: [],
});
expect(wrapper.html()).toBe('');
@@ -52,4 +55,14 @@ describe('FilterSortContainer', () => {
expect(wrapper.find(MembersFilteredSearchBar).exists()).toBe(true);
});
});
+
+ describe('when `tableSortableFields` is set', () => {
+ it('renders `SortDropdown`', () => {
+ createComponent({
+ tableSortableFields: ['account'],
+ });
+
+ expect(wrapper.find(SortDropdown).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
new file mode 100644
index 00000000000..22c1eea1653
--- /dev/null
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -0,0 +1,135 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import { within } from '@testing-library/dom';
+import Vuex from 'vuex';
+import { GlDropdownItem } from '@gitlab/ui';
+import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('SortDropdown', () => {
+ let wrapper;
+
+ const URL_HOST = 'https://localhost/';
+
+ const createComponent = state => {
+ const store = new Vuex.Store({
+ state: {
+ sourceId: 1,
+ tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'],
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor'],
+ searchParam: 'search',
+ placeholder: 'Filter members',
+ recentSearchesStorageKey: 'group_members',
+ },
+ ...state,
+ },
+ });
+
+ wrapper = mount(SortDropdown, {
+ localVue,
+ store,
+ });
+ };
+
+ const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
+ const findDropdownItemByText = text =>
+ wrapper
+ .findAll(GlDropdownItem)
+ .wrappers.find(dropdownItemWrapper => dropdownItemWrapper.text() === text);
+
+ describe('dropdown options', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(URL_HOST);
+ });
+
+ it('adds dropdown items for all the sortable fields', () => {
+ const URL_FILTER_PARAMS = '?two_factor=enabled&search=foobar';
+ const EXPECTED_BASE_URL = `${URL_HOST}${URL_FILTER_PARAMS}&sort=`;
+
+ window.location.search = URL_FILTER_PARAMS;
+
+ const expectedDropdownItems = [
+ {
+ label: 'Account, ascending',
+ url: `${EXPECTED_BASE_URL}name_asc`,
+ },
+ {
+ label: 'Account, descending',
+ url: `${EXPECTED_BASE_URL}name_desc`,
+ },
+ {
+ label: 'Access granted, ascending',
+ url: `${EXPECTED_BASE_URL}last_joined`,
+ },
+ {
+ label: 'Access granted, descending',
+ url: `${EXPECTED_BASE_URL}oldest_joined`,
+ },
+ {
+ label: 'Max role, ascending',
+ url: `${EXPECTED_BASE_URL}access_level_asc`,
+ },
+ {
+ label: 'Max role, descending',
+ url: `${EXPECTED_BASE_URL}access_level_desc`,
+ },
+ {
+ label: 'Last sign-in, ascending',
+ url: `${EXPECTED_BASE_URL}recent_sign_in`,
+ },
+ {
+ label: 'Last sign-in, descending',
+ url: `${EXPECTED_BASE_URL}oldest_sign_in`,
+ },
+ ];
+
+ createComponent();
+
+ expectedDropdownItems.forEach(expectedDropdownItem => {
+ const dropdownItem = findDropdownItemByText(expectedDropdownItem.label);
+
+ expect(dropdownItem).not.toBe(null);
+ expect(dropdownItem.find('a').attributes('href')).toBe(expectedDropdownItem.url);
+ });
+ });
+
+ it('checks selected sort option', () => {
+ window.location.search = '?sort=access_level_asc';
+
+ createComponent();
+
+ expect(findDropdownItemByText('Max role, ascending').props('isChecked')).toBe(true);
+ });
+ });
+
+ describe('dropdown toggle', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(URL_HOST);
+ });
+
+ it('defaults to sorting by "Account, ascending"', () => {
+ createComponent();
+
+ expect(findDropdownToggle().text()).toBe('Account, ascending');
+ });
+
+ it('sets text as selected sort option', () => {
+ window.location.search = '?sort=access_level_asc';
+
+ createComponent();
+
+ expect(findDropdownToggle().text()).toBe('Max role, ascending');
+ });
+ });
+
+ it('renders dropdown label', () => {
+ createComponent();
+
+ expect(within(wrapper.element).queryByText('Sort by')).not.toBe(null);
+ });
+});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index a598b51d8dd..2dcd084b241 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -7,13 +7,17 @@ import {
canResend,
canUpdate,
canOverride,
+ parseSortParam,
+ buildSortUrl,
} from '~/members/utils';
+import { DEFAULT_SORT } from '~/members/constants';
import { member as memberMock, group, invite } from './mock_data';
const DIRECT_MEMBER_ID = 178;
const INHERITED_MEMBER_ID = 179;
const IS_CURRENT_USER_ID = 123;
const IS_NOT_CURRENT_USER_ID = 124;
+const URL_HOST = 'https://localhost/';
describe('Members Utils', () => {
describe('generateBadges', () => {
@@ -119,4 +123,110 @@ describe('Members Utils', () => {
expect(canOverride(memberMock)).toBe(false);
});
});
+
+ describe('parseSortParam', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(URL_HOST);
+ });
+
+ describe('when `sort` param is not present', () => {
+ it('returns default sort options', () => {
+ window.location.search = '';
+
+ expect(parseSortParam(['account'])).toEqual(DEFAULT_SORT);
+ });
+ });
+
+ describe('when field passed in `sortableFields` argument does not have `sort` key defined', () => {
+ it('returns default sort options', () => {
+ window.location.search = '?sort=source_asc';
+
+ expect(parseSortParam(['source'])).toEqual(DEFAULT_SORT);
+ });
+ });
+
+ describe.each`
+ sortParam | expected
+ ${'name_asc'} | ${{ sortBy: 'account', sortDesc: false, sortByLabel: 'Account, ascending' }}
+ ${'name_desc'} | ${{ sortBy: 'account', sortDesc: true, sortByLabel: 'Account, descending' }}
+ ${'last_joined'} | ${{ sortBy: 'granted', sortDesc: false, sortByLabel: 'Access granted, ascending' }}
+ ${'oldest_joined'} | ${{ sortBy: 'granted', sortDesc: true, sortByLabel: 'Access granted, descending' }}
+ ${'access_level_asc'} | ${{ sortBy: 'maxRole', sortDesc: false, sortByLabel: 'Max role, ascending' }}
+ ${'access_level_desc'} | ${{ sortBy: 'maxRole', sortDesc: true, sortByLabel: 'Max role, descending' }}
+ ${'recent_sign_in'} | ${{ sortBy: 'lastSignIn', sortDesc: false, sortByLabel: 'Last sign-in, ascending' }}
+ ${'oldest_sign_in'} | ${{ sortBy: 'lastSignIn', sortDesc: true, sortByLabel: 'Last sign-in, descending' }}
+ `('when `sort` query string param is `$sortParam`', ({ sortParam, expected }) => {
+ it(`returns ${JSON.stringify(expected)}`, async () => {
+ window.location.search = `?sort=${sortParam}`;
+
+ expect(parseSortParam(['account', 'granted', 'expires', 'maxRole', 'lastSignIn'])).toEqual(
+ expected,
+ );
+ });
+ });
+ });
+
+ describe('buildSortUrl', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(URL_HOST);
+ });
+
+ describe('when field passed in `sortBy` argument does not have `sort` key defined', () => {
+ it('returns an empty string', () => {
+ expect(
+ buildSortUrl({
+ sortBy: 'source',
+ sortDesc: false,
+ filteredSearchBarTokens: [],
+ filteredSearchBarSearchParam: 'search',
+ }),
+ ).toBe('');
+ });
+ });
+
+ describe('when there are no filter params set', () => {
+ it('sets `sort` param', () => {
+ expect(
+ buildSortUrl({
+ sortBy: 'account',
+ sortDesc: false,
+ filteredSearchBarTokens: [],
+ filteredSearchBarSearchParam: 'search',
+ }),
+ ).toBe(`${URL_HOST}?sort=name_asc`);
+ });
+ });
+
+ describe('when filter params are set', () => {
+ it('merges the `sort` param with the filter params', () => {
+ window.location.search = '?two_factor=enabled&with_inherited_permissions=exclude';
+
+ expect(
+ buildSortUrl({
+ sortBy: 'account',
+ sortDesc: false,
+ filteredSearchBarTokens: ['two_factor', 'with_inherited_permissions'],
+ filteredSearchBarSearchParam: 'search',
+ }),
+ ).toBe(`${URL_HOST}?two_factor=enabled&with_inherited_permissions=exclude&sort=name_asc`);
+ });
+ });
+
+ describe('when search param is set', () => {
+ it('merges the `sort` param with the search param', () => {
+ window.location.search = '?search=foobar';
+
+ expect(
+ buildSortUrl({
+ sortBy: 'account',
+ sortDesc: false,
+ filteredSearchBarTokens: ['two_factor', 'with_inherited_permissions'],
+ filteredSearchBarSearchParam: 'search',
+ }),
+ ).toBe(`${URL_HOST}?search=foobar&sort=name_asc`);
+ });
+ });
+ });
});
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 84b3f99b89a..c671379c4b4 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -33,6 +33,16 @@ RSpec.describe MembersHelper do
expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to remove this orphaned member from the #{group.name} group and any subresources?"
end
end
+
+ context 'a pending member invitation with no user associated' do
+ before do
+ project_member_invite.update_columns(invite_email: "#{SecureRandom.hex}@example.com", invite_token: 'some-token', user_id: nil)
+ end
+
+ it 'does not error when there is an invitation for the requestor' do
+ expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?"
+ end
+ end
end
describe '#remove_member_title' do
diff --git a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb
index e806f6478b7..cf7b0dbb5fd 100644
--- a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb
+++ b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
require "rack/test"
@@ -104,6 +103,12 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
expect(subject.call(env)).not_to eq error_400
end
+
+ it 'does not reject correct encoded password with special characters' do
+ env = env_for.merge(auth_env("username", "RçKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U", nil))
+
+ expect(subject.call(env)).not_to eq error_400
+ end
end
context 'in params' do
diff --git a/spec/mailers/emails/projects_spec.rb b/spec/mailers/emails/projects_spec.rb
index 6c23625d4a3..a1f19a972f1 100644
--- a/spec/mailers/emails/projects_spec.rb
+++ b/spec/mailers/emails/projects_spec.rb
@@ -19,11 +19,9 @@ RSpec.describe Emails::Projects do
create(:project_incident_management_setting, project: project, create_issue: true)
end
- let(:incident_issues_url) do
- project_issues_url(project, label_name: 'incident')
- end
+ let(:incidents_url) { project_incidents_url(project) }
- it { is_expected.to have_body_text(incident_issues_url) }
+ it { is_expected.to have_body_text(incidents_url) }
end
end
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index 75586970abb..aeb8e3642ed 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe API::Invitations do
it 'does not transform the requester into a proper member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
- params: { email: email, access_level: Member::MAINTAINER }
+ params: { email: access_requester.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
end.not_to change { source.members.count }
@@ -71,7 +71,7 @@ RSpec.describe API::Invitations do
params: { email: email, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
- end.to change { source.requesters.count }.by(1)
+ end.to change { source.members.invite.count }.by(1)
end
it 'invites a list of new email addresses' do
@@ -82,7 +82,7 @@ RSpec.describe API::Invitations do
params: { email: email_list, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
- end.to change { source.requesters.count }.by(2)
+ end.to change { source.members.invite.count }.by(2)
end
end
@@ -140,7 +140,7 @@ RSpec.describe API::Invitations do
it 'invites a member' do
expect do
subject
- end.to change { source.requesters.count }.by(1)
+ end.to change { source.members.invite.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 479edf5e873..bc89dc2fa77 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -280,6 +280,20 @@ RSpec.describe 'Git HTTP requests' do
project.add_developer(user)
end
+ context 'when user is using credentials with special characters' do
+ context 'with password with special characters' do
+ before do
+ user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U')
+ end
+
+ it 'allows clones' do
+ download(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
+
context 'but the repo is disabled' do
let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) }
diff --git a/spec/services/incident_management/incidents/create_service_spec.rb b/spec/services/incident_management/incidents/create_service_spec.rb
index 1330f3ae033..4601bd807d0 100644
--- a/spec/services/incident_management/incidents/create_service_spec.rb
+++ b/spec/services/incident_management/incidents/create_service_spec.rb
@@ -37,6 +37,8 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
end
let(:issue) { new_issue }
+
+ include_examples 'has incident label'
end
context 'with default severity' do
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index eeac7fb9923..cc6a49fc4cf 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe Issues::CreateService do
subject { issue }
it_behaves_like 'incident issue'
+ it_behaves_like 'has incident label'
it_behaves_like 'an incident management tracked event', :incident_management_incident_created
it 'does create an incident label' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 55d6d5cfe2d..06a6a52bc41 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -99,31 +99,18 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'when issue type is incident' do
let(:issue) { create(:incident, project: project) }
- it 'changes updates the severity' do
+ before do
update_issue(opts)
-
- expect(issue.severity).to eq('low')
- end
-
- it_behaves_like 'incident issue' do
- before do
- update_issue(opts)
- end
end
- context 'with existing incident label' do
- let_it_be(:incident_label) { create(:label, :incident, project: project) }
+ it_behaves_like 'incident issue'
- before do
- opts.delete(:label_ids) # don't override but retain existing labels
- issue.labels << incident_label
- end
+ it 'changes updates the severity' do
+ expect(issue.severity).to eq('low')
+ end
- it_behaves_like 'incident issue' do
- before do
- update_issue(opts)
- end
- end
+ it 'does not apply incident labels' do
+ expect(issue.labels).to match_array [label]
end
end
@@ -155,7 +142,6 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'issue in incident type' do
let(:current_user) { user }
- let(:incident_label_attributes) { attributes_for(:label, :incident) }
before do
opts.merge!(issue_type: 'incident', confidential: true)
@@ -170,21 +156,6 @@ RSpec.describe Issues::UpdateService, :mailer do
subject
end
end
-
- it 'does create an incident label' do
- expect { subject }
- .to change { Label.where(incident_label_attributes).count }.by(1)
- end
-
- context 'when invalid' do
- before do
- opts.merge!(title: '')
- end
-
- it 'does not create an incident label prematurely' do
- expect { subject }.not_to change(Label, :count)
- end
- end
end
it 'updates open issue counter for assignees when issue is reassigned' do
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 12a1a54696b..08cdf0d3ae1 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -63,4 +63,15 @@ RSpec.describe Members::InviteService do
expect(result[:status]).to eq(:error)
expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
end
+
+ it 'does not add a member with an access_request' do
+ requested_member = create(:project_member, :access_request, project: project)
+
+ params = { email: requested_member.user.email,
+ access_level: Gitlab::Access::GUEST }
+ result = described_class.new(user, params).execute(project)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message][requested_member.user.email]).to eq("Member cannot be invited because they already requested to join #{project.name}")
+ end
end
diff --git a/spec/services/packages/create_event_service_spec.rb b/spec/services/packages/create_event_service_spec.rb
index 4db7687bb24..f581d704087 100644
--- a/spec/services/packages/create_event_service_spec.rb
+++ b/spec/services/packages/create_event_service_spec.rb
@@ -70,12 +70,34 @@ RSpec.describe Packages::CreateEventService do
end
it 'tracks the event' do
+ expect(::Gitlab::UsageDataCounters::GuestPackageEventCounter).not_to receive(:count)
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(user.id, Packages::Event.allowed_event_name(expected_scope, event_name, originator_type))
subject
end
end
+ shared_examples 'redis package guest event creation' do |originator_type, expected_scope|
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(collect_package_events_redis: false)
+ end
+
+ it 'does not track the event' do
+ expect(::Gitlab::UsageDataCounters::GuestPackageEventCounter).not_to receive(:count)
+
+ subject
+ end
+ end
+
+ it 'tracks the event' do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+ expect(::Gitlab::UsageDataCounters::GuestPackageEventCounter).to receive(:count).with(Packages::Event.allowed_event_name(expected_scope, event_name, originator_type))
+
+ subject
+ end
+ end
+
context 'with a user' do
let(:user) { create(:user) }
@@ -94,6 +116,7 @@ RSpec.describe Packages::CreateEventService do
let(:user) { nil }
it_behaves_like 'db package event creation', 'guest', 'container'
+ it_behaves_like 'redis package guest event creation', 'guest', 'container'
end
context 'with a package as scope' do
@@ -103,6 +126,7 @@ RSpec.describe Packages::CreateEventService do
let(:user) { nil }
it_behaves_like 'db package event creation', 'guest', 'npm'
+ it_behaves_like 'redis package guest event creation', 'guest', 'npm'
end
context 'with user' do
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index 39c22ac8aa3..9fced12b543 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -11,11 +11,13 @@
#
# include_examples 'incident issue'
RSpec.shared_examples 'incident issue' do
- let(:label_properties) { attributes_for(:label, :incident) }
-
it 'has incident as issue type' do
expect(issue.issue_type).to eq('incident')
end
+end
+
+RSpec.shared_examples 'has incident label' do
+ let(:label_properties) { attributes_for(:label, :incident) }
it 'has exactly one incident label' do
expect(issue.labels).to be_one do |label|
diff --git a/spec/tasks/gitlab/packages/events_rake_spec.rb b/spec/tasks/gitlab/packages/events_rake_spec.rb
index 2c3885855be..a485dc2ce58 100644
--- a/spec/tasks/gitlab/packages/events_rake_spec.rb
+++ b/spec/tasks/gitlab/packages/events_rake_spec.rb
@@ -27,14 +27,14 @@ RSpec.describe 'gitlab:packages:events namespace rake task' do
end
Packages::Event::EVENT_SCOPES.keys.each do |event_scope|
- it "includes includes `#{event_scope}` scope" do
+ it "includes `#{event_scope}` scope" do
expect(subject.find { |event| event['name'].include?(event_scope) }).not_to be_nil
end
end
it 'excludes some event types' do
- expect(subject.find { |event| event['name'].include?("search_package") }).to be_nil
- expect(subject.find { |event| event['name'].include?("list_package") }).to be_nil
+ expect(subject.grep(/search_package/)).to be_empty
+ expect(subject.grep(/list_package/)).to be_empty
end
end
@@ -42,7 +42,7 @@ RSpec.describe 'gitlab:packages:events namespace rake task' do
let(:task) { 'generate_guest' }
Packages::Event::EVENT_SCOPES.keys.each do |event_scope|
- it "includes includes `#{event_scope}` scope" do
+ it "includes `#{event_scope}` scope" do
expect(subject.find { |event| event.include?(event_scope) }).not_to be_nil
end
end