Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ebe0e306bb
commit
807c4eae46
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1739a8ca9a5786b4730620b742153f45e00cb094
|
||||
65769c7a58d3339fe94a809bf6fd34f2f300a700
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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...') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
7e464616bdef6e225fdd31db84c4c32e223dffb81e13f1d6a5c85c2cd0a16144
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)**
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ module Gitlab
|
|||
# HTML comment line:
|
||||
# <!-- some commented text -->
|
||||
|
||||
^<!--\ .*\ -->\ *$
|
||||
^<!--\ .*?\ -->\ *$
|
||||
)
|
||||
}mx.freeze
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: true)
|
||||
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
|
||||
|
||||
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 '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_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, 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
|
||||
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: true)
|
||||
end
|
||||
|
||||
it 'returns hash' do
|
||||
expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
|
||||
.to include(base_data)
|
||||
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')
|
||||
expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
|
||||
.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)
|
||||
end
|
||||
|
||||
context 'when instance vars and parameters are not set' do
|
||||
it 'returns instance data in the hash as nil' do
|
||||
context 'with project' do
|
||||
it 'returns hash with parameters' 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
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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:)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue