summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-03-07 15:10:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-07 15:10:50 +0000
commit807c4eae46f96ccd54ce1d8d13f4547eda017267 (patch)
tree190aaf8d8c0a766fa7fc396355fd5e0d865db889
parentebe0e306bbd6e913763bf1865b7778c001994e31 (diff)
downloadgitlab-ce-807c4eae46f96ccd54ce1d8d13f4547eda017267.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/rspec/context_wording.yml2
-rw-r--r--.rubocop_todo/style/percent_literal_delimiters.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/artifacts/components/job_artifacts_table.vue8
-rw-r--r--app/assets/javascripts/work_items/components/notes/activity_filter.vue112
-rw-r--r--app/assets/javascripts/work_items/components/notes/activity_sort.vue99
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue61
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue67
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue73
-rw-r--r--app/assets/javascripts/work_items/constants.js6
-rw-r--r--app/controllers/ide_controller.rb12
-rw-r--r--app/helpers/ide_helper.rb23
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb4
-rw-r--r--app/mailers/previews/notify_preview.rb8
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/services/ci/job_artifacts/bulk_delete_by_project_service.rb5
-rw-r--r--app/services/ci/job_artifacts/destroy_all_expired_service.rb2
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb10
-rw-r--r--app/services/releases/links/base_service.rb4
-rw-r--r--app/services/releases/links/create_service.rb4
-rw-r--r--app/services/releases/links/destroy_service.rb6
-rw-r--r--app/services/releases/links/update_service.rb6
-rw-r--r--app/views/groups/_invite_members_top_nav_link.html.haml2
-rw-r--r--app/views/ide/_show.html.haml7
-rw-r--r--app/views/layouts/group.html.haml3
-rw-r--r--app/views/layouts/header/_new_dropdown.html.haml5
-rw-r--r--app/views/layouts/project.html.haml3
-rw-r--r--app/views/projects/_invite_members_top_nav_link.html.haml2
-rw-r--r--app/views/projects/branch_rules/_show.html.haml1
-rw-r--r--data/deprecations/15-1-deprecate-maintainer_note.yml15
-rw-r--r--db/post_migrate/20230301065107_add_index_on_expired_unlocked_non_trace_job_artifacts.rb17
-rw-r--r--db/schema_migrations/202303010651071
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/dedicated/index.md106
-rw-r--r--doc/administration/get_started.md4
-rw-r--r--doc/administration/package_information/supported_os.md8
-rw-r--r--doc/update/deprecations.md15
-rw-r--r--lib/api/release/links.rb12
-rw-r--r--lib/gitlab/ci/config/external/file/component.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb4
-rw-r--r--lib/gitlab/regex.rb2
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json2
-rw-r--r--spec/fast_spec_helper.rb1
-rw-r--r--spec/frontend/artifacts/components/job_artifacts_table_spec.js16
-rw-r--r--spec/frontend/work_items/components/notes/activity_filter_spec.js61
-rw-r--r--spec/frontend/work_items/components/notes/activity_sort_spec.js69
-rw-r--r--spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js44
-rw-r--r--spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js65
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js21
-rw-r--r--spec/helpers/ide_helper_spec.rb255
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb48
-rw-r--r--spec/lib/gitlab/regex_spec.rb6
-rw-r--r--spec/models/ci/job_artifact_spec.rb23
-rw-r--r--spec/models/concerns/taskable_spec.rb16
-rw-r--r--spec/requests/ide_controller_spec.rb130
-rw-r--r--spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb10
-rw-r--r--spec/services/ci/job_artifacts/destroy_associations_service_spec.rb29
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb32
-rw-r--r--spec/services/releases/links/create_service_spec.rb2
-rw-r--r--spec/services/releases/links/destroy_service_spec.rb3
-rw-r--r--spec/services/releases/links/update_service_spec.rb2
-rw-r--r--spec/support/rspec_order_todo.yml2
-rw-r--r--spec/views/layouts/group.html.haml_spec.rb30
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb2
-rw-r--r--spec/views/layouts/project.html.haml_spec.rb29
-rw-r--r--yarn.lock8
70 files changed, 1065 insertions, 588 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index e442a2a8856..146522c5e76 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -2371,7 +2371,6 @@ Layout/ArgumentAlignment:
- 'spec/helpers/avatars_helper_spec.rb'
- 'spec/helpers/emoji_helper_spec.rb'
- 'spec/helpers/feature_flags_helper_spec.rb'
- - 'spec/helpers/ide_helper_spec.rb'
- 'spec/helpers/namespaces_helper_spec.rb'
- 'spec/helpers/notify_helper_spec.rb'
- 'spec/helpers/page_layout_helper_spec.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 5508542a599..0206346fa0a 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -4927,7 +4927,6 @@ Layout/LineLength:
- 'spec/requests/groups/milestones_controller_spec.rb'
- 'spec/requests/groups/settings/access_tokens_controller_spec.rb'
- 'spec/requests/groups_controller_spec.rb'
- - 'spec/requests/ide_controller_spec.rb'
- 'spec/requests/jwt_controller_spec.rb'
- 'spec/requests/lfs_http_spec.rb'
- 'spec/requests/oauth/tokens_controller_spec.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index 1ad85791e69..d57f93a1719 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -1494,7 +1494,6 @@ RSpec/ContextWording:
- 'spec/helpers/gitlab_routing_helper_spec.rb'
- 'spec/helpers/groups/group_members_helper_spec.rb'
- 'spec/helpers/groups_helper_spec.rb'
- - 'spec/helpers/ide_helper_spec.rb'
- 'spec/helpers/integrations_helper_spec.rb'
- 'spec/helpers/jira_connect_helper_spec.rb'
- 'spec/helpers/labels_helper_spec.rb'
@@ -2575,7 +2574,6 @@ RSpec/ContextWording:
- 'spec/requests/groups/settings/access_tokens_controller_spec.rb'
- 'spec/requests/groups_controller_spec.rb'
- 'spec/requests/health_controller_spec.rb'
- - 'spec/requests/ide_controller_spec.rb'
- 'spec/requests/jira_connect/installations_controller_spec.rb'
- 'spec/requests/jira_connect/oauth_application_ids_controller_spec.rb'
- 'spec/requests/jira_routing_spec.rb'
diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml
index 66de6a35092..bc3c41ae992 100644
--- a/.rubocop_todo/style/percent_literal_delimiters.yml
+++ b/.rubocop_todo/style/percent_literal_delimiters.yml
@@ -993,7 +993,6 @@ Style/PercentLiteralDelimiters:
- 'spec/requests/api/unleash_spec.rb'
- 'spec/requests/api/users_spec.rb'
- 'spec/requests/api/wikis_spec.rb'
- - 'spec/requests/ide_controller_spec.rb'
- 'spec/requests/jwt_controller_spec.rb'
- 'spec/requests/lfs_locks_api_spec.rb'
- 'spec/requests/users_controller_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 0fd64bd648e..f60c9678b67 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1739a8ca9a5786b4730620b742153f45e00cb094
+65769c7a58d3339fe94a809bf6fd34f2f300a700
diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
index a7331cc1fd3..1b7782c6860 100644
--- a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
@@ -68,9 +68,8 @@ export default {
variables() {
return this.queryVariables;
},
- update({ project: { jobs: { nodes = [], pageInfo = {}, count = 0 } = {} } }) {
+ update({ project: { jobs: { nodes = [], pageInfo = {} } = {} } }) {
this.pageInfo = pageInfo;
- this.count = count;
return nodes
.map(mapArchivesToJobNodes)
.map(mapBooleansToJobNodes)
@@ -93,7 +92,6 @@ export default {
data() {
return {
jobArtifacts: [],
- count: 0,
pageInfo: {},
expandedJobs: [],
pagination: INITIAL_PAGINATION_STATE,
@@ -110,7 +108,9 @@ export default {
};
},
showPagination() {
- return this.count > JOBS_PER_PAGE;
+ const { hasNextPage, hasPreviousPage } = this.pageInfo;
+
+ return hasNextPage || hasPreviousPage;
},
prevPage() {
return Number(this.pageInfo.hasPreviousPage);
diff --git a/app/assets/javascripts/work_items/components/notes/activity_filter.vue b/app/assets/javascripts/work_items/components/notes/activity_filter.vue
index 71784d3a807..6d5535797ef 100644
--- a/app/assets/javascripts/work_items/components/notes/activity_filter.vue
+++ b/app/assets/javascripts/work_items/components/notes/activity_filter.vue
@@ -1,18 +1,35 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
import Tracking from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { ASC, DESC } from '~/notes/constants';
-import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+ TRACKING_CATEGORY_SHOW,
+ WORK_ITEM_NOTES_FILTER_KEY,
+} from '~/work_items/constants';
-const SORT_OPTIONS = [
- { key: DESC, text: __('Newest first'), dataid: 'js-newest-first' },
- { key: ASC, text: __('Oldest first'), dataid: 'js-oldest-first' },
+const filterOptions = [
+ {
+ key: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ text: s__('WorkItem|All activity'),
+ },
+ {
+ key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ text: s__('WorkItem|Comments only'),
+ testid: 'comments-activity',
+ },
+ {
+ key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+ text: s__('WorkItem|History only'),
+ testid: 'history-activity',
+ },
];
export default {
- SORT_OPTIONS,
+ filterOptions,
components: {
GlDropdown,
GlDropdownItem,
@@ -20,11 +37,6 @@ export default {
},
mixins: [Tracking.mixin()],
props: {
- sortOrder: {
- type: String,
- default: ASC,
- required: false,
- },
loading: {
type: Boolean,
default: false,
@@ -34,80 +46,74 @@ export default {
type: String,
required: true,
},
- },
- data() {
- return {
- persistSortOrder: true,
- };
+ discussionFilter: {
+ type: String,
+ default: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ required: false,
+ },
},
computed: {
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_sorting',
+ label: 'item_track_notes_filtering',
property: `type_${this.workItemType}`,
};
},
- selectedSortOption() {
- const isSortOptionValid = this.sortOrder === ASC || this.sortOrder === DESC;
- return isSortOptionValid ? SORT_OPTIONS.find(({ key }) => this.sortOrder === key) : ASC;
- },
getDropdownSelectedText() {
return this.selectedSortOption.text;
},
+ selectedSortOption() {
+ return (
+ filterOptions.find(({ key }) => this.discussionFilter === key) ||
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES
+ );
+ },
},
methods: {
- setDiscussionSortDirection(direction) {
- this.$emit('updateSavedSortOrder', direction);
+ setDiscussionFilterOption(filterValue) {
+ this.$emit('changeFilter', filterValue);
},
- fetchSortedDiscussions(direction) {
- if (this.isSortDropdownItemActive(direction)) {
+ fetchFilteredDiscussions(filterValue) {
+ if (this.isSortDropdownItemActive(filterValue)) {
return;
}
- this.track('notes_sort_order_changed');
- this.$emit('changeSortOrder', direction);
+ this.track('work_item_notes_filter_changed');
+ this.$emit('changeFilter', filterValue);
},
- isSortDropdownItemActive(sortDir) {
- return sortDir === this.sortOrder;
+ isSortDropdownItemActive(discussionFilter) {
+ return discussionFilter === this.discussionFilter;
},
},
- WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ WORK_ITEM_NOTES_FILTER_KEY,
};
</script>
<template>
- <div
- id="discussion-preferences"
- data-testid="discussion-preferences"
- class="gl-display-inline-block gl-vertical-align-bottom gl-w-full gl-sm-w-auto"
- >
+ <div class="gl-display-inline-block gl-vertical-align-bottom">
<local-storage-sync
- :value="sortOrder"
- :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
- :persist="persistSortOrder"
+ :value="discussionFilter"
+ :storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
as-string
- @input="setDiscussionSortDirection"
+ @input="setDiscussionFilterOption"
/>
<gl-dropdown
- :id="`discussion-preferences-dropdown-${workItemType}`"
class="gl-xs-w-full"
size="small"
:text="getDropdownSelectedText"
:disabled="loading"
right
>
- <div id="discussion-sort">
- <gl-dropdown-item
- v-for="{ text, key, dataid } in $options.SORT_OPTIONS"
- :key="text"
- :data-testid="dataid"
- is-check-item
- :is-checked="isSortDropdownItemActive(key)"
- @click="fetchSortedDiscussions(key)"
- >
- {{ text }}
- </gl-dropdown-item>
- </div>
+ <gl-dropdown-item
+ v-for="{ text, key, testid } in $options.filterOptions"
+ :key="text"
+ :data-testid="testid"
+ is-check-item
+ :is-checked="isSortDropdownItemActive(key)"
+ @click="fetchFilteredDiscussions(key)"
+ >
+ {{ text }}
+ </gl-dropdown-item>
</gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/notes/activity_sort.vue b/app/assets/javascripts/work_items/components/notes/activity_sort.vue
new file mode 100644
index 00000000000..bfbb2b65346
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/activity_sort.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { ASC, DESC } from '~/notes/constants';
+import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants';
+
+const sortOptions = [
+ { key: DESC, text: __('Newest first'), testid: 'newest-first' },
+ { key: ASC, text: __('Oldest first') },
+];
+
+export default {
+ sortOptions,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ LocalStorageSync,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ sortOrder: {
+ type: String,
+ default: ASC,
+ required: false,
+ },
+ loading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_track_notes_sorting',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ selectedSortOption() {
+ return sortOptions.find(({ key }) => this.sortOrder === key) || ASC;
+ },
+ getDropdownSelectedText() {
+ return this.selectedSortOption.text;
+ },
+ },
+ methods: {
+ setDiscussionSortDirection(direction) {
+ this.$emit('changeSort', direction);
+ },
+ fetchSortedDiscussions(direction) {
+ if (this.isSortDropdownItemActive(direction)) {
+ return;
+ }
+ this.track('work_item_notes_sort_order_changed');
+ this.$emit('changeSort', direction);
+ },
+ isSortDropdownItemActive(sortDir) {
+ return sortDir === this.sortOrder;
+ },
+ },
+ WORK_ITEM_NOTES_SORT_ORDER_KEY,
+};
+</script>
+
+<template>
+ <div class="gl-display-inline-block gl-vertical-align-bottom">
+ <local-storage-sync
+ :value="sortOrder"
+ :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
+ as-string
+ @input="setDiscussionSortDirection"
+ />
+ <gl-dropdown
+ class="gl-xs-w-full"
+ size="small"
+ :text="getDropdownSelectedText"
+ :disabled="loading"
+ right
+ >
+ <gl-dropdown-item
+ v-for="{ text, key, testid } in $options.sortOptions"
+ :key="text"
+ :data-testid="testid"
+ is-check-item
+ :is-checked="isSortDropdownItemActive(key)"
+ @click="fetchSortedDiscussions(key)"
+ >
+ {{ text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue
new file mode 100644
index 00000000000..07e25312f87
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+} from '~/work_items/constants';
+
+export default {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ i18n: {
+ information: s__(
+ "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options.",
+ ),
+ },
+ components: {
+ GlButton,
+ GlIcon,
+ GlSprintf,
+ },
+ methods: {
+ selectFilter(value) {
+ this.$emit('changeFilter', value);
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="timeline-entry note note-wrapper discussion-filter-note">
+ <div class="timeline-icon gl-display-none gl-lg-display-flex">
+ <gl-icon name="comment" />
+ </div>
+ <div class="timeline-content gl-pl-8">
+ <gl-sprintf :message="$options.i18n.information">
+ <template #bold="{ content }">
+ <b>{{ content }}</b>
+ </template>
+ </gl-sprintf>
+
+ <div class="discussion-filter-actions">
+ <gl-button
+ class="gl-mr-2 gl-mt-3"
+ data-testid="show-all-activity"
+ @click="selectFilter($options.WORK_ITEM_NOTES_FILTER_ALL_NOTES)"
+ >
+ {{ __('Show all activity') }}
+ </gl-button>
+ <gl-button
+ class="gl-mt-3"
+ data-testid="show-comments-only"
+ @click="selectFilter($options.WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS)"
+ >
+ {{ __('Show comments only') }}
+ </gl-button>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
new file mode 100644
index 00000000000..e700d5353e2
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
@@ -0,0 +1,67 @@
+<script>
+import ActivitySort from '~/work_items/components/notes/activity_sort.vue';
+import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
+import { s__ } from '~/locale';
+import { ASC } from '~/notes/constants';
+import { WORK_ITEM_NOTES_FILTER_ALL_NOTES } from '~/work_items/constants';
+
+export default {
+ i18n: {
+ activityLabel: s__('WorkItem|Activity'),
+ },
+ components: {
+ ActivitySort,
+ ActivityFilter,
+ },
+ props: {
+ disableActivityFilterSort: {
+ type: Boolean,
+ required: true,
+ },
+ sortOrder: {
+ type: String,
+ default: ASC,
+ required: false,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ discussionFilter: {
+ type: String,
+ default: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ required: false,
+ },
+ },
+ methods: {
+ changeNotesSortOrder(direction) {
+ this.$emit('changeSort', direction);
+ },
+ filterDiscussions(filterValue) {
+ this.$emit('changeFilter', filterValue);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-pb-3 gl-align-items-center"
+ >
+ <h3 class="gl-font-base gl-m-0">{{ $options.i18n.activityLabel }}</h3>
+ <div class="gl-display-flex gl-gap-3">
+ <activity-filter
+ :loading="disableActivityFilterSort"
+ :work-item-type="workItemType"
+ :discussion-filter="discussionFilter"
+ @changeFilter="filterDiscussions"
+ />
+ <activity-sort
+ :loading="disableActivityFilterSort"
+ :sort-order="sortOrder"
+ :work-item-type="workItemType"
+ @changeSort="changeNotesSortOrder"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index aa6dd9b5184..331d0b5e8d0 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -1,11 +1,17 @@
<script>
import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
-import { s__, __ } from '~/locale';
+import { __ } from '~/locale';
import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants';
import SystemNote from '~/work_items/components/notes/system_note.vue';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
-import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
+import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
+import {
+ i18n,
+ DEFAULT_PAGE_SIZE_NOTES,
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+} from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
import { getWorkItemNotesQuery } from '~/work_items/utils';
import {
@@ -13,6 +19,7 @@ import {
updateCacheAfterDeletingNote,
} from '~/work_items/graphql/cache_utils';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue';
import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql';
import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql';
import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql';
@@ -20,9 +27,6 @@ import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation
import WorkItemAddNote from './notes/work_item_add_note.vue';
export default {
- i18n: {
- ACTIVITY_LABEL: s__('WorkItem|Activity'),
- },
loader: {
repeat: 10,
width: 1000,
@@ -31,10 +35,11 @@ export default {
components: {
GlSkeletonLoader,
GlModal,
- ActivityFilter,
SystemNote,
WorkItemAddNote,
WorkItemDiscussion,
+ WorkItemNotesActivityHeader,
+ WorkItemHistoryOnlyFilterNote,
},
props: {
workItemId: {
@@ -65,6 +70,7 @@ export default {
perPage: DEFAULT_PAGE_SIZE_NOTES,
sortOrder: ASC,
noteToDelete: null,
+ discussionFilter: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
};
},
computed: {
@@ -83,7 +89,7 @@ export default {
showLoadingMoreSkeleton() {
return this.isLoadingMore && !this.changeNotesSortOrderAfterLoading;
},
- disableActivityFilter() {
+ disableActivityFilterSort() {
return this.initialLoading || this.isLoadingMore;
},
formAtTop() {
@@ -102,10 +108,27 @@ export default {
notesArray() {
const notes = this.workItemNotes?.nodes || [];
+ const visibleNotes = notes.filter((note) => {
+ const isSystemNote = this.isSystemNote(note);
+
+ if (this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS && isSystemNote) {
+ return false;
+ }
+
+ if (this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_HISTORY && !isSystemNote) {
+ return false;
+ }
+
+ return true;
+ });
+
if (this.sortOrder === DESC) {
- return [...notes].reverse();
+ return [...visibleNotes].reverse();
}
- return notes;
+ return visibleNotes;
+ },
+ commentsDisabled() {
+ return this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_HISTORY;
},
},
apollo: {
@@ -210,6 +233,9 @@ export default {
changeNotesSortOrder(direction) {
this.sortOrder = direction;
},
+ filterDiscussions(filterValue) {
+ this.discussionFilter = filterValue;
+ },
async fetchMoreNotes() {
this.isLoadingMore = true;
// copied from discussions batch logic - every fetchMore call has a higher
@@ -271,17 +297,14 @@ export default {
<template>
<div class="gl-border-t gl-mt-5 work-item-notes">
- <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
- <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label>
- <activity-filter
- class="gl-min-h-5 gl-pb-3"
- :loading="disableActivityFilter"
- :sort-order="sortOrder"
- :work-item-type="workItemType"
- @changeSortOrder="changeNotesSortOrder"
- @updateSavedSortOrder="changeNotesSortOrder"
- />
- </div>
+ <work-item-notes-activity-header
+ :sort-order="sortOrder"
+ :disable-activity-filter-sort="disableActivityFilterSort"
+ :work-item-type="workItemType"
+ :discussion-filter="discussionFilter"
+ @changeSort="changeNotesSortOrder"
+ @changeFilter="filterDiscussions"
+ />
<div v-if="initialLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
@@ -298,7 +321,7 @@ export default {
<template v-if="!initialLoading">
<ul class="notes main-notes-list timeline gl-clearfix!">
<work-item-add-note
- v-if="formAtTop"
+ v-if="formAtTop && !commentsDisabled"
v-bind="workItemCommentFormProps"
@error="$emit('error', $event)"
/>
@@ -325,10 +348,14 @@ export default {
</template>
<work-item-add-note
- v-if="!formAtTop"
+ v-if="!formAtTop && !commentsDisabled"
v-bind="workItemCommentFormProps"
@error="$emit('error', $event)"
/>
+ <work-item-history-only-filter-note
+ v-if="commentsDisabled"
+ @changeFilter="filterDiscussions"
+ />
</ul>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 81f9bf04bc8..b372f2d6f7b 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -176,3 +176,9 @@ export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
export const DEFAULT_PAGE_SIZE_NOTES = 30;
export const WORK_ITEM_NOTES_SORT_ORDER_KEY = 'sort_direction_work_item';
+
+export const WORK_ITEM_NOTES_FILTER_ALL_NOTES = 'ALL_NOTES';
+export const WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS = 'ONLY_COMMENTS';
+export const WORK_ITEM_NOTES_FILTER_ONLY_HISTORY = 'ONLY_HISTORY';
+
+export const WORK_ITEM_NOTES_FILTER_KEY = 'filter_key_work_item';
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 1b3d9223502..18c6f0bb9d3 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -10,7 +10,6 @@ class IdeController < ApplicationController
before_action do
push_frontend_feature_flag(:build_service_proxy)
push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab)
- define_index_vars
end
feature_category :web_ide
@@ -22,6 +21,7 @@ class IdeController < ApplicationController
if project
Gitlab::Tracking.event(self.class.to_s, 'web_ide_views', namespace: project.namespace, user: current_user)
+ @fork_info = fork_info(project, params[:branch])
end
render layout: 'fullscreen', locals: { minimal: helpers.use_new_web_ide? }
@@ -33,16 +33,6 @@ class IdeController < ApplicationController
render_404 unless can?(current_user, :read_project, project)
end
- def define_index_vars
- return unless project
-
- @branch = params[:branch]
- @path = params[:path]
- @merge_request = params[:merge_request_id]
- @learn_gitlab_source = params[:learn_gitlab_source]
- @fork_info = fork_info(project, @branch)
- end
-
def fork_info(project, branch)
return if can?(current_user, :push_code, project)
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 296fe6856ac..063eef41f77 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -1,21 +1,26 @@
# frozen_string_literal: true
module IdeHelper
- def ide_data(project:, branch:, path:, merge_request:, fork_info:, learn_gitlab_source:)
- {
+ # Overridden in EE
+ def ide_data(project:, fork_info:, params:)
+ base_data = {
'can-use-new-web-ide' => can_use_new_web_ide?.to_s,
'use-new-web-ide' => use_new_web_ide?.to_s,
'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
'user-preferences-path' => profile_preferences_path,
- 'branch-name' => branch,
- 'file-path' => path,
- 'fork-info' => fork_info&.to_json,
'editor-font-src-url' => font_url('jetbrains-mono/JetBrainsMono.woff2'),
'editor-font-family' => 'JetBrains Mono',
- 'editor-font-format' => 'woff2',
- 'merge-request' => merge_request,
- 'learn-gitlab-source' => (!!learn_gitlab_source).to_s
+ 'editor-font-format' => 'woff2'
}.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project))
+
+ return base_data unless project
+
+ base_data.merge(
+ 'fork-info' => fork_info&.to_json,
+ 'branch-name' => params[:branch],
+ 'file-path' => params[:path],
+ 'merge-request' => params[:merge_request_id]
+ )
end
def can_use_new_web_ide?
@@ -77,3 +82,5 @@ module IdeHelper
current_user.dismissed_callout?(feature_name: 'web_ide_ci_environments_guidance')
end
end
+
+IdeHelper.prepend_mod_with('IdeHelper')
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 81d083bd082..0d93aff2bae 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -156,7 +156,7 @@ module IssuablesHelper
end
output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline")
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block")
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none")
author_output << issuable_meta_author_slot(issuable.author, css_class: 'ml-1')
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index 89211ed6a3e..201007863b2 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -2,7 +2,7 @@
module Nav
module NewDropdownHelper
- def new_dropdown_view_model(group:, project:, with_context: false)
+ def new_dropdown_view_model(group:, project:)
return unless current_user
menu_sections = []
@@ -10,10 +10,8 @@ module Nav
if project&.persisted?
menu_sections.push(project_menu_section(project))
- data[:context] = project if with_context
elsif group&.persisted?
menu_sections.push(group_menu_section(group))
- data[:context] = group if with_context
end
menu_sections.push(general_menu_section)
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 0d98c5a176a..17b225c5e9b 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -60,6 +60,14 @@ class NotifyPreview < ActionMailer::Preview
end
end
+ def access_token_created_email
+ Notify.access_token_created_email(user, 'token_name').message
+ end
+
+ def access_token_revoked_email
+ Notify.access_token_revoked_email(user, 'token_name').message
+ end
+
def new_mention_in_merge_request_email
Notify.new_mention_in_merge_request_email(user.id, merge_request.id, user.id).message
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 89a3d269a43..92ab2e1af25 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -177,6 +177,8 @@ module Ci
where(file_type: self.erasable_file_types)
end
+ scope :non_trace, -> { where.not(file_type: [:trace]) }
+
scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) }
scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked) }
scope :order_expired_asc, -> { order(expire_at: :asc) }
diff --git a/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb b/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb
index 7862774473c..738fa19e29b 100644
--- a/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb
+++ b/app/services/ci/job_artifacts/bulk_delete_by_project_service.rb
@@ -30,10 +30,7 @@ module Ci
return ServiceResponse.error(message: 'Not all artifacts belong to requested project')
end
- result = Ci::JobArtifacts::DestroyBatchService.new(
- job_artifact_scope,
- skip_trace_artifacts: false
- ).execute
+ result = Ci::JobArtifacts::DestroyBatchService.new(job_artifact_scope).execute
destroyed_artifacts_count = result.fetch(:destroyed_artifacts_count)
destroyed_ids = result.fetch(:destroyed_ids)
diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
index 30683475ad2..57b95e59d7d 100644
--- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
@@ -35,7 +35,7 @@ module Ci
def destroy_unlocked_job_artifacts
loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
- artifacts = Ci::JobArtifact.expired_before(@start_at).artifact_unlocked.limit(BATCH_SIZE)
+ artifacts = Ci::JobArtifact.expired_before(@start_at).non_trace.artifact_unlocked.limit(BATCH_SIZE)
service_response = destroy_batch(artifacts)
@removed_artifacts_count += service_response[:destroyed_artifacts_count]
end
diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb
index 94ecfa96be0..81cbeb31711 100644
--- a/app/services/ci/job_artifacts/destroy_batch_service.rb
+++ b/app/services/ci/job_artifacts/destroy_batch_service.rb
@@ -17,11 +17,10 @@ module Ci
# +pick_up_at+:: When to pick up for deletion of files
# Returns:
# +Hash+:: A hash with status and destroyed_artifacts_count keys
- def initialize(job_artifacts, pick_up_at: nil, skip_projects_on_refresh: false, skip_trace_artifacts: true)
+ def initialize(job_artifacts, pick_up_at: nil, skip_projects_on_refresh: false)
@job_artifacts = job_artifacts.with_destroy_preloads.to_a
@pick_up_at = pick_up_at
@skip_projects_on_refresh = skip_projects_on_refresh
- @skip_trace_artifacts = skip_trace_artifacts
@destroyed_ids = []
end
@@ -33,8 +32,6 @@ module Ci
track_artifacts_undergoing_stats_refresh
end
- exclude_trace_artifacts if @skip_trace_artifacts
-
if @job_artifacts.empty?
return success(destroyed_ids: @destroyed_ids, destroyed_artifacts_count: 0, statistics_updates: {})
end
@@ -119,11 +116,6 @@ module Ci
end
end
- # Traces should never be destroyed.
- def exclude_trace_artifacts
- _trace_artifacts, @job_artifacts = @job_artifacts.partition(&:trace?)
- end
-
def track_artifacts_undergoing_stats_refresh
project_ids = @job_artifacts.find_all do |artifact|
artifact.project.refreshing_build_artifacts_size?
diff --git a/app/services/releases/links/base_service.rb b/app/services/releases/links/base_service.rb
index 939de982db4..8bab258f80a 100644
--- a/app/services/releases/links/base_service.rb
+++ b/app/services/releases/links/base_service.rb
@@ -2,6 +2,10 @@
module Releases
module Links
+ REASON_BAD_REQUEST = :bad_request
+ REASON_NOT_FOUND = :not_found
+ REASON_FORBIDDEN = :forbidden
+
class BaseService
attr_accessor :release, :current_user, :params
diff --git a/app/services/releases/links/create_service.rb b/app/services/releases/links/create_service.rb
index c73c9f40254..94823c54596 100644
--- a/app/services/releases/links/create_service.rb
+++ b/app/services/releases/links/create_service.rb
@@ -4,14 +4,14 @@ module Releases
module Links
class CreateService < BaseService
def execute
- return ServiceResponse.error(message: _('Access Denied')) unless allowed?
+ return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed?
link = release.links.create(allowed_params)
if link.persisted?
ServiceResponse.success(payload: { link: link })
else
- ServiceResponse.error(message: link.errors.full_messages)
+ ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages)
end
end
diff --git a/app/services/releases/links/destroy_service.rb b/app/services/releases/links/destroy_service.rb
index 9edde2f357b..1c1158017bb 100644
--- a/app/services/releases/links/destroy_service.rb
+++ b/app/services/releases/links/destroy_service.rb
@@ -4,13 +4,13 @@ module Releases
module Links
class DestroyService < BaseService
def execute(link)
- return ServiceResponse.error(message: _('Access Denied')) unless allowed?
- return ServiceResponse.error(message: _('Link does not exist')) unless link
+ return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed?
+ return ServiceResponse.error(reason: REASON_NOT_FOUND, message: _('Link does not exist')) unless link
if link.destroy
ServiceResponse.success(payload: { link: link })
else
- ServiceResponse.error(message: link.errors.full_messages)
+ ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages)
end
end
diff --git a/app/services/releases/links/update_service.rb b/app/services/releases/links/update_service.rb
index f50cde5c5a9..c29de86f31b 100644
--- a/app/services/releases/links/update_service.rb
+++ b/app/services/releases/links/update_service.rb
@@ -4,13 +4,13 @@ module Releases
module Links
class UpdateService < BaseService
def execute(link)
- return ServiceResponse.error(message: _('Access Denied')) unless allowed?
- return ServiceResponse.error(message: _('Link does not exist')) unless link
+ return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed?
+ return ServiceResponse.error(reason: REASON_NOT_FOUND, message: _('Link does not exist')) unless link
if link.update(allowed_params)
ServiceResponse.success(payload: { link: link })
else
- ServiceResponse.error(message: link.errors.full_messages)
+ ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages)
end
end
diff --git a/app/views/groups/_invite_members_top_nav_link.html.haml b/app/views/groups/_invite_members_top_nav_link.html.haml
index e419c479bca..35a8d4d9944 100644
--- a/app/views/groups/_invite_members_top_nav_link.html.haml
+++ b/app/views/groups/_invite_members_top_nav_link.html.haml
@@ -3,5 +3,3 @@
- data[:icon] = local_assigns.fetch(:icon)
.js-invite-members-trigger{ data: data }
-
-= render 'groups/invite_members_modal', group: local_assigns.fetch(:context)
diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml
index 091d7e7a4f1..eb6d5668807 100644
--- a/app/views/ide/_show.html.haml
+++ b/app/views/ide/_show.html.haml
@@ -7,11 +7,6 @@
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')
-- data = ide_data(project: @project,
- branch: @branch,
- path: @path,
- merge_request: @merge_request,
- fork_info: @fork_info,
- learn_gitlab_source: @learn_gitlab_source)
+- data = ide_data(project: @project, fork_info: @fork_info, params: params)
= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the GitLab IDE...') }
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 28579c7f7ea..40ec1ff199b 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -16,6 +16,9 @@
:plain
window.uploads_path = "#{group_uploads_path(@group)}";
+- content_for :before_content do
+ = render 'groups/invite_members_modal', group: @group
+
= dispensable_render_if_exists "shared/web_hooks/group_web_hook_disabled_alert"
= dispensable_render_if_exists "shared/free_user_cap_alert", source: @group
diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml
index 42c2fd645da..50a2b45aa7e 100644
--- a/app/views/layouts/header/_new_dropdown.html.haml
+++ b/app/views/layouts/header/_new_dropdown.html.haml
@@ -1,4 +1,4 @@
-- view_model = new_dropdown_view_model(project: @project, group: @group, with_context: true)
+- view_model = new_dropdown_view_model(project: @project, group: @group)
- menu_sections = view_model.fetch(:menu_sections)
- title = view_model.fetch(:title)
- show_headers = menu_sections.length > 1
@@ -28,8 +28,7 @@
%li<
- if menu_item.fetch(:partial).present?
= render partial: menu_item.fetch(:partial),
- locals: { context: view_model[:context],
- display_text: menu_item.fetch(:title),
+ locals: { display_text: menu_item.fetch(:title),
icon: menu_item.fetch(:icon),
data: menu_item.fetch(:data) }
- else
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 6ad6696b313..09fa8575106 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -18,6 +18,9 @@
:plain
window.uploads_path = "#{project_uploads_path(project)}";
+- content_for :before_content do
+ = render 'projects/invite_members_modal', project: @project
+
= dispensable_render_if_exists "shared/web_hooks/web_hook_disabled_alert"
= dispensable_render_if_exists "projects/free_user_cap_alert", project: @project
diff --git a/app/views/projects/_invite_members_top_nav_link.html.haml b/app/views/projects/_invite_members_top_nav_link.html.haml
index d2e68325a09..35a8d4d9944 100644
--- a/app/views/projects/_invite_members_top_nav_link.html.haml
+++ b/app/views/projects/_invite_members_top_nav_link.html.haml
@@ -3,5 +3,3 @@
- data[:icon] = local_assigns.fetch(:icon)
.js-invite-members-trigger{ data: data }
-
-= render 'projects/invite_members_modal', project: local_assigns.fetch(:context)
diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml
index 32b093bb95c..c0362f3e85d 100644
--- a/app/views/projects/branch_rules/_show.html.haml
+++ b/app/views/projects/branch_rules/_show.html.haml
@@ -10,6 +10,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _('Define rules for who can push, merge, and the required approvals for each branch.')
+ = link_to(_('Leave feadback.'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/388149', target: '_blank', rel: 'noopener noreferrer')
.settings-content.gl-pr-0
#js-branch-rules{ data: { project_path: @project.full_path, branch_rules_path: project_settings_repository_branch_rules_path(@project), show_code_owners: show_code_owners.to_s, show_status_checks: show_status_checks.to_s, show_approvers: show_approvers.to_s } }
diff --git a/data/deprecations/15-1-deprecate-maintainer_note.yml b/data/deprecations/15-1-deprecate-maintainer_note.yml
deleted file mode 100644
index 175f85e997f..00000000000
--- a/data/deprecations/15-1-deprecate-maintainer_note.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-- title: "REST API Runner maintainer_note" # (required) The name of the feature to be deprecated
- announcement_milestone: "15.1" # (required) The milestone when this feature was first announced as deprecated.
- removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
- breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
- reporter: pedropombeiro # (required) GitLab username of the person reporting the deprecation
- stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
- issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363192 # (required) Link to the deprecation issue in GitLab
- body: | # (required) Do not modify this line, instead modify the lines below.
- The `maintainer_note` argument in the `POST /runners` REST endpoint was deprecated in GitLab 14.8 and replaced with the `maintenance_note` argument.
- The `maintainer_note` argument will be removed in GitLab 16.0.
-# The following items are not published on the docs page, but may be used in the future.
- tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
- documentation_url: https://docs.gitlab.com/ee/api/runners.html#register-a-new-runner # (optional) This is a link to the current documentation page
- image_url: # (optional) This is a link to a thumbnail image depicting the feature
- video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
diff --git a/db/post_migrate/20230301065107_add_index_on_expired_unlocked_non_trace_job_artifacts.rb b/db/post_migrate/20230301065107_add_index_on_expired_unlocked_non_trace_job_artifacts.rb
new file mode 100644
index 00000000000..feda6971a85
--- /dev/null
+++ b/db/post_migrate/20230301065107_add_index_on_expired_unlocked_non_trace_job_artifacts.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnExpiredUnlockedNonTraceJobArtifacts < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_ci_job_artifacts_expire_at_unlocked_non_trace'
+
+ def up
+ add_concurrent_index :ci_job_artifacts, :expire_at,
+ name: INDEX_NAME,
+ where: 'locked = 0 AND file_type != 3 AND expire_at IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index_by_name :ci_job_artifacts, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230301065107 b/db/schema_migrations/20230301065107
new file mode 100644
index 00000000000..495cefad9d2
--- /dev/null
+++ b/db/schema_migrations/20230301065107
@@ -0,0 +1 @@
+7e464616bdef6e225fdd31db84c4c32e223dffb81e13f1d6a5c85c2cd0a16144 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c1e5eca5283..1198a673376 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -29501,6 +29501,8 @@ CREATE UNIQUE INDEX index_ci_instance_variables_on_key ON ci_instance_variables
CREATE INDEX index_ci_job_artifact_states_on_job_artifact_id ON ci_job_artifact_states USING btree (job_artifact_id);
+CREATE INDEX index_ci_job_artifacts_expire_at_unlocked_non_trace ON ci_job_artifacts USING btree (expire_at) WHERE ((locked = 0) AND (file_type <> 3) AND (expire_at IS NOT NULL));
+
CREATE INDEX index_ci_job_artifacts_for_terraform_reports ON ci_job_artifacts USING btree (project_id, id) WHERE (file_type = 18);
CREATE INDEX index_ci_job_artifacts_id_for_terraform_reports ON ci_job_artifacts USING btree (id) WHERE (file_type = 18);
diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md
index 926500090dc..1991d0b64cc 100644
--- a/doc/administration/dedicated/index.md
+++ b/doc/administration/dedicated/index.md
@@ -27,6 +27,7 @@ To request the creation of a new GitLab Dedicated environment for your organizat
- Desired instance subdomain: The main domain for GitLab Dedicated instances is `gitlab-dedicated.com`. You get to choose the subdomain name where your instance is accessible from (for example, `customer_name.gitlab-dedicated.com`).
- Initial storage: Initial storage size for your repositories in GB.
- Availability Zone IDs for PrivateLink: If you plan to later add a PrivateLink connection (either [inbound](#inbound-private-link) or [outbound](#outbound-private-link)) to your environment, and you require the connections to be available in specific Availability Zones, you must provide up to two [Availability Zone IDs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#az-ids) during onboarding. If not specified, GitLab will select two random Availability Zone IDs in which the connections will be available.
+- [KMS keys](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html) for encrypted AWS services (if you are using that functionality).
### Maintenance window
@@ -45,6 +46,111 @@ To change or update the configuration for your GitLab Dedicated instance, open a
The turnaround time for processing configuration change requests is [documented in the GitLab handbook](https://about.gitlab.com/handbook/engineering/infrastructure/team/gitlab-dedicated/#handling-configuration-changes-for-tenant-environments).
+### Encrypted Data At Rest (BYOK)
+
+If you want your GitLab data to be encrypted at rest, the KMS keys used must be accessible by GitLab services. KMS keys can be used in two modes for this purpose:
+
+1. Per-service KMS keys (Backup, EBS, RDS, S3), or
+1. One KMS key for all services.
+
+If you use a key per service, all services must be encrypted at rest. Selective enablement of this feature is not supported.
+
+The keys provided have to reside in the same primary and secondary region specified during [onboarding](#onboarding).
+
+For instructions on how to create and manage KMS keys, visit [Managing keys](https://docs.aws.amazon.com/kms/latest/developerguide/getting-started.html) in the AWS KMS documentation.
+
+To create a KMS key using the AWS Console:
+
+1. In `Configure key`, select:
+ 1. Key type: **Symmetrical**
+ 1. Key usage: **Encrypt and decrypt**
+ 1. `Advanced options`:
+ 1. Key material origin: **KMS**
+ 1. Regionality: **Multi-Region key**
+1. Enter your values for key alias, description, and tags.
+1. Select Key administrators (optionally allow or deny key administrators to delete the key).
+1. For Key usage permissions, add the GitLab AWS account using the **Other AWS accounts** dialog.
+
+The last page asks you to confirm the KMS key policy. It should look similar to the following example, populated with your account IDs and usernames:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Id": "byok-key-policy",
+ "Statement": [
+ {
+ "Sid": "Enable IAM User Permissions",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::<CUSTOMER-ACCOUNT-ID>:root"
+ },
+ "Action": "kms:*",
+ "Resource": "*"
+ },
+ {
+ "Sid": "Allow access for Key Administrators",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": [
+ "arn:aws:iam::<CUSTOMER-ACCOUNT-ID>:user/<CUSTOMER-USER>"
+ ]
+ },
+ "Action": [
+ "kms:Create*",
+ "kms:Describe*",
+ "kms:Enable*",
+ "kms:List*",
+ "kms:Put*",
+ "kms:Update*",
+ "kms:Revoke*",
+ "kms:Disable*",
+ "kms:Get*",
+ "kms:Delete*",
+ "kms:TagResource",
+ "kms:UntagResource",
+ "kms:ScheduleKeyDeletion",
+ "kms:CancelKeyDeletion",
+ "kms:ReplicateKey",
+ "kms:UpdatePrimaryRegion"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Sid": "Allow use of the key",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": [
+ "arn:aws:iam::<GITLAB-ACCOUNT-ID>:root"
+ ]
+ },
+ "Action": [
+ "kms:Encrypt",
+ "kms:Decrypt",
+ "kms:ReEncrypt*",
+ "kms:GenerateDataKey*",
+ "kms:DescribeKey"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Sid": "Allow attachment of persistent resources",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": [
+ "arn:aws:iam::<GITLAB-ACCOUNT-ID>:root"
+ ]
+ },
+ "Action": [
+ "kms:CreateGrant",
+ "kms:ListGrants",
+ "kms:RevokeGrant"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+```
+
### Inbound Private Link
[AWS Private Link](https://docs.aws.amazon.com/vpc/latest/privatelink/what-is-privatelink.html) allows users and applications in your VPC on AWS to securely connect to the GitLab Dedicated endpoint without network traffic going over the public internet.
diff --git a/doc/administration/get_started.md b/doc/administration/get_started.md
index 7a5c846bdbc..b11524083b1 100644
--- a/doc/administration/get_started.md
+++ b/doc/administration/get_started.md
@@ -1,7 +1,7 @@
---
-info: For assistance with this CSM Onboarding page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects.
+info: For assistance with this tutorial, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects.
stage: none
-group: unassigned
+group: Tutorials
---
# Get started administering GitLab **(FREE)**
diff --git a/doc/administration/package_information/supported_os.md b/doc/administration/package_information/supported_os.md
index da16a6c2012..f92d57c0035 100644
--- a/doc/administration/package_information/supported_os.md
+++ b/doc/administration/package_information/supported_os.md
@@ -56,6 +56,14 @@ although [new versions have been released](https://about.gitlab.com/releases/cat
of the [Linux package install guide](https://about.gitlab.com/install/#content).
Future GitLab upgrades are fetched according to your upgraded OS.
+## Update both GitLab and the operating system
+
+To upgrade both the operating system (OS) and GitLab:
+
+1. Upgrade the OS.
+1. Check if it's necessary to [update the GitLab package sources](#update-gitlab-package-sources-after-upgrading-the-os).
+1. [Upgrade GitLab](../../update/index.md).
+
## Packages for ARM64
> [Introduced](https://gitlab.com/gitlab-org/gitlab-omnibus-builder/-/issues/27) in GitLab 13.4.
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 1788d503a54..5a2a1678a08 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -1908,21 +1908,6 @@ The [`project_fingerprint`](https://gitlab.com/groups/gitlab-org/-/epics/2791) a
</div>
-<div class="deprecation removal-160 breaking-change">
-
-### REST API Runner maintainer_note
-
-Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>
-
-WARNING:
-This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
-Review the details carefully before upgrading.
-
-The `maintainer_note` argument in the `POST /runners` REST endpoint was deprecated in GitLab 14.8 and replaced with the `maintenance_note` argument.
-The `maintainer_note` argument will be removed in GitLab 16.0.
-
-</div>
-
<div class="deprecation removal-153">
### Vulnerability Report sort by Tool
diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb
index 66115baf120..311fcf9aba1 100644
--- a/lib/api/release/links.rb
+++ b/lib/api/release/links.rb
@@ -65,14 +65,14 @@ module API
end
route_setting :authentication, job_token_allowed: true
post 'links' do
- authorize! :create_release, release
-
result = ::Releases::Links::CreateService
.new(release, current_user, declared_params(include_missing: false))
.execute
if result.success?
present result.payload[:link], with: Entities::Releases::Link
+ elsif result.reason == ::Releases::Links::REASON_FORBIDDEN
+ forbidden!
else
render_api_error!(result.message, 400)
end
@@ -121,14 +121,14 @@ module API
end
route_setting :authentication, job_token_allowed: true
put do
- authorize! :update_release, release
-
result = ::Releases::Links::UpdateService
.new(release, current_user, declared_params(include_missing: false))
.execute(link)
if result.success?
present result.payload[:link], with: Entities::Releases::Link
+ elsif result.reason == ::Releases::Links::REASON_FORBIDDEN
+ forbidden!
else
render_api_error!(result.message, 400)
end
@@ -145,14 +145,14 @@ module API
end
route_setting :authentication, job_token_allowed: true
delete do
- authorize! :destroy_release, release
-
result = ::Releases::Links::DestroyService
.new(release, current_user)
.execute(link)
if result.success?
present result.payload[:link], with: Entities::Releases::Link
+ elsif result.reason == ::Releases::Links::REASON_FORBIDDEN
+ forbidden!
else
render_api_error!(result.message, 400)
end
diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb
index 33e7724bf9b..7ab7dc3d64e 100644
--- a/lib/gitlab/ci/config/external/file/component.rb
+++ b/lib/gitlab/ci/config/external/file/component.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def matching?
- super && ::Feature.enabled?(:ci_include_components, context.project)
+ super && ::Feature.enabled?(:ci_include_components, context.project&.root_namespace)
end
def content
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 271da2edbc9..c3a7b384172 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -367,12 +367,12 @@ module Gitlab
def foreign_key_exists?(source, target = nil, **options)
# This if block is necessary because foreign_key_exists? is called in down migrations that may execute before
- # the postgres_foreign_keys view had necessary columns added, or even before the view existed.
+ # the postgres_foreign_keys view had necessary columns added.
# In that case, we revert to the previous behavior of this method.
# The behavior in the if block has a bug: it always returns false if the fk being checked has multiple columns.
# This can be removed after init_schema.rb passes 20221122210711_add_columns_to_postgres_foreign_keys.rb
# Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/386796
- if ActiveRecord::Migrator.current_version < 20221122210711
+ unless connection.column_exists?('postgres_foreign_keys', 'constrained_table_name')
return foreign_keys(source).any? do |foreign_key|
tables_match?(target.to_s, foreign_key.to_table.to_s) &&
options_match?(foreign_key.options, options)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 84f7338a9ce..e8e8bb6275d 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -466,7 +466,7 @@ module Gitlab
# HTML comment line:
# <!-- some commented text -->
- ^<!--\ .*\ -->\ *$
+ ^<!--\ .*?\ -->\ *$
)
}mx.freeze
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f77f1a61811..6a8ed0085a3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25294,6 +25294,9 @@ msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost."
msgstr ""
+msgid "Leave feadback."
+msgstr ""
+
msgid "Leave group"
msgstr ""
@@ -48872,6 +48875,9 @@ msgstr ""
msgid "WorkItem|Add to milestone"
msgstr ""
+msgid "WorkItem|All activity"
+msgstr ""
+
msgid "WorkItem|Are you sure you want to cancel editing?"
msgstr ""
@@ -48895,6 +48901,9 @@ msgstr ""
msgid "WorkItem|Closed"
msgstr ""
+msgid "WorkItem|Comments only"
+msgstr ""
+
msgid "WorkItem|Convert to task"
msgstr ""
@@ -48928,6 +48937,9 @@ msgstr ""
msgid "WorkItem|Health status"
msgstr ""
+msgid "WorkItem|History only"
+msgstr ""
+
msgid "WorkItem|Incident"
msgstr ""
@@ -49087,6 +49099,9 @@ msgstr ""
msgid "WorkItem|Work item not found"
msgstr ""
+msgid "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options."
+msgstr ""
+
msgid "Would you like to create a new branch?"
msgstr ""
diff --git a/package.json b/package.json
index b259228d18f..2bf5ac780d9 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
- "@gitlab/svgs": "3.22.0",
+ "@gitlab/svgs": "3.23.0",
"@gitlab/ui": "56.2.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230223005157",
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 451f3d56af7..3247acedaa5 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -15,6 +15,7 @@ ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
RSpec.configure(&:disable_monkey_patching!)
require 'active_support/all'
+require 'pry'
require_relative 'rails_autoload'
require_relative '../config/settings'
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
index 790b082c10b..44c242fa2cb 100644
--- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
@@ -74,7 +74,14 @@ describe('JobArtifactsTable component', () => {
];
}
const getJobArtifactsResponseThatPaginates = {
- data: { project: { jobs: { nodes: enoughJobsToPaginate } } },
+ data: {
+ project: {
+ jobs: {
+ nodes: enoughJobsToPaginate,
+ pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo, hasNextPage: true },
+ },
+ },
+ },
};
const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
@@ -316,7 +323,7 @@ describe('JobArtifactsTable component', () => {
});
describe('pagination', () => {
- const { pageInfo } = getJobArtifactsResponse.data.project.jobs;
+ const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs;
const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates);
beforeEach(async () => {
@@ -324,10 +331,7 @@ describe('JobArtifactsTable component', () => {
{
getJobArtifactsQuery: query,
},
- {
- count: enoughJobsToPaginate.length,
- pageInfo,
- },
+ { pageInfo },
);
await waitForPromises();
diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js
index eb4bcbf942b..86c4ad9b361 100644
--- a/spec/frontend/work_items/components/notes/activity_filter_spec.js
+++ b/spec/frontend/work_items/components/notes/activity_filter_spec.js
@@ -1,25 +1,33 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { ASC, DESC } from '~/notes/constants';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ TRACKING_CATEGORY_SHOW,
+} from '~/work_items/constants';
import { mockTracking } from 'helpers/tracking_helper';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-describe('Activity Filter', () => {
+describe('Work Item Activity/Discussions Filtering', () => {
let wrapper;
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findNewestFirstItem = () => wrapper.findByTestId('js-newest-first');
+ const findOnlyCommentsItem = () => wrapper.findByTestId('comments-activity');
+ const findOnlyHistoryItem = () => wrapper.findByTestId('history-activity');
- const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => {
+ const createComponent = ({
+ discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ loading = false,
+ workItemType = 'Task',
+ } = {}) => {
wrapper = shallowMountExtended(ActivityFilter, {
propsData: {
- sortOrder,
+ discussionFilter,
loading,
workItemType,
},
@@ -30,45 +38,46 @@ describe('Activity Filter', () => {
createComponent();
});
- describe('default', () => {
- it('has a dropdown with 2 options', () => {
+ describe('Default', () => {
+ it('has a dropdown with 3 options', () => {
expect(findDropdown().exists()).toBe(true);
- expect(findAllDropdownItems()).toHaveLength(ActivityFilter.SORT_OPTIONS.length);
+ expect(findAllDropdownItems()).toHaveLength(ActivityFilter.filterOptions.length);
});
it('has local storage sync with the correct props', () => {
expect(findLocalStorageSync().props('asString')).toBe(true);
});
- it('emits `updateSavedSortOrder` event when update is emitted', async () => {
- findLocalStorageSync().vm.$emit('input', ASC);
+ it('emits `changeFilter` event when local storage input is emitted', () => {
+ findLocalStorageSync().vm.$emit('input', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
- await nextTick();
- expect(wrapper.emitted('updateSavedSortOrder')).toHaveLength(1);
- expect(wrapper.emitted('updateSavedSortOrder')).toEqual([[ASC]]);
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]);
});
});
- describe('when asc', () => {
- describe('when the dropdown is clicked', () => {
- it('calls the right actions', async () => {
+ describe('Changing filter value', () => {
+ it.each`
+ dropdownLabel | filterValue | dropdownItem
+ ${'Comments only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${findOnlyCommentsItem}
+ ${'History only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_HISTORY} | ${findOnlyHistoryItem}
+ `(
+ 'when `$dropdownLabel` is clicked it emits `$filterValue` with tracking info',
+ ({ dropdownItem, filterValue }) => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- findNewestFirstItem().vm.$emit('click');
- await nextTick();
+ dropdownItem().vm.$emit('click');
- expect(wrapper.emitted('changeSortOrder')).toHaveLength(1);
- expect(wrapper.emitted('changeSortOrder')).toEqual([[DESC]]);
+ expect(wrapper.emitted('changeFilter')).toEqual([[filterValue]]);
expect(trackingSpy).toHaveBeenCalledWith(
TRACKING_CATEGORY_SHOW,
- 'notes_sort_order_changed',
+ 'work_item_notes_filter_changed',
{
category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_sorting',
+ label: 'item_track_notes_filtering',
property: 'type_Task',
},
);
- });
- });
+ },
+ );
});
});
diff --git a/spec/frontend/work_items/components/notes/activity_sort_spec.js b/spec/frontend/work_items/components/notes/activity_sort_spec.js
new file mode 100644
index 00000000000..289823dc59e
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/activity_sort_spec.js
@@ -0,0 +1,69 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ActivitySort from '~/work_items/components/notes/activity_sort.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { ASC, DESC } from '~/notes/constants';
+
+import { mockTracking } from 'helpers/tracking_helper';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+
+describe('Work Item Activity Sorting', () => {
+ let wrapper;
+
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findNewestFirstItem = () => wrapper.findByTestId('newest-first');
+
+ const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => {
+ wrapper = shallowMountExtended(ActivitySort, {
+ propsData: {
+ sortOrder,
+ loading,
+ workItemType,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('default', () => {
+ it('has a dropdown with 2 options', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findAllDropdownItems()).toHaveLength(ActivitySort.sortOptions.length);
+ });
+
+ it('has local storage sync with the correct props', () => {
+ expect(findLocalStorageSync().props('asString')).toBe(true);
+ });
+
+ it('emits `changeSort` event when update is emitted', () => {
+ findLocalStorageSync().vm.$emit('input', ASC);
+
+ expect(wrapper.emitted('changeSort')).toEqual([[ASC]]);
+ });
+ });
+
+ describe('when asc', () => {
+ describe('when the dropdown is clicked', () => {
+ it('calls the right actions', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ findNewestFirstItem().vm.$emit('click');
+
+ expect(wrapper.emitted('changeSort')).toEqual([[DESC]]);
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ TRACKING_CATEGORY_SHOW,
+ 'work_item_notes_sort_order_changed',
+ {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_track_notes_sorting',
+ property: 'type_Task',
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js
new file mode 100644
index 00000000000..339efad0608
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js
@@ -0,0 +1,44 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+} from '~/work_items/constants';
+
+describe('Work Item History Filter note', () => {
+ let wrapper;
+
+ const findShowAllActivityButton = () => wrapper.findByTestId('show-all-activity');
+ const findShowCommentsButton = () => wrapper.findByTestId('show-comments-only');
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(WorkItemHistoryOnlyFilterNote, {
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('timelineContent renders a string containing instruction for switching feed type', () => {
+ expect(wrapper.text()).toContain(
+ "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.",
+ );
+ });
+
+ it('emits `changeFilter` event with 0 parameter on clicking Show all activity button', () => {
+ findShowAllActivityButton().vm.$emit('click');
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ALL_NOTES]]);
+ });
+
+ it('emits `changeFilter` event with 1 parameter on clicking Show comments only button', () => {
+ findShowCommentsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
new file mode 100644
index 00000000000..3b87a5e3e88
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
@@ -0,0 +1,65 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
+import ActivitySort from '~/work_items/components/notes/activity_sort.vue';
+import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
+import { ASC } from '~/notes/constants';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+} from '~/work_items/constants';
+
+describe('Work Item Note Activity Header', () => {
+ let wrapper;
+
+ const findActivityLabelHeading = () => wrapper.find('h3');
+ const findActivityFilterDropdown = () => wrapper.findComponent(ActivityFilter);
+ const findActivitySortDropdown = () => wrapper.findComponent(ActivitySort);
+
+ const createComponent = ({
+ disableActivityFilterSort = false,
+ sortOrder = ASC,
+ workItemType = 'Task',
+ discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ } = {}) => {
+ wrapper = shallowMount(WorkItemNotesActivityHeader, {
+ propsData: {
+ disableActivityFilterSort,
+ sortOrder,
+ workItemType,
+ discussionFilter,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Should have the Activity label', () => {
+ expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel);
+ });
+
+ it('Should have Activity filtering dropdown', () => {
+ expect(findActivityFilterDropdown().exists()).toBe(true);
+ });
+
+ it('Should have Activity sorting dropdown', () => {
+ expect(findActivitySortDropdown().exists()).toBe(true);
+ });
+
+ describe('Activity Filter', () => {
+ it('emits `changeFilter` when filtering discussions', () => {
+ findActivityFilterDropdown().vm.$emit('changeFilter', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]);
+ });
+ });
+
+ describe('Activity Sorting', () => {
+ it('emits `changeSort` when sorting discussions/activity', () => {
+ findActivitySortDropdown().vm.$emit('changeSort', ASC);
+
+ expect(wrapper.emitted('changeSort')).toEqual([[ASC]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index 157e00d3eac..09d0022fa1e 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -9,7 +9,7 @@ import SystemNote from '~/work_items/components/notes/system_note.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
+import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
@@ -59,10 +59,9 @@ describe('WorkItemNotes component', () => {
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
const findAllListItems = () => wrapper.findAll('ul.timeline > *');
- const findActivityLabel = () => wrapper.find('label');
const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
+ const findActivityHeader = () => wrapper.findComponent(WorkItemNotesActivityHeader);
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
@@ -129,8 +128,8 @@ describe('WorkItemNotes component', () => {
createComponent();
});
- it('renders activity label', () => {
- expect(findActivityLabel().exists()).toBe(true);
+ it('has the work item note activity header', () => {
+ expect(findActivityHeader().exists()).toBe(true);
});
it('passes correct props to comment form component', async () => {
@@ -221,26 +220,22 @@ describe('WorkItemNotes component', () => {
await waitForPromises();
});
- it('filter exists', () => {
- expect(findSortingFilter().exists()).toBe(true);
- });
-
- it('sorts the list when the `changeSortOrder` event is emitted', async () => {
+ it('sorts the list when the `changeSort` event is emitted', async () => {
expect(findSystemNoteAtIndex(0).props('note').id).toEqual(firstSystemNodeId);
- await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+ await findActivityHeader().vm.$emit('changeSort', DESC);
expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId);
});
it('puts form at start of list in when sorting by newest first', async () => {
- await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+ await findActivityHeader().vm.$emit('changeSort', DESC);
expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true);
});
it('puts form at end of list in when sorting by oldest first', async () => {
- await findSortingFilter().vm.$emit('changeSortOrder', ASC);
+ await findActivityHeader().vm.$emit('changeSort', ASC);
expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true);
});
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index 811b7a3490c..e5a39f6a24e 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -6,188 +6,135 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
describe '#ide_data' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.creator }
+ let_it_be(:fork_info) { { ide_path: '/test/ide/path' } }
+
+ let_it_be(:params) do
+ {
+ branch: 'master',
+ path: 'foo/bar',
+ merge_request_id: '1'
+ }
+ end
+
+ let(:base_data) do
+ {
+ 'can-use-new-web-ide' => 'false',
+ 'use-new-web-ide' => 'false',
+ 'user-preferences-path' => profile_preferences_path,
+ 'project' => nil,
+ 'preview-markdown-path' => nil
+ }
+ end
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:content_security_policy_nonce).and_return('test-csp-nonce')
end
- context 'with vscode_web_ide=true and instance vars set' do
+ it 'returns hash' do
+ expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
+ .to include(base_data)
+ end
+
+ context 'with project' do
+ it 'returns hash with parameters' do
+ serialized_project = API::Entities::Project.represent(project, current_user: user).to_json
+
+ expect(
+ helper.ide_data(project: project, fork_info: nil, params: params)
+ ).to include(base_data.merge(
+ 'fork-info' => nil,
+ 'branch-name' => params[:branch],
+ 'file-path' => params[:path],
+ 'merge-request' => params[:merge_request_id],
+ 'project' => serialized_project,
+ 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
+ ))
+ end
+
+ context 'with fork info' do
+ it 'returns hash with fork info' do
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('fork-info' => fork_info.to_json)
+ end
+ end
+ end
+
+ context 'with environments guidance experiment', :experiment do
before do
- stub_feature_flags(vscode_web_ide: true)
+ stub_experiments(in_product_guidance_environments_webide: :candidate)
end
- it 'returns hash' do
- expect(
- helper.ide_data(
- project: project,
- branch: 'master',
- path: 'foo/README.md',
- merge_request: '7',
- fork_info: nil,
- learn_gitlab_source: nil
- )
- ).to match(
+ context 'when project has no enviornments' do
+ it 'enables environment guidance' do
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('enable-environments-guidance' => 'true')
+ end
+
+ context 'and the callout has been dismissed' do
+ it 'disables environment guidance' do
+ callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: user)
+ callout.update!(dismissed_at: Time.now - 1.week)
+ allow(helper).to receive(:current_user).and_return(User.find(user.id))
+
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('enable-environments-guidance' => 'false')
+ end
+ end
+ end
+
+ context 'when the project has environments' do
+ it 'disables environment guidance' do
+ create(:environment, project: project)
+
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('enable-environments-guidance' => 'false')
+ end
+ end
+ end
+
+ context 'with vscode_web_ide=true' do
+ let(:base_data) do
+ {
'can-use-new-web-ide' => 'true',
'use-new-web-ide' => 'true',
'user-preferences-path' => profile_preferences_path,
'new-web-ide-help-page-path' =>
help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
- 'branch-name' => 'master',
- 'project-path' => project.path_with_namespace,
'csp-nonce' => 'test-csp-nonce',
'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'),
- 'file-path' => 'foo/README.md',
'editor-font-family' => 'JetBrains Mono',
'editor-font-format' => 'woff2',
- 'editor-font-src-url' => a_string_matching(%r{jetbrains-mono/JetBrainsMono}),
- 'merge-request' => '7',
- 'fork-info' => nil,
- 'learn-gitlab-source' => 'false'
- )
+ 'editor-font-src-url' => a_string_matching(%r{jetbrains-mono/JetBrainsMono})
+ }
end
- it 'does not use new web ide if user.use_legacy_web_ide' do
- allow(user).to receive(:use_legacy_web_ide).and_return(true)
-
- expect(
- helper.ide_data(
- project: project,
- branch: nil,
- path: nil,
- merge_request: nil,
- fork_info: nil,
- learn_gitlab_source: nil
- )
- ).to include('use-new-web-ide' => 'false')
- end
-
- it 'returns source data in the hash if learn gitlab source' do
- allow(user).to receive(:use_legacy_web_ide).and_return(true)
-
- expect(
- helper.ide_data(
- project: project,
- branch: nil,
- path: nil,
- merge_request: nil,
- fork_info: nil,
- learn_gitlab_source: true
- )
- ).to include('learn-gitlab-source' => 'true')
- end
- end
-
- context 'with vscode_web_ide=false' do
before do
- stub_feature_flags(vscode_web_ide: false)
+ stub_feature_flags(vscode_web_ide: true)
end
- context 'when instance vars and parameters are not set' do
- it 'returns instance data in the hash as nil' do
- expect(
- helper.ide_data(
- project: nil,
- branch: nil,
- path: nil,
- merge_request: nil,
- fork_info: nil,
- learn_gitlab_source: nil
- )
- ).to include(
- 'can-use-new-web-ide' => 'false',
- 'use-new-web-ide' => 'false',
- 'user-preferences-path' => profile_preferences_path,
- 'branch-name' => nil,
- 'file-path' => nil,
- 'merge-request' => nil,
- 'fork-info' => nil,
- 'project' => nil,
- 'preview-markdown-path' => nil
- )
- end
+ it 'returns hash' do
+ expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
+ .to include(base_data)
end
- context 'when instance vars are set' do
- it 'returns instance data in the hash' do
- fork_info = { ide_path: '/test/ide/path' }
-
- serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
+ it 'does not use new web ide if user.use_legacy_web_ide' do
+ allow(user).to receive(:use_legacy_web_ide).and_return(true)
- expect(
- helper.ide_data(
- project: project,
- branch: 'master',
- path: 'foo/bar',
- merge_request: '1',
- fork_info: fork_info,
- learn_gitlab_source: nil
- )
- ).to include(
- 'branch-name' => 'master',
- 'file-path' => 'foo/bar',
- 'merge-request' => '1',
- 'fork-info' => fork_info.to_json,
- 'project' => serialized_project,
- 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
- )
- end
+ expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
+ .to include('use-new-web-ide' => 'false')
end
- context 'environments guidance experiment', :experiment do
- before do
- stub_experiments(in_product_guidance_environments_webide: :candidate)
- end
-
- context 'when project has no enviornments' do
- it 'enables environment guidance' do
- expect(
- helper.ide_data(
- project: project,
- branch: nil,
- path: nil,
- merge_request: nil,
- fork_info: nil,
- learn_gitlab_source: nil
- )
- ).to include('enable-environments-guidance' => 'true')
- end
-
- context 'and the callout has been dismissed' do
- it 'disables environment guidance' do
- callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
- callout.update!(dismissed_at: Time.now - 1.week)
- allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
-
- expect(
- helper.ide_data(
- project: project,
- branch: nil,
- path: nil,
- merge_request: nil,
- fork_info: nil,
- learn_gitlab_source: nil
- )
- ).to include('enable-environments-guidance' => 'false')
- end
- end
- end
-
- context 'when the project has environments' do
- it 'disables environment guidance' do
- create(:environment, project: project)
-
- expect(
- helper.ide_data(
- project: project,
- branch: nil,
- path: nil,
- merge_request: nil,
- fork_info: nil,
- learn_gitlab_source: nil
- )
- ).to include('enable-environments-guidance' => 'false')
- end
+ context 'with project' do
+ it 'returns hash with parameters' do
+ expect(
+ helper.ide_data(project: project, fork_info: nil, params: params)
+ ).to include(base_data.merge(
+ 'branch-name' => params[:branch],
+ 'file-path' => params[:path],
+ 'merge-request' => params[:merge_request_id],
+ 'fork-info' => nil
+ ))
end
end
end
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 174a5a668a8..5ae057dc97d 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -11,11 +11,10 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
let(:with_can_create_project) { false }
let(:with_can_create_group) { false }
let(:with_can_create_snippet) { false }
- let(:with_context) { true }
let(:title) { 'Create new...' }
subject(:view_model) do
- helper.new_dropdown_view_model(project: current_project, group: current_group, with_context: with_context)
+ helper.new_dropdown_view_model(project: current_project, group: current_group)
end
before do
@@ -157,26 +156,12 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
it 'has base results' do
results = {
title: title,
- menu_sections: [],
- context: group
+ menu_sections: []
}
expect(view_model).to eq(results)
end
- context 'without context' do
- let(:with_context) { false }
-
- it 'has base results' do
- results = {
- title: title,
- menu_sections: []
- }
-
- expect(view_model).to eq(results)
- end
- end
-
context 'when can create projects in group' do
let(:with_can_create_projects_in_group) { true }
@@ -247,29 +232,15 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
allow(helper).to receive(:can_admin_project_member?) { with_can_admin_project_member }
end
- it 'has base results with context' do
+ it 'has base results' do
results = {
title: title,
- menu_sections: [],
- context: project
+ menu_sections: []
}
expect(view_model).to eq(results)
end
- context 'without context' do
- let(:with_context) { false }
-
- it 'has base results without context' do
- results = {
- title: title,
- menu_sections: []
- }
-
- expect(view_model).to eq(results)
- end
- end
-
context 'with show_new_issue_link?' do
let(:with_show_new_issue_link) { true }
@@ -375,20 +346,11 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
)
results = {
title: title,
- menu_sections: project_section,
- context: project
+ menu_sections: project_section
}
expect(view_model).to eq(results)
end
-
- context 'without context' do
- let(:with_context) { false }
-
- it 'does not include context' do
- expect(view_model.keys).to match_array([:title, :menu_sections])
- end
- end
end
def expected_menu_section(title:, menu_item:)
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 19a6735e439..8feae58cdc7 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -1144,7 +1144,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
context 'HTML comment lines' do
subject { described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX }
- let(:expected) { %(<!-- an HTML comment -->) }
+ let(:expected) { [['<!-- an HTML comment -->'], ['<!-- another HTML comment -->']] }
let(:markdown) do
<<~MARKDOWN
Regular text
@@ -1152,13 +1152,15 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
<!-- an HTML comment -->
more text
+
+ <!-- another HTML comment -->
MARKDOWN
end
it { is_expected.to match(%(<!-- single line comment -->)) }
it { is_expected.not_to match(%(<!--\nblock comment\n-->)) }
it { is_expected.not_to match(%(must start in first column <!-- comment -->)) }
- it { expect(subject.match(markdown)[:html_comment_line]).to eq expected }
+ it { expect(markdown.scan(subject)).to eq expected }
end
context 'HTML comment blocks' do
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index e94445f17cd..58b654ed65e 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -243,6 +243,29 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
end
end
+ describe '.non_trace' do
+ subject { described_class.non_trace }
+
+ context 'when there is only a trace job artifact' do
+ let!(:trace) { create(:ci_job_artifact, :trace) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when there is only a non-trace job artifact' do
+ let!(:junit) { create(:ci_job_artifact, :junit) }
+
+ it { is_expected.to eq([junit]) }
+ end
+
+ context 'when there are both trace and non-trace job artifacts' do
+ let!(:trace) { create(:ci_job_artifact, :trace) }
+ let!(:junit) { create(:ci_job_artifact, :junit) }
+
+ it { is_expected.to eq([junit]) }
+ end
+ end
+
describe '.downloadable' do
subject { described_class.downloadable }
diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb
index 0ad29454ff3..14f346f353b 100644
--- a/spec/models/concerns/taskable_spec.rb
+++ b/spec/models/concerns/taskable_spec.rb
@@ -46,6 +46,22 @@ RSpec.describe Taskable, feature_category: :team_planning do
subject { described_class.get_tasks(description) }
it { is_expected.to match(expected_result) }
+
+ describe 'with single line comments' do
+ let(:description) do
+ <<~MARKDOWN
+ <!-- line comment -->
+
+ - [ ] only task item
+
+ <!-- another line comment -->
+ MARKDOWN
+ end
+
+ let(:expected_result) { [TaskList::Item.new('- [ ]', 'only task item')] }
+
+ it { is_expected.to match(expected_result) }
+ end
end
describe '#task_list_items' do
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index 31a53949f2f..38708399519 100644
--- a/spec/requests/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -19,7 +19,6 @@ RSpec.describe IdeController, feature_category: :web_ide do
let_it_be(:top_nav_partial) { 'layouts/header/_default' }
let(:user) { creator }
- let(:branch) { '' }
def find_csp_frame_src
csp = response.headers['Content-Security-Policy']
@@ -42,14 +41,14 @@ RSpec.describe IdeController, feature_category: :web_ide do
subject { get route }
shared_examples 'user access rights check' do
- context 'user can read project' do
+ context 'when user can read project' do
it 'increases the views counter' do
expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
subject
end
- context 'user can read project but cannot push code' do
+ context 'when user can read project but cannot push code' do
include ProjectForksHelper
let(:user) { reporter }
@@ -60,7 +59,15 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) })
+
+ expect(assigns(:fork_info)).to eq({
+ fork_path: controller.helpers.ide_fork_and_edit_path(
+ project,
+ '',
+ '',
+ with_notice: false
+ )
+ })
end
it 'has nil fork_info if user cannot fork' do
@@ -81,13 +88,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') })
+ expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, '', '') })
end
end
end
end
- context 'user cannot read project' do
+ context 'when user cannot read project' do
let(:user) { other_user }
it 'returns 404' do
@@ -98,7 +105,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide' do
+ context 'with /-/ide' do
let(:route) { '/-/ide' }
it 'returns 404' do
@@ -108,7 +115,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide/project' do
+ context 'with /-/ide/project' do
let(:route) { '/-/ide/project' }
it 'returns 404' do
@@ -118,7 +125,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide/project/:project' do
+ context 'with /-/ide/project/:project' do
let(:route) { "/-/ide/project/#{project.full_path}" }
it 'instantiates project instance var and returns 200' do
@@ -126,33 +133,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
- expect(assigns(:learn_gitlab_source)).to be_nil
end
it_behaves_like 'user access rights check'
- context "/-/ide/project/:project?learn_gitlab_source=true" do
- let(:route) { "/-/ide/project/#{project.full_path}?learn_gitlab_source=true" }
-
- it 'instantiates project instance var and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- expect(assigns(:learn_gitlab_source)).to eq 'true'
- end
- end
-
- %w(edit blob tree).each do |action|
- context "/-/ide/project/:project/#{action}" do
+ %w[edit blob tree].each do |action|
+ context "with /-/ide/project/:project/#{action}" do
let(:route) { "/-/ide/project/#{project.full_path}/#{action}" }
it 'instantiates project instance var and returns 200' do
@@ -160,94 +147,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
- expect(assigns(:learn_gitlab_source)).to be_nil
end
it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch" do
- let(:branch) { 'master' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}" }
-
- it 'instantiates project and branch instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- expect(assigns(:learn_gitlab_source)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch/-" do
- let(:branch) { 'branch/slash' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-" }
-
- it 'instantiates project and branch instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- expect(assigns(:learn_gitlab_source)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch/-/:path" do
- let(:branch) { 'master' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-/foo/.bar" }
-
- it 'instantiates project, branch, and path instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to eq 'foo/.bar'
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- expect(assigns(:learn_gitlab_source)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
- end
- end
- end
end
end
- context '/-/ide/project/:project/merge_requests/:merge_request_id' do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
-
- let(:route) { "/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.id}" }
-
- it 'instantiates project and merge_request instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to eq merge_request.id.to_s
- expect(assigns(:fork_info)).to be_nil
- expect(assigns(:learn_gitlab_source)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
- end
-
describe 'Snowplow view event', :snowplow do
it 'is tracked' do
subject
diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
index 07e76f65232..d1ec2a1d3a6 100644
--- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
@@ -231,6 +231,16 @@ feature_category: :build_artifacts do
end
end
+ context 'when some artifacts are trace' do
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
+ let!(:trace_artifact) { create(:ci_job_artifact, :trace, :expired, job: job, locked: job.pipeline.locked) }
+
+ it 'destroys only non trace artifacts' do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
+ expect(trace_artifact).to be_persisted
+ end
+ end
+
context 'when all artifacts are locked' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
diff --git a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb
index 0e27f79487c..f4839ccb04b 100644
--- a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb
@@ -7,19 +7,32 @@ RSpec.describe Ci::JobArtifacts::DestroyAssociationsService, feature_category: :
let_it_be(:project_2) { create(:project) }
let_it_be(:artifact_1, refind: true) { create(:ci_job_artifact, :zip, project: project_1) }
- let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :zip, project: project_2) }
- let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :zip, project: project_1) }
+ let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :junit, project: project_2) }
+ let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :terraform, project: project_1) }
+ let_it_be(:artifact_4, refind: true) { create(:ci_job_artifact, :trace, project: project_2) }
+ let_it_be(:artifact_5, refind: true) { create(:ci_job_artifact, :metadata, project: project_2) }
- let(:artifacts) { Ci::JobArtifact.where(id: [artifact_1.id, artifact_2.id, artifact_3.id]) }
+ let_it_be(:locked_artifact, refind: true) { create(:ci_job_artifact, :zip, :locked, project: project_1) }
+
+ let(:artifact_ids_to_be_removed) { [artifact_1.id, artifact_2.id, artifact_3.id, artifact_4.id, artifact_5.id] }
+ let(:artifacts) { Ci::JobArtifact.where(id: artifact_ids_to_be_removed) }
let(:service) { described_class.new(artifacts) }
describe '#destroy_records' do
- it 'removes artifacts without updating statistics' do
+ it 'removes all types of artifacts without updating statistics' do
expect_next_instance_of(Ci::JobArtifacts::DestroyBatchService) do |service|
expect(service).to receive(:execute).with(update_stats: false).and_call_original
end
- expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-3)
+ expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count)
+ end
+
+ context 'with a locked artifact' do
+ let(:artifact_ids_to_be_removed) { [artifact_1.id, locked_artifact.id] }
+
+ it 'removes all artifacts' do
+ expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count)
+ end
end
context 'when there are no artifacts' do
@@ -42,7 +55,11 @@ RSpec.describe Ci::JobArtifacts::DestroyAssociationsService, feature_category: :
have_attributes(amount: -artifact_1.size, ref: artifact_1.id),
have_attributes(amount: -artifact_3.size, ref: artifact_3.id)
]
- project2_increments = [have_attributes(amount: -artifact_2.size, ref: artifact_2.id)]
+ project2_increments = [
+ have_attributes(amount: -artifact_2.size, ref: artifact_2.id),
+ have_attributes(amount: -artifact_4.size, ref: artifact_4.id),
+ have_attributes(amount: -artifact_5.size, ref: artifact_5.id)
+ ]
expect(ProjectStatistics).to receive(:bulk_increment_statistic).once
.with(project_1, :build_artifacts_size, match_array(project1_increments))
diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
index ed3e3e6766f..6f9dcf47535 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_artifacts do
- let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id, trace_artifact.id]) }
+ let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id]) }
let(:skip_projects_on_refresh) { false }
let(:service) do
described_class.new(
@@ -25,34 +25,9 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_a
create(:ci_job_artifact)
end
- let_it_be(:trace_artifact, refind: true) do
- create(:ci_job_artifact, :trace, :expired)
- end
-
describe '#execute' do
subject(:execute) { service.execute }
- context 'with skip_trace_artifacts false' do
- let(:service) do
- described_class.new(
- artifacts,
- pick_up_at: Time.current,
- skip_projects_on_refresh: skip_projects_on_refresh,
- skip_trace_artifacts: false
- )
- end
-
- subject(:execute) { service.execute }
-
- it 'deletes trace artifacts' do
- expect { subject }
- .to change { Ci::JobArtifact.exists?(trace_artifact.id) }.from(true).to(false)
-
- expected_destroyed_ids = [artifact_with_file.id, artifact_without_file.id, trace_artifact.id]
- is_expected.to include(destroyed_artifacts_count: 3, destroyed_ids: expected_destroyed_ids)
- end
- end
-
it 'creates a deleted object for artifact with attached file' do
expect { subject }.to change { Ci::DeletedObject.count }.by(1)
end
@@ -81,11 +56,6 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_a
execute
end
- it 'preserves trace artifacts' do
- expect { subject }
- .to not_change { Ci::JobArtifact.exists?(trace_artifact.id) }
- end
-
context 'when artifact belongs to a project that is undergoing stats refresh' do
let!(:artifact_under_refresh_1) do
create(:ci_job_artifact, :zip)
diff --git a/spec/services/releases/links/create_service_spec.rb b/spec/services/releases/links/create_service_spec.rb
index aa154647509..9928d2162d7 100644
--- a/spec/services/releases/links/create_service_spec.rb
+++ b/spec/services/releases/links/create_service_spec.rb
@@ -44,6 +44,7 @@ RSpec.describe Releases::Links::CreateService, feature_category: :release_orches
is_expected.to be_error
expect(execute.message).to include('Access Denied')
+ expect(execute.reason).to eq(:forbidden)
end
end
@@ -55,6 +56,7 @@ RSpec.describe Releases::Links::CreateService, feature_category: :release_orches
is_expected.to be_error
expect(execute.message[0]).to include('Url is blocked')
+ expect(execute.reason).to eq(:bad_request)
end
end
diff --git a/spec/services/releases/links/destroy_service_spec.rb b/spec/services/releases/links/destroy_service_spec.rb
index fed98a62aa7..a248932eada 100644
--- a/spec/services/releases/links/destroy_service_spec.rb
+++ b/spec/services/releases/links/destroy_service_spec.rb
@@ -40,6 +40,7 @@ RSpec.describe Releases::Links::DestroyService, feature_category: :release_orche
is_expected.to be_error
expect(execute.message).to include('Access Denied')
+ expect(execute.reason).to eq(:forbidden)
end
end
@@ -51,6 +52,7 @@ RSpec.describe Releases::Links::DestroyService, feature_category: :release_orche
is_expected.to be_error
expect(execute.message).to eq('Link does not exist')
+ expect(execute.reason).to eq(:not_found)
end
end
@@ -63,6 +65,7 @@ RSpec.describe Releases::Links::DestroyService, feature_category: :release_orche
expect { execute }.not_to change { release.links.count }
is_expected.to be_error
+ expect(execute.reason).to eq(:bad_request)
end
end
end
diff --git a/spec/services/releases/links/update_service_spec.rb b/spec/services/releases/links/update_service_spec.rb
index 40756c7eced..3f48985cf60 100644
--- a/spec/services/releases/links/update_service_spec.rb
+++ b/spec/services/releases/links/update_service_spec.rb
@@ -51,6 +51,7 @@ RSpec.describe Releases::Links::UpdateService, feature_category: :release_orches
it 'returns an error' do
is_expected.to be_error
expect(execute.message).to include('Access Denied')
+ expect(execute.reason).to eq(:forbidden)
end
end
@@ -60,6 +61,7 @@ RSpec.describe Releases::Links::UpdateService, feature_category: :release_orches
it 'returns an error' do
is_expected.to be_error
expect(execute.message[0]).to include('Url is blocked')
+ expect(execute.reason).to eq(:bad_request)
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 2e3014b2f51..039459e6cd2 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -5138,7 +5138,6 @@
- './spec/helpers/groups/settings_helper_spec.rb'
- './spec/helpers/hooks_helper_spec.rb'
- './spec/helpers/icons_helper_spec.rb'
-- './spec/helpers/ide_helper_spec.rb'
- './spec/helpers/import_helper_spec.rb'
- './spec/helpers/instance_configuration_helper_spec.rb'
- './spec/helpers/integrations_helper_spec.rb'
@@ -8951,7 +8950,6 @@
- './spec/requests/groups/settings/access_tokens_controller_spec.rb'
- './spec/requests/groups/settings/applications_controller_spec.rb'
- './spec/requests/health_controller_spec.rb'
-- './spec/requests/ide_controller_spec.rb'
- './spec/requests/import/gitlab_groups_controller_spec.rb'
- './spec/requests/import/gitlab_projects_controller_spec.rb'
- './spec/requests/import/url_controller_spec.rb'
diff --git a/spec/views/layouts/group.html.haml_spec.rb b/spec/views/layouts/group.html.haml_spec.rb
new file mode 100644
index 00000000000..0b8f735a1d6
--- /dev/null
+++ b/spec/views/layouts/group.html.haml_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/group', feature_category: :subgroups do
+ let_it_be(:group) { create(:group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let(:invite_member) { true }
+
+ before do
+ allow(view).to receive(:can_admin_group_member?).and_return(invite_member)
+ assign(:group, group)
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(build_stubbed(:user)))
+ end
+
+ subject do
+ render
+
+ rendered
+ end
+
+ context 'with ability to invite members' do
+ it { is_expected.to have_selector('.js-invite-members-modal') }
+ end
+
+ context 'without ability to invite members' do
+ let(:invite_member) { false }
+
+ it { is_expected.not_to have_selector('.js-invite-members-modal') }
+ end
+end
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index a547c1be2f4..2c5882fce3d 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -8,14 +8,12 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
shared_examples_for 'invite member selector' do
context 'with ability to invite members' do
it { is_expected.to have_selector('.js-invite-members-trigger') }
- it { is_expected.to have_selector('.js-invite-members-modal') }
end
context 'without ability to invite members' do
let(:invite_member) { false }
it { is_expected.not_to have_selector('.js-invite-members-trigger') }
- it { is_expected.not_to have_selector('.js-invite-members-modal') }
end
end
diff --git a/spec/views/layouts/project.html.haml_spec.rb b/spec/views/layouts/project.html.haml_spec.rb
new file mode 100644
index 00000000000..588828f7bd6
--- /dev/null
+++ b/spec/views/layouts/project.html.haml_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/project', feature_category: :projects do
+ let(:invite_member) { true }
+
+ before do
+ allow(view).to receive(:can_admin_project_member?).and_return(invite_member)
+ assign(:project, build_stubbed(:project))
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(build_stubbed(:user)))
+ end
+
+ subject do
+ render
+
+ rendered
+ end
+
+ context 'with ability to invite members' do
+ it { is_expected.to have_selector('.js-invite-members-modal') }
+ end
+
+ context 'without ability to invite members' do
+ let(:invite_member) { false }
+
+ it { is_expected.not_to have_selector('.js-invite-members-modal') }
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 212ddc49fe6..de413a5f5c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1221,10 +1221,10 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
-"@gitlab/svgs@3.22.0":
- version "3.22.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.22.0.tgz#29e6789efd03b21c8e028063ff40b1272924bae8"
- integrity sha512-To1MOwAvstlX1sZ9rB5SWxhkd0+rba1pzrHPgDdc6Ye15EPHHHUbJTZ4WPNAjWrxcqCkGNw+5NFyWx5y1GHuOQ==
+"@gitlab/svgs@3.23.0":
+ version "3.23.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.23.0.tgz#92ed37ebd2058f1c1ed4651f86d4a20736790afb"
+ integrity sha512-rq6md86C+2AH75wk3zY0e+aPRRK1QuBdhNPex/Q7IfR8gm+kADhYj1GSS6bnU80rfG6Fk49xi6VpSHWRlQZ0Zg==
"@gitlab/ui@56.2.0":
version "56.2.0"