Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-07 15:10:50 +00:00
parent ebe0e306bb
commit 807c4eae46
70 changed files with 1068 additions and 591 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -1 +1 @@
1739a8ca9a5786b4730620b742153f45e00cb094
65769c7a58d3339fe94a809bf6fd34f2f300a700

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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';

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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) }

View File

@ -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)

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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...') }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 } }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
7e464616bdef6e225fdd31db84c4c32e223dffb81e13f1d6a5c85c2cd0a16144

View File

@ -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);

View File

@ -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.

View File

@ -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)**

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -466,7 +466,7 @@ module Gitlab
# HTML comment line:
# <!-- some commented text -->
^<!--\ .*\ -->\ *$
^<!--\ .*?\ -->\ *$
)
}mx.freeze

View File

@ -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 ""

View File

@ -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",

View File

@ -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'

View File

@ -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();

View File

@ -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',
},
);
});
});
},
);
});
});

View File

@ -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',
},
);
});
});
});
});

View File

@ -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]]);
});
});

View File

@ -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]]);
});
});
});

View File

@ -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);
});

View File

@ -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

View File

@ -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:)

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"