Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ffe7c069a4
commit
584ccdaf68
|
|
@ -1,11 +1,13 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import imageDiff from '~/diffs/mixins/image_diff';
|
||||
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
|
||||
import DraftNote from './draft_note.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DraftNote,
|
||||
DesignNotePin,
|
||||
},
|
||||
mixins: [imageDiff],
|
||||
props: {
|
||||
|
|
@ -31,9 +33,12 @@ export default {
|
|||
class="discussion-notes diff-discussions position-relative"
|
||||
>
|
||||
<div class="notes">
|
||||
<span class="d-block btn-transparent badge badge-pill is-draft js-diff-notes-index">
|
||||
{{ toggleText(draft, index) }}
|
||||
</span>
|
||||
<design-note-pin
|
||||
:label="toggleText(draft, index)"
|
||||
is-draft
|
||||
class="js-diff-notes-index gl-translate-x-n50"
|
||||
size="sm"
|
||||
/>
|
||||
<draft-note :draft="draft" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -694,7 +694,7 @@ export default class Notes {
|
|||
// Convert returned HTML to a jQuery object so we can modify it further
|
||||
const $noteEntityEl = $(noteEntity.html);
|
||||
const $noteAvatar = $noteEntityEl.find('.image-diff-avatar-link');
|
||||
const $targetNoteBadge = $targetNote.find('.badge');
|
||||
const $targetNoteBadge = $targetNote.find('.design-note-pin');
|
||||
|
||||
$noteAvatar.append($targetNoteBadge);
|
||||
this.revertNoteEditForm($targetNote);
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ export default {
|
|||
"
|
||||
:is-inactive="isNoteInactive(note)"
|
||||
:is-resolved="note.resolved"
|
||||
is-on-image
|
||||
@mousedown.stop="onNoteMousedown($event, note)"
|
||||
@mouseup.stop="onNoteMouseup(note)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { mapActions } from 'vuex';
|
||||
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
|
||||
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
noteableDiscussion,
|
||||
GlIcon,
|
||||
DesignNotePin,
|
||||
},
|
||||
props: {
|
||||
discussions: {
|
||||
|
|
@ -62,20 +64,22 @@ export default {
|
|||
<ul :data-discussion-id="discussion.id" class="notes">
|
||||
<template v-if="shouldCollapseDiscussions">
|
||||
<button
|
||||
:class="{
|
||||
'diff-notes-collapse': discussion.expanded,
|
||||
'btn-transparent badge badge-pill': !discussion.expanded,
|
||||
}"
|
||||
v-if="discussion.expanded"
|
||||
class="diff-notes-collapse js-diff-notes-toggle"
|
||||
type="button"
|
||||
class="js-diff-notes-toggle"
|
||||
:aria-label="__('Show comments')"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id })"
|
||||
>
|
||||
<gl-icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
|
||||
<template v-else>
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
<gl-icon name="collapse" class="collapse-icon" />
|
||||
</button>
|
||||
<design-note-pin
|
||||
v-else
|
||||
:label="index + 1"
|
||||
:is-resolved="discussion.resolved"
|
||||
size="sm"
|
||||
class="js-diff-notes-toggle gl-translate-x-n50"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id })"
|
||||
/>
|
||||
</template>
|
||||
<noteable-discussion
|
||||
v-show="isExpanded(discussion)"
|
||||
|
|
@ -87,9 +91,12 @@ export default {
|
|||
@noteDeleted="deleteNoteHandler"
|
||||
>
|
||||
<template v-if="renderAvatarBadge" #avatar-badge>
|
||||
<span class="badge badge-pill">
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<design-note-pin
|
||||
:label="index + 1"
|
||||
class="user-avatar"
|
||||
:is-resolved="discussion.resolved"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
</noteable-discussion>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { isArray } from 'lodash';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
|
||||
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
|
||||
|
||||
function calcPercent(pos, renderedSize) {
|
||||
return (100 * pos) / renderedSize;
|
||||
|
|
@ -11,7 +11,7 @@ function calcPercent(pos, renderedSize) {
|
|||
export default {
|
||||
name: 'ImageDiffOverlay',
|
||||
components: {
|
||||
GlIcon,
|
||||
DesignNotePin,
|
||||
},
|
||||
mixins: [imageDiffMixin],
|
||||
props: {
|
||||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
badgeClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'badge badge-pill',
|
||||
default: '',
|
||||
},
|
||||
shouldToggleDiscussion: {
|
||||
type: Boolean,
|
||||
|
|
@ -114,30 +114,28 @@ export default {
|
|||
>
|
||||
<span class="sr-only"> {{ __('Add image comment') }} </span>
|
||||
</button>
|
||||
<button
|
||||
|
||||
<design-note-pin
|
||||
v-for="(discussion, index) in allDiscussions"
|
||||
:key="discussion.id"
|
||||
:style="getPosition(discussion)"
|
||||
:class="[badgeClass, { 'is-draft': discussion.isDraft }]"
|
||||
:disabled="!shouldToggleDiscussion"
|
||||
class="js-image-badge"
|
||||
type="button"
|
||||
:label="showCommentIcon ? null : toggleText(discussion, index)"
|
||||
:position="getPosition(discussion)"
|
||||
:aria-label="__('Show comments')"
|
||||
class="js-image-badge"
|
||||
:class="badgeClass"
|
||||
:is-draft="discussion.isDraft"
|
||||
:is-resolved="discussion.resolved"
|
||||
is-on-image
|
||||
:disabled="!shouldToggleDiscussion"
|
||||
@click="clickedToggle(discussion)"
|
||||
>
|
||||
<gl-icon v-if="showCommentIcon" name="image-comment-dark" :size="24" />
|
||||
<template v-else>
|
||||
{{ toggleText(discussion, index) }}
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
|
||||
<design-note-pin
|
||||
v-if="canComment && currentCommentForm"
|
||||
:style="{ left: `${currentCommentForm.xPercent}%`, top: `${currentCommentForm.yPercent}%` }"
|
||||
:aria-label="__('Comment form position')"
|
||||
class="btn-transparent comment-indicator position-absolute"
|
||||
type="button"
|
||||
>
|
||||
<gl-icon name="image-comment-dark" :size="24" />
|
||||
</button>
|
||||
:position="{
|
||||
left: `${currentCommentForm.xPercent}%`,
|
||||
top: `${currentCommentForm.yPercent}%`,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,15 @@ export function createImageBadge(noteId, { x, y }, classNames = []) {
|
|||
}
|
||||
|
||||
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
|
||||
const buttonEl = createImageBadge(noteId, coordinate, ['badge', 'badge-pill']);
|
||||
const buttonEl = createImageBadge(noteId, coordinate, [
|
||||
'gl-display-flex',
|
||||
'gl-align-items-center',
|
||||
'gl-justify-content-center',
|
||||
'gl-font-sm',
|
||||
'design-note-pin',
|
||||
'on-image',
|
||||
'gl-absolute',
|
||||
]);
|
||||
buttonEl.textContent = badgeText;
|
||||
|
||||
containerEl.appendChild(buttonEl);
|
||||
|
|
@ -30,8 +38,8 @@ export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
|
|||
export function addAvatarBadge(el, event) {
|
||||
const { noteId, badgeNumber } = event.detail;
|
||||
|
||||
// Add badge to new comment
|
||||
const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
|
||||
// Add design pin to new comment
|
||||
const avatarBadgeEl = el.querySelector(`#${noteId} .design-note-pin`);
|
||||
avatarBadgeEl.textContent = badgeNumber;
|
||||
avatarBadgeEl.classList.remove('hidden');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ export function setPositionDataAttribute(el, options) {
|
|||
}
|
||||
|
||||
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
|
||||
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
|
||||
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .design-note-pin');
|
||||
avatarBadgeEl.textContent = newBadgeNumber;
|
||||
}
|
||||
|
||||
export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
|
||||
const discussionBadgeEl = discussionEl.querySelector('.badge');
|
||||
const discussionBadgeEl = discussionEl.querySelector('.design-note-pin');
|
||||
discussionBadgeEl.textContent = newBadgeNumber;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export default class ImageDiff {
|
|||
removeBadge(event) {
|
||||
const { badgeNumber } = event.detail;
|
||||
const indexToRemove = badgeNumber - 1;
|
||||
const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge');
|
||||
const imageBadgeEls = this.imageFrameEl.querySelectorAll('.design-note-pin');
|
||||
|
||||
if (this.imageBadges.length !== badgeNumber) {
|
||||
// Cascade badges count numbers for (avatar badges + image badges)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default class ReplacedImageDiff extends ImageDiff {
|
|||
this.currentView = newView;
|
||||
|
||||
// Clear existing badges on new view
|
||||
const existingBadges = this.imageFrameEl.querySelectorAll('.badge');
|
||||
const existingBadges = this.imageFrameEl.querySelectorAll('.design-note-pin');
|
||||
[...existingBadges].map((badge) => badge.remove());
|
||||
|
||||
// Remove existing references to old view image badges
|
||||
|
|
|
|||
|
|
@ -28,12 +28,37 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isOnImage: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isDraft: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'md',
|
||||
validator: (value) => ['sm', 'md'].includes(value),
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isNewNote() {
|
||||
return this.label === null;
|
||||
},
|
||||
pinLabel() {
|
||||
if (this.ariaLabel) {
|
||||
return this.ariaLabel;
|
||||
}
|
||||
|
||||
return this.isNewNote
|
||||
? __('Comment form position')
|
||||
: sprintf(__("Comment '%{label}' position"), { label: this.label });
|
||||
|
|
@ -51,7 +76,10 @@ export default {
|
|||
'js-image-badge design-note-pin': !isNewNote,
|
||||
resolved: isResolved,
|
||||
inactive: isInactive,
|
||||
draft: isDraft,
|
||||
'on-image': isOnImage,
|
||||
'gl-absolute': position,
|
||||
small: size === 'sm',
|
||||
}"
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm"
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
$design-pin-diameter: 28px;
|
||||
$design-pin-diameter-sm: 24px;
|
||||
$t-gray-a-16-design-pin: rgba($black, 0.16);
|
||||
|
||||
.layout-page.design-detail-layout {
|
||||
|
|
@ -12,24 +13,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
|
|||
top: 35px;
|
||||
}
|
||||
|
||||
.design-note-pin {
|
||||
display: flex;
|
||||
height: $design-pin-diameter;
|
||||
width: $design-pin-diameter;
|
||||
box-sizing: content-box;
|
||||
background-color: $purple-500;
|
||||
color: $white;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
&.resolved {
|
||||
background-color: $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-indicator {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
|
@ -40,35 +23,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
|
|||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Design pin that overlays the design
|
||||
*/
|
||||
.frame .design-note-pin {
|
||||
box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
|
||||
border: $white 2px solid;
|
||||
will-change: transform, box-shadow, opacity;
|
||||
// NOTE: verbose transition property required for Safari
|
||||
transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
|
||||
transform-origin: 0 0;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2) translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
@include gl-opacity-5;
|
||||
|
||||
&:hover {
|
||||
@include gl-opacity-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.design-scaler-wrapper {
|
||||
|
|
@ -177,3 +131,63 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
|
|||
.design-card-header {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.design-note-pin {
|
||||
display: flex;
|
||||
height: $design-pin-diameter;
|
||||
width: $design-pin-diameter;
|
||||
box-sizing: content-box;
|
||||
background-color: $purple-500;
|
||||
color: $white;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
&.draft {
|
||||
background-color: $orange-500;
|
||||
}
|
||||
|
||||
&.resolved {
|
||||
background-color: $gray-500;
|
||||
}
|
||||
|
||||
&.on-image {
|
||||
box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
|
||||
border: $white 2px solid;
|
||||
will-change: transform, box-shadow, opacity;
|
||||
// NOTE: verbose transition property required for Safari
|
||||
transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
|
||||
transform-origin: 0 0;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2) translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
@include gl-opacity-5;
|
||||
|
||||
&:hover {
|
||||
@include gl-opacity-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
position: absolute;
|
||||
border: 1px solid $white;
|
||||
height: $design-pin-diameter-sm;
|
||||
width: $design-pin-diameter-sm;
|
||||
}
|
||||
|
||||
&.user-avatar {
|
||||
top: 25px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1072,24 +1072,6 @@ table.code {
|
|||
}
|
||||
}
|
||||
|
||||
.frame .badge.badge-pill,
|
||||
.image-diff-avatar-link .badge.badge-pill,
|
||||
.user-avatar-link .badge.badge-pill,
|
||||
.notes > .badge.badge-pill {
|
||||
position: absolute;
|
||||
background-color: $blue-400;
|
||||
color: $white;
|
||||
border: $white 1px solid;
|
||||
min-height: $gl-padding;
|
||||
padding: 5px 8px;
|
||||
border-radius: 12px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.frame .badge.badge-pill,
|
||||
.frame .image-comment-badge,
|
||||
.frame .comment-indicator {
|
||||
// Center align badges on the frame
|
||||
|
|
@ -1121,11 +1103,6 @@ table.code {
|
|||
}
|
||||
}
|
||||
|
||||
.notes > .badge.badge-pill {
|
||||
display: none;
|
||||
left: -13px;
|
||||
}
|
||||
|
||||
.discussion-notes {
|
||||
min-height: 35px;
|
||||
|
||||
|
|
@ -1134,18 +1111,22 @@ table.code {
|
|||
min-height: 25px;
|
||||
}
|
||||
|
||||
.diff-notes-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
background-color: $white;
|
||||
|
||||
.diff-notes-expand {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.diff-notes-collapse,
|
||||
.note,
|
||||
.discussion-reply-holder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notes > .badge.badge-pill {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Members
|
||||
class MembersAddedEvent < ::Gitlab::EventStore::Event
|
||||
def schema
|
||||
{
|
||||
'type' => 'object',
|
||||
'required' => %w[source_id source_type],
|
||||
'properties' => {
|
||||
'source_id' => { 'type' => 'integer' },
|
||||
'source_type' => { 'type' => 'string' }
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -125,6 +125,12 @@ module Types
|
|||
field :archived, GraphQL::Types::Boolean, null: true, method: :archived?,
|
||||
description: 'Whether the current project is archived.'
|
||||
|
||||
field :language, GraphQL::Types::String,
|
||||
description: 'Blob language.',
|
||||
method: :blob_language,
|
||||
null: true,
|
||||
calls_gitaly: true
|
||||
|
||||
def raw_text_blob
|
||||
object.data unless object.binary?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
def blob_language
|
||||
@_blob_language ||= Gitlab::Diff::CustomDiff.transformed_blob_language(blob) || language
|
||||
@_blob_language ||= Gitlab::Diff::CustomDiff.transformed_blob_language(blob) || gitattr_language || detect_language
|
||||
end
|
||||
|
||||
def raw_plain_data
|
||||
|
|
@ -166,9 +166,15 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
|
|||
@all_lines ||= blob.data.lines
|
||||
end
|
||||
|
||||
def language
|
||||
def gitattr_language
|
||||
blob.language_from_gitattributes
|
||||
end
|
||||
|
||||
def detect_language
|
||||
return if blob.binary?
|
||||
|
||||
Rouge::Lexer.guess(filename: blob.path, source: blob_data(nil)) { |lex| lex.min_by(&:tag) }.tag
|
||||
end
|
||||
end
|
||||
|
||||
BlobPresenter.prepend_mod_with('BlobPresenter')
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class SnippetBlobPresenter < BlobPresenter
|
|||
blob.container
|
||||
end
|
||||
|
||||
def language
|
||||
def gitattr_language
|
||||
nil
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ module Members
|
|||
|
||||
add_members
|
||||
enqueue_onboarding_progress_action
|
||||
|
||||
publish_event!
|
||||
|
||||
result
|
||||
rescue BlankInvitesError, TooManyInvitesError, MembershipLockedError => e
|
||||
error(e.message)
|
||||
|
|
@ -144,6 +147,15 @@ module Members
|
|||
def formatted_errors
|
||||
errors.to_sentence
|
||||
end
|
||||
|
||||
def publish_event!
|
||||
Gitlab::EventStore.publish(
|
||||
Members::MembersAddedEvent.new(data: {
|
||||
source_id: source.id,
|
||||
source_type: source.class.name
|
||||
})
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
-# to the first note position when we click on a badge diff discussion
|
||||
%ul.notes{ id: "discussion_#{discussion.id}", data: { discussion_id: discussion.id, position: discussion.notes[0].position.to_json } }
|
||||
- if discussion.try(:on_image?) && show_toggle
|
||||
%button.gl-button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
|
||||
%button.comment-indicator.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-font-sm.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
|
||||
= sprite_icon('collapse', css_class: 'collapse-icon')
|
||||
%button.gl-button.btn-transparent.badge.badge-pill.js-diff-notes-toggle{ type: 'button' }
|
||||
%button.gl-align-items-center.gl-justify-content-center.gl-font-sm.small.gl-translate-x-n50.design-note-pin.js-diff-notes-toggle.diff-notes-expand{ type: 'button' }
|
||||
= badge_counter
|
||||
= render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge }
|
||||
|
||||
|
|
|
|||
|
|
@ -64,43 +64,13 @@
|
|||
|
||||
- experiment(:new_project_sast_enabled, user: current_user) do |e|
|
||||
- e.try(:candidate) do
|
||||
.form-group
|
||||
.form-check.gl-mb-3
|
||||
= check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
|
||||
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
|
||||
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
|
||||
.form-text.text-muted
|
||||
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
|
||||
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
|
||||
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: true, with_free_badge: false
|
||||
- e.try(:unchecked_candidate) do
|
||||
.form-group
|
||||
.form-check.gl-mb-3
|
||||
= check_box_tag 'project[initialize_with_sast]', '1', false, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
|
||||
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
|
||||
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
|
||||
.form-text.text-muted
|
||||
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
|
||||
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
|
||||
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: false, with_free_badge: false
|
||||
- e.try(:free_indicator) do
|
||||
.form-group
|
||||
.form-check.gl-mb-3
|
||||
= check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
|
||||
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
|
||||
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
|
||||
= gl_badge_tag _('Free'), variant: :info, size: :sm
|
||||
.form-text.text-muted
|
||||
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
|
||||
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
|
||||
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: true, with_free_badge: true
|
||||
- e.try(:unchecked_free_indicator) do
|
||||
.form-group
|
||||
.form-check.gl-mb-3
|
||||
= check_box_tag 'project[initialize_with_sast]', '1', false, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
|
||||
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
|
||||
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
|
||||
= gl_badge_tag _('Free'), variant: :info, size: :sm
|
||||
.form-text.text-muted
|
||||
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
|
||||
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
|
||||
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: false, with_free_badge: true
|
||||
|
||||
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
|
||||
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
- experiment_name = local_assigns.fetch(:experiment_name)
|
||||
- track_label = local_assigns.fetch(:track_label)
|
||||
|
||||
- with_free_badge = local_assigns.fetch(:with_free_badge, false)
|
||||
- checked = local_assigns.fetch(:checked, false)
|
||||
|
||||
.form-group
|
||||
.form-check.gl-mb-3
|
||||
= check_box_tag 'project[initialize_with_sast]', '1', checked, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: experiment_name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
|
||||
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
|
||||
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
|
||||
- if with_free_badge
|
||||
= gl_badge_tag _('Free'), variant: :info, size: :sm
|
||||
.form-text.text-muted
|
||||
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
|
||||
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: experiment_name }
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
- elsif note_counter == 0
|
||||
- counter = badge_counter if local_assigns[:badge_counter]
|
||||
- badge_class = "hidden" if @fresh_discussion || counter.nil?
|
||||
%span.badge.badge-pill{ class: badge_class }
|
||||
%span.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-font-sm.design-note-pin.small.user-avatar{ class: badge_class }
|
||||
= counter
|
||||
.timeline-content
|
||||
.note-header
|
||||
|
|
|
|||
|
|
@ -177,6 +177,8 @@
|
|||
- 1
|
||||
- - gitlab_shell
|
||||
- 2
|
||||
- - gitlab_subscriptions_notify_seats_exceeded
|
||||
- 1
|
||||
- - group_destroy
|
||||
- 1
|
||||
- - group_export
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveIndexClustersKubernetesNamespacesOnClusterId < Gitlab::Database::Migration[1.0]
|
||||
INDEX = 'index_clusters_kubernetes_namespaces_on_cluster_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name :clusters_kubernetes_namespaces, INDEX
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :clusters_kubernetes_namespaces, :cluster_id, name: INDEX
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
873ff811d4f70c012785297ee8c07eb496994af69d5ae4b266e8f675c88daca4
|
||||
|
|
@ -25991,8 +25991,6 @@ CREATE INDEX index_clusters_integration_elasticstack_enabled ON clusters_integra
|
|||
|
||||
CREATE INDEX index_clusters_integration_prometheus_enabled ON clusters_integration_prometheus USING btree (enabled, created_at, cluster_id);
|
||||
|
||||
CREATE INDEX index_clusters_kubernetes_namespaces_on_cluster_id ON clusters_kubernetes_namespaces USING btree (cluster_id);
|
||||
|
||||
CREATE INDEX index_clusters_kubernetes_namespaces_on_cluster_project_id ON clusters_kubernetes_namespaces USING btree (cluster_project_id);
|
||||
|
||||
CREATE INDEX index_clusters_kubernetes_namespaces_on_environment_id ON clusters_kubernetes_namespaces USING btree (environment_id);
|
||||
|
|
|
|||
|
|
@ -4362,6 +4362,27 @@ Input type: `TerraformStateUnlockInput`
|
|||
| <a id="mutationterraformstateunlockclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationterraformstateunlockerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.timelineEventCreate`
|
||||
|
||||
Input type: `TimelineEventCreateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationtimelineeventcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationtimelineeventcreateincidentid"></a>`incidentId` | [`IssueID!`](#issueid) | Incident ID of the timeline event. |
|
||||
| <a id="mutationtimelineeventcreatenote"></a>`note` | [`String!`](#string) | Text note of the timeline event. |
|
||||
| <a id="mutationtimelineeventcreateoccurredat"></a>`occurredAt` | [`Time!`](#time) | Timestamp of when the event occurred. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationtimelineeventcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationtimelineeventcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationtimelineeventcreatetimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
|
||||
|
||||
### `Mutation.timelineEventDestroy`
|
||||
|
||||
Input type: `TimelineEventDestroyInput`
|
||||
|
|
@ -14759,6 +14780,7 @@ Returns [`Tree`](#tree).
|
|||
| <a id="repositoryblobid"></a>`id` | [`ID!`](#id) | ID of the blob. |
|
||||
| <a id="repositoryblobideeditpath"></a>`ideEditPath` | [`String`](#string) | Web path to edit this blob in the Web IDE. |
|
||||
| <a id="repositoryblobideforkandeditpath"></a>`ideForkAndEditPath` | [`String`](#string) | Web path to edit this blob in the Web IDE using a forked project. |
|
||||
| <a id="repositorybloblanguage"></a>`language` | [`String`](#string) | Blob language. |
|
||||
| <a id="repositorybloblfsoid"></a>`lfsOid` | [`String`](#string) | LFS OID of the blob. |
|
||||
| <a id="repositoryblobmode"></a>`mode` | [`String`](#string) | Blob mode. |
|
||||
| <a id="repositoryblobname"></a>`name` | [`String`](#string) | Blob name. |
|
||||
|
|
|
|||
|
|
@ -263,6 +263,9 @@ Other commonly used variables for `if` clauses:
|
|||
branch. Use when you want to have the same configuration in multiple
|
||||
projects with different default branches.
|
||||
- `if: '$CI_COMMIT_BRANCH =~ /regex-expression/'`: If the commit branch matches a regular expression.
|
||||
- `if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE =~ /Merge branch.*/`:
|
||||
If the commit branch is the default branch and the commit message title matches a regular expression.
|
||||
For example, the default commit message for a merge commit starts with `Merge branch`.
|
||||
- `if: '$CUSTOM_VARIABLE !~ /regex-expression/'`: If the [custom variable](../variables/index.md#custom-cicd-variables)
|
||||
`CUSTOM_VARIABLE` does **not** match a regular expression.
|
||||
- `if: '$CUSTOM_VARIABLE == "value1"'`: If the custom variable `CUSTOM_VARIABLE` is
|
||||
|
|
|
|||
|
|
@ -252,19 +252,20 @@ add a line like this to the `Gitlab::EventStore.configure!` method:
|
|||
```ruby
|
||||
module Gitlab
|
||||
module EventStore
|
||||
def self.configure!
|
||||
Store.new.tap do |store|
|
||||
# ...
|
||||
def self.configure!(store)
|
||||
# ...
|
||||
|
||||
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
|
||||
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
|
||||
|
||||
# ...
|
||||
end
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
A worker that is only defined in the EE codebase can subscribe to an event in the same way by
|
||||
declaring the subscription in `ee/lib/ee/gitlab/event_store.rb`.
|
||||
|
||||
Subscriptions are stored in memory when the Rails app is loaded and they are immediately frozen.
|
||||
It's not possible to modify subscriptions at runtime.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Container Host Security **(FREE)**
|
||||
|
||||
NOTE:
|
||||
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
|
||||
You can continue using Container Host Security, even though it relies on this certificate-based
|
||||
method. The work to allow all aspects of Container Host Security to function through the [GitLab Agent](../../../../clusters/agent/index.md)
|
||||
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350).
|
||||
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
|
||||
|
||||
WARNING:
|
||||
Container Host Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
|
||||
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
|
||||
in GitLab 15.0.
|
||||
|
||||
Container Host Security in GitLab provides Intrusion Detection and Prevention capabilities that can
|
||||
monitor and (optionally) block activity inside the containers themselves. This is done by leveraging
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Getting started with Container Host Security **(FREE)**
|
||||
|
||||
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
|
||||
|
||||
WARNING:
|
||||
Container Host Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
|
||||
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
|
||||
in GitLab 15.0.
|
||||
|
||||
The following steps are recommended for installing Container Host Security.
|
||||
|
||||
## Installation steps
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Container Network Security **(FREE)**
|
||||
|
||||
NOTE:
|
||||
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
|
||||
You can continue using Container Network Security, even though it relies on this certificate-based
|
||||
method. The work to allow all aspects of Container Network Security to function through the [GitLab Agent](../../../../clusters/agent/index.md)
|
||||
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350) and [this GitLab Epic](https://gitlab.com/groups/gitlab-org/-/epics/7057).
|
||||
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
|
||||
|
||||
WARNING:
|
||||
Container Network Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
|
||||
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
|
||||
in GitLab 15.0.
|
||||
|
||||
Container Network Security in GitLab provides basic firewall functionality by leveraging Cilium
|
||||
NetworkPolicies to filter traffic going in and out of the cluster as well as traffic between pods
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Getting started with Container Network Security **(FREE)**
|
||||
|
||||
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
|
||||
|
||||
WARNING:
|
||||
Container Network Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
|
||||
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
|
||||
in GitLab 15.0.
|
||||
|
||||
The following steps are recommended for installing Container Network Security.
|
||||
|
||||
## Installation steps
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Protecting your deployed applications **(FREE)**
|
||||
|
||||
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
|
||||
|
||||
WARNING:
|
||||
The Container Network Security and Container Host Security features are in their end-of-life
|
||||
processes. They're
|
||||
[deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
|
||||
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
|
||||
in GitLab 15.0.
|
||||
|
||||
GitLab makes it straightforward to protect applications deployed in [connected Kubernetes clusters](index.md).
|
||||
These protections are available in the Kubernetes network layer and in the container itself. At
|
||||
the network layer, the Container Network Security capabilities in GitLab provide basic firewall
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ You can configure GitLab to send notifications to a Webex Teams space:
|
|||
|
||||
## Create a webhook for the space
|
||||
|
||||
1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/applications/incoming-webhooks-cisco-systems-38054).
|
||||
1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/applications/incoming-webhooks-cisco-systems-38054-23307).
|
||||
1. Select **Connect** and log in to Webex Teams, if required.
|
||||
1. Enter a name for the webhook and select the space to receive the notifications.
|
||||
1. Select **ADD**.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def self.instance
|
||||
@instance ||= configure!
|
||||
@instance ||= Store.new { |store| configure!(store) }
|
||||
end
|
||||
|
||||
# Define all event subscriptions using:
|
||||
|
|
@ -29,14 +29,14 @@ module Gitlab
|
|||
#
|
||||
# store.subscribe(DomainA::SomeWorker, to: DomainB::SomeEvent), if: ->(event) { event.data == :some_value }
|
||||
#
|
||||
def self.configure!
|
||||
Store.new do |store|
|
||||
###
|
||||
# Add subscriptions here:
|
||||
def self.configure!(store)
|
||||
###
|
||||
# Add subscriptions here:
|
||||
|
||||
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
|
||||
end
|
||||
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
|
||||
end
|
||||
private_class_method :configure!
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::EventStore.prepend_mod_with('Gitlab::EventStore')
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ module Gitlab
|
|||
raise InvalidEvent, "Event being published is not an instance of Gitlab::EventStore::Event: got #{event.inspect}"
|
||||
end
|
||||
|
||||
subscriptions[event.class].each do |subscription|
|
||||
subscriptions.fetch(event.class, []).each do |subscription|
|
||||
subscription.consume_event(event)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,29 +43,29 @@ module Gitlab
|
|||
TRANSLATION_LEVELS = {
|
||||
'bg' => 0,
|
||||
'cs_CZ' => 0,
|
||||
'da_DK' => 49,
|
||||
'da_DK' => 48,
|
||||
'de' => 15,
|
||||
'en' => 100,
|
||||
'eo' => 0,
|
||||
'es' => 38,
|
||||
'es' => 39,
|
||||
'fil_PH' => 0,
|
||||
'fr' => 11,
|
||||
'gl_ES' => 0,
|
||||
'id_ID' => 0,
|
||||
'it' => 2,
|
||||
'ja' => 36,
|
||||
'ko' => 12,
|
||||
'nb_NO' => 32,
|
||||
'ja' => 35,
|
||||
'ko' => 13,
|
||||
'nb_NO' => 31,
|
||||
'nl_NL' => 0,
|
||||
'pl_PL' => 5,
|
||||
'pl_PL' => 4,
|
||||
'pt_BR' => 50,
|
||||
'ro_RO' => 22,
|
||||
'ru' => 26,
|
||||
'ru' => 32,
|
||||
'tr_TR' => 14,
|
||||
'uk' => 45,
|
||||
'zh_CN' => 98,
|
||||
'uk' => 44,
|
||||
'zh_CN' => 96,
|
||||
'zh_HK' => 2,
|
||||
'zh_TW' => 3
|
||||
'zh_TW' => 2
|
||||
}.freeze
|
||||
private_constant :TRANSLATION_LEVELS
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ module Gitlab
|
|||
super(presenter_class: BlobPresenter)
|
||||
end
|
||||
|
||||
def binary?
|
||||
false
|
||||
end
|
||||
|
||||
def fetch_blob
|
||||
path = [ref, blob_path]
|
||||
missing_blob = { binary_path: blob_path }
|
||||
|
|
|
|||
|
|
@ -33432,6 +33432,9 @@ msgstr ""
|
|||
msgid "Slack logo"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Are you sure you want to remove this project from the Slack application?"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|GitLab for Slack"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33441,6 +33444,9 @@ msgstr ""
|
|||
msgid "SlackIntegration|Project alias"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Remove project"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Select a GitLab project to link with your Slack workspace."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ module QA
|
|||
|
||||
view 'app/views/projects/_new_project_fields.html.haml' do
|
||||
element :initialize_with_readme_checkbox
|
||||
element :initialize_with_sast_checkbox
|
||||
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
|
||||
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
|
||||
element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern
|
||||
|
|
@ -21,6 +20,10 @@ module QA
|
|||
element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
view 'app/views/projects/_new_project_initialize_with_sast.html.haml' do
|
||||
element :initialize_with_sast_checkbox
|
||||
end
|
||||
|
||||
view 'app/views/projects/project_templates/_template.html.haml' do
|
||||
element :use_template_button
|
||||
element :template_option_row
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ RSpec.describe 'Merge request > User creates image diff notes', :js do
|
|||
|
||||
it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
|
||||
indicator = find('.js-image-badge')
|
||||
badge = find('.image-diff-avatar-link .badge')
|
||||
badge = find('.image-diff-avatar-link .design-note-pin')
|
||||
|
||||
expect(indicator).to have_content('1')
|
||||
expect(badge).to have_content('1')
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Vue from 'vue';
|
|||
import Vuex from 'vuex';
|
||||
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
|
||||
import DraftNote from '~/batch_comments/components/draft_note.vue';
|
||||
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
|
@ -40,10 +41,12 @@ describe('Batch comments diff file drafts component', () => {
|
|||
it('renders index of draft note', () => {
|
||||
factory();
|
||||
|
||||
expect(vm.findAll('.js-diff-notes-index').length).toEqual(2);
|
||||
const elements = vm.findAll(DesignNotePin);
|
||||
|
||||
expect(vm.findAll('.js-diff-notes-index').at(0).text()).toEqual('1');
|
||||
expect(elements.length).toEqual(2);
|
||||
|
||||
expect(vm.findAll('.js-diff-notes-index').at(1).text()).toEqual('2');
|
||||
expect(elements.at(0).props('label')).toEqual(1);
|
||||
|
||||
expect(elements.at(1).props('label')).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('DiffDiscussions', () => {
|
|||
|
||||
expect(diffNotesToggle.text().trim()).toBe('1');
|
||||
expect(diffNotesToggle.classes()).toEqual(
|
||||
expect.arrayContaining(['btn-transparent', 'badge', 'badge-pill']),
|
||||
expect.arrayContaining(['js-diff-notes-toggle', 'gl-translate-x-n50', 'design-note-pin']),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -87,8 +87,8 @@ describe('DiffDiscussions', () => {
|
|||
createComponent({ renderAvatarBadge: true });
|
||||
const noteableDiscussion = wrapper.find(NoteableDiscussion);
|
||||
|
||||
expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true);
|
||||
expect(noteableDiscussion.find('.badge-pill').text().trim()).toBe('1');
|
||||
expect(noteableDiscussion.find('.design-note-pin').exists()).toBe(true);
|
||||
expect(noteableDiscussion.find('.design-note-pin').text().trim()).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import { imageDiffDiscussions } from '../mock_data/diff_discussions';
|
||||
|
|
@ -19,7 +19,7 @@ describe('Diffs image diff overlay component', () => {
|
|||
extendStore(store);
|
||||
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
|
||||
wrapper = shallowMount(ImageDiffOverlay, {
|
||||
wrapper = mount(ImageDiffOverlay, {
|
||||
store,
|
||||
parentComponent: {
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ describe('badge helper', () => {
|
|||
});
|
||||
|
||||
it('should add badge classes', () => {
|
||||
expect(buttonEl.className).toContain('badge badge-pill');
|
||||
const classes = buttonEl.className.split(' ');
|
||||
expect(classes).toEqual(
|
||||
expect.arrayContaining(['design-note-pin', 'on-image', 'gl-absolute']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set the badge text', () => {
|
||||
|
|
@ -105,7 +108,7 @@ describe('badge helper', () => {
|
|||
beforeEach(() => {
|
||||
containerEl.innerHTML = `
|
||||
<div id="${noteId}">
|
||||
<div class="badge hidden">
|
||||
<div class="design-note-pin hidden">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -116,7 +119,7 @@ describe('badge helper', () => {
|
|||
badgeNumber,
|
||||
},
|
||||
});
|
||||
avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
|
||||
avatarBadgeEl = containerEl.querySelector(`#${noteId} .design-note-pin`);
|
||||
});
|
||||
|
||||
it('should update badge number', () => {
|
||||
|
|
|
|||
|
|
@ -37,14 +37,16 @@ describe('domHelper', () => {
|
|||
discussionEl = document.createElement('div');
|
||||
discussionEl.innerHTML = `
|
||||
<a href="#" class="image-diff-avatar-link">
|
||||
<div class="badge"></div>
|
||||
<div class="design-note-pin"></div>
|
||||
</a>
|
||||
`;
|
||||
domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
|
||||
});
|
||||
|
||||
it('should update avatar badge number', () => {
|
||||
expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
|
||||
expect(discussionEl.querySelector('.design-note-pin').textContent).toEqual(
|
||||
badgeNumber.toString(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -54,13 +56,15 @@ describe('domHelper', () => {
|
|||
beforeEach(() => {
|
||||
discussionEl = document.createElement('div');
|
||||
discussionEl.innerHTML = `
|
||||
<div class="badge"></div>
|
||||
<div class="design-note-pin"></div>
|
||||
`;
|
||||
domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
|
||||
});
|
||||
|
||||
it('should update discussion badge number', () => {
|
||||
expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
|
||||
expect(discussionEl.querySelector('.design-note-pin').textContent).toEqual(
|
||||
badgeNumber.toString(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ describe('ImageDiff', () => {
|
|||
<div class="js-image-frame">
|
||||
<img src="${TEST_HOST}/image.png">
|
||||
<div class="comment-indicator"></div>
|
||||
<div id="badge-1" class="badge">1</div>
|
||||
<div id="badge-2" class="badge">2</div>
|
||||
<div id="badge-3" class="badge">3</div>
|
||||
<div id="badge-1" class="design-note-pin">1</div>
|
||||
<div id="badge-2" class="design-note-pin">2</div>
|
||||
<div id="badge-3" class="design-note-pin">3</div>
|
||||
</div>
|
||||
<div class="note-container">
|
||||
<div class="discussion-notes">
|
||||
|
|
@ -335,7 +335,7 @@ describe('ImageDiff', () => {
|
|||
|
||||
describe('cascade badge count', () => {
|
||||
it('should update next imageBadgeEl value', () => {
|
||||
const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
|
||||
const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.design-note-pin');
|
||||
|
||||
expect(imageBadgeEls[0].textContent).toEqual('1');
|
||||
expect(imageBadgeEls[1].textContent).toEqual('2');
|
||||
|
|
|
|||
|
|
@ -39,4 +39,72 @@ describe('Design note pin component', () => {
|
|||
createComponent({ position: null });
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('applies `on-image` class when isOnImage is true', () => {
|
||||
createComponent({ isOnImage: true });
|
||||
|
||||
expect(wrapper.find('.on-image').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('applies `draft` class when isDraft is true', () => {
|
||||
createComponent({ isDraft: true });
|
||||
|
||||
expect(wrapper.find('.draft').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('size', () => {
|
||||
it('is `sm` it applies `small` class', () => {
|
||||
createComponent({ size: 'sm' });
|
||||
expect(wrapper.find('.small').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('is `md` it applies no size class', () => {
|
||||
createComponent({ size: 'md' });
|
||||
expect(wrapper.find('.small').exists()).toBe(false);
|
||||
expect(wrapper.find('.medium').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('throws when passed any other value except `sm` or `md`', () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
createComponent({ size: 'lg' });
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ariaLabel', () => {
|
||||
describe('when value is passed', () => {
|
||||
it('overrides default aria-label', () => {
|
||||
const ariaLabel = 'Aria Label';
|
||||
|
||||
createComponent({ ariaLabel });
|
||||
|
||||
const button = wrapper.find('button');
|
||||
|
||||
expect(button.attributes('aria-label')).toBe(ariaLabel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no value is passed', () => {
|
||||
it('shows new note label as aria-label when label is absent', () => {
|
||||
createComponent({ label: null });
|
||||
|
||||
const button = wrapper.find('button');
|
||||
|
||||
expect(button.attributes('aria-label')).toBe('Comment form position');
|
||||
});
|
||||
|
||||
it('shows label position as aria-label when label is present', () => {
|
||||
const label = 1;
|
||||
|
||||
createComponent({ label, isNewNote: false });
|
||||
|
||||
const button = wrapper.find('button');
|
||||
|
||||
expect(button.attributes('aria-label')).toBe(`Comment '${label}' position`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ RSpec.describe Types::Repository::BlobType do
|
|||
:ide_edit_path,
|
||||
:external_storage_url,
|
||||
:fork_and_edit_path,
|
||||
:ide_fork_and_edit_path
|
||||
:ide_fork_and_edit_path,
|
||||
:language
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -224,6 +224,26 @@ RSpec.describe Gitlab::EventStore::Store do
|
|||
store.publish(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the event does not have any subscribers' do
|
||||
let(:store) do
|
||||
described_class.new do |s|
|
||||
s.subscribe unrelated_worker, to: another_event_klass
|
||||
end
|
||||
end
|
||||
|
||||
let(:event) { event_klass.new(data: data) }
|
||||
|
||||
it 'returns successfully' do
|
||||
expect { store.publish(event) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does not dispatch the event to another subscription' do
|
||||
expect(unrelated_worker).not_to receive(:perform_async)
|
||||
|
||||
store.publish(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'subscriber' do
|
||||
|
|
|
|||
|
|
@ -170,13 +170,13 @@ RSpec.describe BlobPresenter do
|
|||
let(:git_blob) { blob.__getobj__ }
|
||||
|
||||
it 'returns highlighted content' do
|
||||
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil)
|
||||
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby')
|
||||
|
||||
presenter.highlight
|
||||
end
|
||||
|
||||
it 'returns plain content when :plain is true' do
|
||||
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil)
|
||||
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: 'ruby')
|
||||
|
||||
presenter.highlight(plain: true)
|
||||
end
|
||||
|
|
@ -189,7 +189,7 @@ RSpec.describe BlobPresenter do
|
|||
end
|
||||
|
||||
it 'returns limited highlighted content' do
|
||||
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: nil)
|
||||
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: 'ruby')
|
||||
|
||||
presenter.highlight(to: 1)
|
||||
end
|
||||
|
|
@ -247,6 +247,36 @@ RSpec.describe BlobPresenter do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#blob_language' do
|
||||
subject { presenter.blob_language }
|
||||
|
||||
it { is_expected.to eq('ruby') }
|
||||
|
||||
context 'gitlab-language contains a match' do
|
||||
before do
|
||||
allow(blob).to receive(:language_from_gitattributes).and_return('cpp')
|
||||
end
|
||||
|
||||
it { is_expected.to eq('cpp') }
|
||||
end
|
||||
|
||||
context 'when blob is ipynb' do
|
||||
let(:blob) { repository.blob_at('f6b7a707', 'files/ipython/markdown-table.ipynb') }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Diff::CustomDiff).to receive(:transformed_for_diff?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to eq('md') }
|
||||
end
|
||||
|
||||
context 'when blob is binary' do
|
||||
let(:blob) { repository.blob_at('HEAD', 'Gemfile.zip') }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#raw_plain_data' do
|
||||
let(:blob) { repository.blob_at('HEAD', file) }
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
|
|||
expect(source.users).to include member
|
||||
expect(OnboardingProgress.completed?(source, :user_added)).to be(true)
|
||||
end
|
||||
|
||||
it 'triggers a members added event' do
|
||||
expect(Gitlab::EventStore)
|
||||
.to receive(:publish)
|
||||
.with(an_instance_of(Members::MembersAddedEvent))
|
||||
.and_call_original
|
||||
|
||||
expect(execute_service[:status]).to eq(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue