Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fd767b7d65
commit
1ccebc7b3f
|
@ -1 +1 @@
|
|||
e4d8f69ffa2efd3f2cb0adff5fa66f367f66f6fb
|
||||
c13d9d902ef8175a0b1165ef0bc8643fb37b7897
|
||||
|
|
|
@ -8,21 +8,18 @@ import {
|
|||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormSelect,
|
||||
GlSegmentedControl,
|
||||
} from '@gitlab/ui';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { setUrlFragment } from '~/lib/utils/url_utility';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import {
|
||||
CONTENT_EDITOR_LOADED_ACTION,
|
||||
SAVED_USING_CONTENT_EDITOR_ACTION,
|
||||
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
|
||||
WIKI_FORMAT_LABEL,
|
||||
WIKI_FORMAT_UPDATED_ACTION,
|
||||
CONTENT_EDITOR_LOADED_ACTION,
|
||||
} from '../constants';
|
||||
|
||||
const trackingMixin = Tracking.mixin({
|
||||
|
@ -74,10 +71,6 @@ export default {
|
|||
},
|
||||
cancel: s__('WikiPage|Cancel'),
|
||||
},
|
||||
switchEditingControlOptions: [
|
||||
{ text: s__('Wiki Page|Source'), value: 'source' },
|
||||
{ text: s__('Wiki Page|Rich text'), value: 'richText' },
|
||||
],
|
||||
components: {
|
||||
GlIcon,
|
||||
GlForm,
|
||||
|
@ -87,13 +80,7 @@ export default {
|
|||
GlSprintf,
|
||||
GlLink,
|
||||
GlButton,
|
||||
GlSegmentedControl,
|
||||
MarkdownField,
|
||||
LocalStorageSync,
|
||||
ContentEditor: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
|
||||
),
|
||||
MarkdownEditor,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
inject: ['formatOptions', 'pageInfo'],
|
||||
|
@ -106,7 +93,7 @@ export default {
|
|||
commitMessage: '',
|
||||
isDirty: false,
|
||||
contentEditorEmpty: false,
|
||||
switchEditingControlDisabled: false,
|
||||
isContentEditorActive: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -162,12 +149,6 @@ export default {
|
|||
disableSubmitButton() {
|
||||
return this.noContent || !this.title;
|
||||
},
|
||||
isContentEditorActive() {
|
||||
return this.isMarkdownFormat && this.useContentEditor;
|
||||
},
|
||||
useContentEditor() {
|
||||
return this.editingMode === 'richText';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateCommitMessage();
|
||||
|
@ -178,23 +159,10 @@ export default {
|
|||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
},
|
||||
methods: {
|
||||
renderMarkdown(content) {
|
||||
return axios
|
||||
.post(this.pageInfo.markdownPreviewPath, { text: content })
|
||||
.then(({ data }) => data.body);
|
||||
},
|
||||
|
||||
setEditingMode(editingMode) {
|
||||
this.editingMode = editingMode;
|
||||
},
|
||||
|
||||
async handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.useContentEditor) {
|
||||
this.trackFormSubmit();
|
||||
}
|
||||
|
||||
this.trackWikiFormat();
|
||||
|
||||
// Wait until form field values are refreshed
|
||||
|
@ -205,16 +173,6 @@ export default {
|
|||
this.isDirty = false;
|
||||
},
|
||||
|
||||
handleContentChange() {
|
||||
this.isDirty = true;
|
||||
},
|
||||
|
||||
handleContentEditorChange({ empty, markdown, changed }) {
|
||||
this.contentEditorEmpty = empty;
|
||||
this.isDirty = changed;
|
||||
this.content = markdown;
|
||||
},
|
||||
|
||||
onPageUnload(event) {
|
||||
if (!this.isDirty) return undefined;
|
||||
|
||||
|
@ -235,8 +193,13 @@ export default {
|
|||
this.commitMessage = newCommitMessage;
|
||||
},
|
||||
|
||||
trackContentEditorLoaded() {
|
||||
this.track(CONTENT_EDITOR_LOADED_ACTION);
|
||||
notifyContentEditorActive() {
|
||||
this.isContentEditorActive = true;
|
||||
this.trackContentEditorLoaded();
|
||||
},
|
||||
|
||||
notifyContentEditorInactive() {
|
||||
this.isContentEditorActive = false;
|
||||
},
|
||||
|
||||
trackFormSubmit() {
|
||||
|
@ -256,12 +219,12 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
enableSwitchEditingControl() {
|
||||
this.switchEditingControlDisabled = false;
|
||||
trackContentEditorLoaded() {
|
||||
this.track(CONTENT_EDITOR_LOADED_ACTION);
|
||||
},
|
||||
|
||||
disableSwitchEditingControl() {
|
||||
this.switchEditingControlDisabled = true;
|
||||
checkDirty(markdown) {
|
||||
this.isDirty = this.pageInfo.content !== markdown;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -329,74 +292,22 @@ export default {
|
|||
<div class="row" data-testid="wiki-form-content-fieldset">
|
||||
<div class="col-sm-12 row-sm-5">
|
||||
<gl-form-group>
|
||||
<div v-if="isMarkdownFormat" class="gl-display-flex gl-justify-content-start gl-mb-3">
|
||||
<gl-segmented-control
|
||||
data-testid="toggle-editing-mode-button"
|
||||
data-qa-selector="editing_mode_button"
|
||||
class="gl-display-flex"
|
||||
:checked="editingMode"
|
||||
:options="$options.switchEditingControlOptions"
|
||||
:disabled="switchEditingControlDisabled"
|
||||
@input="setEditingMode"
|
||||
/>
|
||||
</div>
|
||||
<local-storage-sync
|
||||
storage-key="gl-wiki-content-editor-enabled"
|
||||
:value="editingMode"
|
||||
@input="setEditingMode"
|
||||
/>
|
||||
<markdown-field
|
||||
v-if="!isContentEditorActive"
|
||||
:markdown-preview-path="pageInfo.markdownPreviewPath"
|
||||
:can-attach-file="true"
|
||||
:enable-autocomplete="true"
|
||||
:textarea-value="content"
|
||||
<markdown-editor
|
||||
v-model="content"
|
||||
:render-markdown-path="pageInfo.markdownPreviewPath"
|
||||
:markdown-docs-path="pageInfo.markdownHelpPath"
|
||||
:uploads-path="pageInfo.uploadsPath"
|
||||
:enable-content-editor="isMarkdownFormat"
|
||||
:enable-preview="isMarkdownFormat"
|
||||
class="bordered-box"
|
||||
>
|
||||
<template #textarea>
|
||||
<textarea
|
||||
id="wiki_content"
|
||||
ref="textarea"
|
||||
v-model="content"
|
||||
name="wiki[content]"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
||||
dir="auto"
|
||||
data-supports-quick-actions="false"
|
||||
data-qa-selector="wiki_content_textarea"
|
||||
:autofocus="pageInfo.persisted"
|
||||
:aria-label="$options.i18n.content.label"
|
||||
:placeholder="$options.i18n.content.placeholder"
|
||||
@input="handleContentChange"
|
||||
>
|
||||
</textarea>
|
||||
</template>
|
||||
</markdown-field>
|
||||
<div v-if="isContentEditorActive">
|
||||
<content-editor
|
||||
:render-markdown="renderMarkdown"
|
||||
:uploads-path="pageInfo.uploadsPath"
|
||||
:markdown="content"
|
||||
@initialized="trackContentEditorLoaded"
|
||||
@change="handleContentEditorChange"
|
||||
@loading="disableSwitchEditingControl"
|
||||
@loadingSuccess="enableSwitchEditingControl"
|
||||
@loadingError="enableSwitchEditingControl"
|
||||
:form-field-placeholder="$options.i18n.content.placeholder"
|
||||
:form-field-aria-label="$options.i18n.content.label"
|
||||
form-field-id="wiki_content"
|
||||
form-field-name="wiki[content]"
|
||||
@contentEditor="notifyContentEditorActive"
|
||||
@markdownField="notifyContentEditorInactive"
|
||||
@input="checkDirty"
|
||||
/>
|
||||
<input
|
||||
id="wiki_content"
|
||||
v-model.trim="content"
|
||||
type="hidden"
|
||||
name="wiki[content]"
|
||||
data-qa-selector="wiki_hidden_content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<div class="error-alert"></div>
|
||||
|
||||
<div class="form-text gl-text-gray-600">
|
||||
<gl-sprintf
|
||||
v-if="displayWikiSpecificMarkdownHelp"
|
||||
|
|
|
@ -161,11 +161,7 @@ export default {
|
|||
@click="handleEmojiClick"
|
||||
>
|
||||
<template #button-content>
|
||||
<span
|
||||
v-if="noEmoji"
|
||||
class="no-emoji-placeholder position-relative"
|
||||
data-testid="no-emoji-placeholder"
|
||||
>
|
||||
<span v-if="noEmoji" class="gl-relative" data-testid="no-emoji-placeholder">
|
||||
<gl-icon name="slight-smile" class="award-control-icon-neutral" />
|
||||
<gl-icon name="smiley" class="award-control-icon-positive" />
|
||||
<gl-icon name="smile" class="award-control-icon-super-positive" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { IssuableType } from '~/issues/constants';
|
||||
import { isLoggedIn } from '~/lib/utils/common_utils';
|
||||
|
@ -22,6 +22,7 @@ export default {
|
|||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlDropdownForm,
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
GlToggle,
|
||||
|
@ -181,7 +182,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isMergeRequest" class="gl-new-dropdown-item">
|
||||
<gl-dropdown-form v-if="isMergeRequest" class="gl-new-dropdown-item">
|
||||
<div class="gl-px-5 gl-pb-2 gl-pt-1">
|
||||
<gl-toggle
|
||||
:value="subscribed"
|
||||
|
@ -192,7 +193,7 @@ export default {
|
|||
@change="toggleSubscribed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</gl-dropdown-form>
|
||||
<sidebar-editable-item
|
||||
v-else
|
||||
ref="editable"
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
<script>
|
||||
import { GlSegmentedControl } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants';
|
||||
import MarkdownField from './field.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MarkdownField,
|
||||
LocalStorageSync,
|
||||
GlSegmentedControl,
|
||||
ContentEditor: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
|
||||
),
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
renderMarkdownPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
markdownDocsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
uploadsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
enableContentEditor: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
formFieldId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
formFieldName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
enablePreview: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
enableAutocomplete: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
formFieldPlaceholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
formFieldAriaLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editingMode: EDITING_MODE_MARKDOWN_FIELD,
|
||||
switchEditingControlEnabled: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isContentEditorActive() {
|
||||
return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateMarkdownFromContentEditor({ markdown }) {
|
||||
this.$emit('input', markdown);
|
||||
},
|
||||
updateMarkdownFromMarkdownField({ target }) {
|
||||
this.$emit('input', target.value);
|
||||
},
|
||||
enableSwitchEditingControl() {
|
||||
this.switchEditingControlEnabled = true;
|
||||
},
|
||||
disableSwitchEditingControl() {
|
||||
this.switchEditingControlEnabled = false;
|
||||
},
|
||||
renderMarkdown(markdown) {
|
||||
return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body);
|
||||
},
|
||||
notifyEditingModeChange(editingMode) {
|
||||
this.$emit(editingMode);
|
||||
},
|
||||
},
|
||||
switchEditingControlOptions: [
|
||||
{ text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
|
||||
{ text: __('Rich text'), value: EDITING_MODE_CONTENT_EDITOR },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="gl-display-flex gl-justify-content-start gl-mb-3">
|
||||
<gl-segmented-control
|
||||
v-model="editingMode"
|
||||
data-testid="toggle-editing-mode-button"
|
||||
data-qa-selector="editing_mode_button"
|
||||
class="gl-display-flex"
|
||||
:options="$options.switchEditingControlOptions"
|
||||
:disabled="!enableContentEditor || !switchEditingControlEnabled"
|
||||
@change="notifyEditingModeChange"
|
||||
/>
|
||||
</div>
|
||||
<local-storage-sync
|
||||
v-model="editingMode"
|
||||
storage-key="gl-wiki-content-editor-enabled"
|
||||
@input="notifyEditingModeChange"
|
||||
/>
|
||||
<markdown-field
|
||||
v-if="!isContentEditorActive"
|
||||
:markdown-preview-path="renderMarkdownPath"
|
||||
can-attach-file
|
||||
:enable-autocomplete="enableAutocomplete"
|
||||
:textarea-value="value"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
:uploads-path="uploadsPath"
|
||||
:enable-preview="enablePreview"
|
||||
class="bordered-box"
|
||||
>
|
||||
<template #textarea>
|
||||
<textarea
|
||||
:id="formFieldId"
|
||||
ref="textarea"
|
||||
:value="value"
|
||||
:name="formFieldName"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
||||
dir="auto"
|
||||
data-supports-quick-actions="false"
|
||||
data-qa-selector="markdown_editor_form_field"
|
||||
:autofocus="autofocus"
|
||||
:aria-label="formFieldAriaLabel"
|
||||
:placeholder="formFieldPlaceholder"
|
||||
@input="updateMarkdownFromMarkdownField"
|
||||
>
|
||||
</textarea>
|
||||
</template>
|
||||
</markdown-field>
|
||||
<div v-else>
|
||||
<content-editor
|
||||
:render-markdown="renderMarkdown"
|
||||
:uploads-path="uploadsPath"
|
||||
:markdown="value"
|
||||
@change="updateMarkdownFromContentEditor"
|
||||
@loading="disableSwitchEditingControl"
|
||||
@loadingSuccess="enableSwitchEditingControl"
|
||||
@loadingError="enableSwitchEditingControl"
|
||||
/>
|
||||
<input
|
||||
:id="formFieldId"
|
||||
:value="value"
|
||||
:name="formFieldName"
|
||||
data-qa-selector="markdown_editor_form_field"
|
||||
type="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -93,3 +93,6 @@ export const confidentialityInfoText = (workspaceType, issuableType) =>
|
|||
: __('at least the Reporter role'),
|
||||
},
|
||||
);
|
||||
|
||||
export const EDITING_MODE_MARKDOWN_FIELD = 'markdownField';
|
||||
export const EDITING_MODE_CONTENT_EDITOR = 'contentEditor';
|
||||
|
|
|
@ -832,6 +832,8 @@ $tabs-holder-z-index: 250;
|
|||
.detail-page-header-actions {
|
||||
.gl-toggle {
|
||||
@include gl-ml-auto;
|
||||
@include gl-rounded-pill;
|
||||
@include gl-w-9;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -844,3 +846,7 @@ $tabs-holder-z-index: 250;
|
|||
@include gl-font-weight-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu li button.gl-toggle:not(.is-checked) {
|
||||
background: $gray-400;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,46 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
@import 'framework/buttons';
|
||||
|
||||
.avatar-image {
|
||||
margin-bottom: $grid-size;
|
||||
|
||||
.avatar {
|
||||
float: none;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-user {
|
||||
.emoji-menu-toggle-button {
|
||||
@include emoji-menu-toggle-button;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.input-md,
|
||||
.input-lg {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-profile-crop {
|
||||
.modal-dialog {
|
||||
width: 380px;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-crop-image-container {
|
||||
height: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-block {
|
||||
padding-left: 0;
|
||||
|
|
|
@ -1,22 +1,3 @@
|
|||
.avatar-image {
|
||||
margin-bottom: $grid-size;
|
||||
|
||||
.avatar {
|
||||
float: none;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-file-name {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.account-well {
|
||||
padding: 10px;
|
||||
background-color: $gray-light;
|
||||
|
@ -29,13 +10,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user-avatar-button {
|
||||
.file-name {
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.subkeys-list {
|
||||
@include basic-list;
|
||||
|
||||
|
@ -113,26 +87,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.modal-profile-crop {
|
||||
.modal-dialog {
|
||||
width: 380px;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-crop-image-container {
|
||||
height: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.crop-controls {
|
||||
padding: 10px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.created-personal-access-token-container {
|
||||
.btn-clipboard {
|
||||
border: 1px solid $border-color;
|
||||
|
@ -247,36 +201,6 @@ table.u2f-registrations {
|
|||
}
|
||||
}
|
||||
|
||||
.edit-user {
|
||||
svg {
|
||||
fill: $gl-text-color-secondary;
|
||||
}
|
||||
|
||||
.form-group > label {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
.form-group > .form-text {
|
||||
font-size: $gl-font-size;
|
||||
}
|
||||
|
||||
.emoji-menu-toggle-button {
|
||||
@include emoji-menu-toggle-button;
|
||||
padding: 6px 10px;
|
||||
|
||||
.no-emoji-placeholder {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.input-md,
|
||||
.input-lg {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-block {
|
||||
color: $gl-text-color-secondary;
|
||||
}
|
||||
|
|
|
@ -217,6 +217,10 @@ class NotifyPreview < ActionMailer::Preview
|
|||
Notify.project_was_exported_email(user, project).message
|
||||
end
|
||||
|
||||
def request_review_merge_request_email
|
||||
Notify.request_review_merge_request_email(user.id, merge_request.id, user.id).message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
|
|
|
@ -760,8 +760,14 @@ module Ci
|
|||
# There is no ActiveRecord relation between Ci::Pipeline and notes
|
||||
# as they are related to a commit sha. This method helps importing
|
||||
# them using the +Gitlab::ImportExport::Project::RelationFactory+ class.
|
||||
def notes=(notes)
|
||||
notes.each do |note|
|
||||
def notes=(notes_to_save)
|
||||
notes_to_save.reject! do |note_to_save|
|
||||
notes.any? do |note|
|
||||
[note_to_save.note, note_to_save.created_at.to_i] == [note.note, note.created_at.to_i]
|
||||
end
|
||||
end
|
||||
|
||||
notes_to_save.each do |note|
|
||||
note[:id] = nil
|
||||
note[:commit_id] = sha
|
||||
note[:noteable_id] = self['id']
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
%p
|
||||
#{sanitize_name(@updated_by.name)} requested a new review on #{merge_request_reference_link(@merge_request)}.
|
||||
= html_escape(s_('Notify|%{name} requested a new review on %{mr_link}.')) % {name: sanitize_name(@updated_by.name), mr_link: merge_request_reference_link(@merge_request).html_safe}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- breadcrumb_title s_("Profiles|Edit Profile")
|
||||
- page_title s_("Profiles|Edit Profile")
|
||||
- add_page_specific_style 'page_bundles/profile'
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
|
||||
|
||||
|
@ -27,9 +28,9 @@
|
|||
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
|
||||
= image_tag avatar_icon_for_user(@user, 96), alt: '', class: 'avatar s96'
|
||||
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
|
||||
.gl-my-3
|
||||
.gl-display-flex.gl-align-items-center.gl-my-3
|
||||
%button.gl-button.btn.btn-default.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
|
||||
%span.avatar-file-name.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
|
||||
%span.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
|
||||
= f.file_field :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
|
||||
.gl-text-gray-500= s_("Profiles|The maximum file size allowed is 200KB.")
|
||||
- if @user.avatar?
|
||||
|
@ -152,7 +153,7 @@
|
|||
.modal-body
|
||||
.profile-crop-image-container
|
||||
%img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
|
||||
.crop-controls
|
||||
.gl-text-center.gl-mt-4
|
||||
.btn-group
|
||||
%button.btn.gl-button.btn-default{ data: { method: 'zoom', option: '-0.1' } }
|
||||
%span
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_rules_changes_compare
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90968
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366412
|
||||
milestone: '15.3'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: true
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillInternalOnNotes < Gitlab::Database::Migration[2.0]
|
||||
MIGRATION = 'BackfillInternalOnNotes'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
TABLE = :notes
|
||||
BATCH_SIZE = 2000
|
||||
SUB_BATCH_SIZE = 10
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
4a975867dc0539049902229521b4d94f940817ffd9196810856c8eb962c57e62
|
|
@ -351,6 +351,22 @@ scope block takes an argument). Preloading instance dependent scopes is not
|
|||
supported.
|
||||
```
|
||||
|
||||
### Primary key
|
||||
|
||||
Primary key must include the partitioning key column to partition the table.
|
||||
|
||||
We first create a unique index including the `(id, partition_id)`.
|
||||
Then, we drop the primary key constraint and use the new index created to set
|
||||
the new primary key constraint.
|
||||
|
||||
We must set the primary key explicitly as `ActiveRecord` does not support composite primary keys.
|
||||
|
||||
```ruby
|
||||
class Model
|
||||
self.primary_key = 'id'
|
||||
end
|
||||
```
|
||||
|
||||
### Foreign keys
|
||||
|
||||
Foreign keys must reference columns that either are a primary key or form a
|
||||
|
|
|
@ -3343,7 +3343,8 @@ In this example, both jobs have the same behavior.
|
|||
|
||||
##### `rules:changes:compare_to`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Enabled by default.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Enabled by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366412) in GitLab 15.5. Feature flag `ci_rules_changes_compare` removed.
|
||||
|
||||
Use `rules:changes:compare_to` to specify which ref to compare against for changes to the files
|
||||
listed under [`rules:changes:paths`](#ruleschangespaths).
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# This syncs the data to `internal` from `confidential` as we rename the column.
|
||||
class BackfillInternalOnNotes < BatchedMigrationJob
|
||||
scope_to -> (relation) { relation.where(confidential: true) }
|
||||
|
||||
def perform
|
||||
each_sub_batch(operation_name: :update_all) do |sub_batch|
|
||||
sub_batch.update_all(internal: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -41,7 +41,6 @@ module Gitlab
|
|||
|
||||
def find_modified_paths(pipeline)
|
||||
return unless pipeline
|
||||
return pipeline.modified_paths unless ::Feature.enabled?(:ci_rules_changes_compare, pipeline.project)
|
||||
|
||||
compare_to_sha = find_compare_to_sha(pipeline)
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ pages:
|
|||
- apk update && apk add doxygen
|
||||
- doxygen doxygen/Doxyfile
|
||||
- mv doxygen/documentation/html/ public/
|
||||
environment: production
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# Full project: https://gitlab.com/pages/plain-html
|
||||
pages:
|
||||
stage: deploy
|
||||
environment: production
|
||||
script:
|
||||
- mkdir .public
|
||||
- cp -r ./* .public
|
||||
|
|
|
@ -11,6 +11,7 @@ pages:
|
|||
- npm install hexo-cli -g
|
||||
- test -e package.json && npm install
|
||||
- hexo generate
|
||||
environment: production
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
|
|
|
@ -21,6 +21,7 @@ test:
|
|||
|
||||
pages:
|
||||
stage: deploy
|
||||
environment: production
|
||||
script:
|
||||
- pip install hyde
|
||||
- hyde gen -d public
|
||||
|
|
|
@ -29,6 +29,7 @@ before_script:
|
|||
|
||||
# This build job produced the output directory of your site
|
||||
pages:
|
||||
environment: production
|
||||
script:
|
||||
- jbake . public
|
||||
artifacts:
|
||||
|
|
|
@ -46,10 +46,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def instrumentation_object
|
||||
@instrumentation_object ||= instrumentation_class.constantize.new(
|
||||
time_frame: definition.time_frame,
|
||||
options: definition.attributes[:options]
|
||||
)
|
||||
@instrumentation_object ||= instrumentation_class.constantize.new(definition.attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,9 +23,9 @@ module Gitlab
|
|||
attr_reader :metric_available
|
||||
end
|
||||
|
||||
def initialize(time_frame:, options: {})
|
||||
@time_frame = time_frame
|
||||
@options = options
|
||||
def initialize(metric_definition)
|
||||
@time_frame = metric_definition.fetch(:time_frame)
|
||||
@options = metric_definition.fetch(:options, {})
|
||||
end
|
||||
|
||||
def instrumentation
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
class CountBulkImportsEntitiesMetric < DatabaseMetric
|
||||
operation :count
|
||||
|
||||
def initialize(time_frame:, options: {})
|
||||
def initialize(metric_definition)
|
||||
super
|
||||
|
||||
if source_type.present? && !source_type.in?(allowed_source_types)
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
class CountImportedProjectsMetric < DatabaseMetric
|
||||
operation :count
|
||||
|
||||
def initialize(time_frame:, options: {})
|
||||
def initialize(metric_definition)
|
||||
super
|
||||
|
||||
raise ArgumentError, "import_type options attribute is required" unless import_type.present?
|
||||
|
|
|
@ -28,9 +28,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def initialize(time_frame: 'none', options: {})
|
||||
@time_frame = time_frame
|
||||
@options = options
|
||||
def initialize(metric_definition)
|
||||
super(metric_definition.reverse_merge(time_frame: 'none'))
|
||||
end
|
||||
|
||||
def value
|
||||
|
|
|
@ -12,7 +12,7 @@ module Gitlab
|
|||
# events:
|
||||
# - g_analytics_valuestream
|
||||
# end
|
||||
def initialize(time_frame:, options: {})
|
||||
def initialize(metric_definition)
|
||||
super
|
||||
|
||||
raise ArgumentError, "options events are required" unless metric_events.present?
|
||||
|
|
|
@ -19,7 +19,7 @@ module Gitlab
|
|||
USAGE_PREFIX = "USAGE_"
|
||||
OPTIONS_PREFIX_KEY = :prefix
|
||||
|
||||
def initialize(time_frame:, options: {})
|
||||
def initialize(metric_definition)
|
||||
super
|
||||
|
||||
raise ArgumentError, "'event' option is required" unless metric_event.present?
|
||||
|
|
|
@ -27162,6 +27162,9 @@ msgstr ""
|
|||
msgid "Notify|%{mr_highlight}Merge request%{highlight_end} %{mr_link} %{reviewer_highlight}was unapproved by%{highlight_end} %{reviewer_avatar} %{reviewer_link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify|%{name} requested a new review on %{mr_link}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify|%{paragraph_start}Hi %{name}!%{paragraph_end} %{paragraph_start}A new public key was added to your account:%{paragraph_end} %{paragraph_start}title: %{key_title}%{paragraph_end} %{paragraph_start}If this key was added in error, you can remove it under %{removal_link}%{paragraph_end}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34176,6 +34179,9 @@ msgstr ""
|
|||
msgid "Revoked personal access token %{personal_access_token_name}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rich text"
|
||||
msgstr ""
|
||||
|
||||
msgid "RightSidebar|Copy email address"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44969,12 +44975,6 @@ msgstr ""
|
|||
msgid "Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wiki Page|Rich text"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wiki Page|Source"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wiki page"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -22,8 +22,8 @@ module QA
|
|||
element :file_upload_field
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue' do
|
||||
element :wiki_hidden_content
|
||||
base.view 'app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue' do
|
||||
element :markdown_editor_form_field
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -47,7 +47,7 @@ module QA
|
|||
end
|
||||
|
||||
QA::Support::Retrier.retry_on_exception do
|
||||
source = find_element(:wiki_hidden_content, visible: false)
|
||||
source = find_element(:markdown_editor_form_field, visible: false)
|
||||
source.value =~ %r{uploads/.*#{::File.basename(image_path)}}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,9 +11,12 @@ module QA
|
|||
|
||||
base.view 'app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue' do
|
||||
element :wiki_title_textbox
|
||||
element :wiki_content_textarea
|
||||
element :wiki_message_textbox
|
||||
element :wiki_submit_button
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue' do
|
||||
element :markdown_editor_form_field
|
||||
element :editing_mode_button
|
||||
end
|
||||
|
||||
|
@ -27,7 +30,7 @@ module QA
|
|||
end
|
||||
|
||||
def set_content(content)
|
||||
fill_element(:wiki_content_textarea, content)
|
||||
fill_element(:markdown_editor_form_field, content)
|
||||
end
|
||||
|
||||
def set_message(message)
|
||||
|
|
|
@ -50,15 +50,11 @@ RSpec.describe 'User manages subscription', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
click_button 'Toggle dropdown'
|
||||
|
||||
expect(page).to have_selector('.gl-toggle.is-checked')
|
||||
find('[data-testid="notifications-toggle"] .gl-toggle').click
|
||||
|
||||
wait_for_requests
|
||||
|
||||
click_button 'Toggle dropdown'
|
||||
|
||||
expect(page).to have_selector('.gl-toggle:not(.is-checked)')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlAlert, GlButton, GlFormInput, GlFormGroup, GlSegmentedControl } from '@gitlab/ui';
|
||||
import { GlAlert, GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ContentEditor from '~/content_editor/components/content_editor.vue';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import {
|
||||
CONTENT_EDITOR_LOADED_ACTION,
|
||||
SAVED_USING_CONTENT_EDITOR_ACTION,
|
||||
|
@ -18,8 +15,6 @@ import {
|
|||
WIKI_FORMAT_UPDATED_ACTION,
|
||||
} from '~/pages/shared/wikis/constants';
|
||||
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
|
||||
jest.mock('~/emoji');
|
||||
|
||||
describe('WikiForm', () => {
|
||||
|
@ -30,16 +25,12 @@ describe('WikiForm', () => {
|
|||
const findForm = () => wrapper.find('form');
|
||||
const findTitle = () => wrapper.find('#wiki_title');
|
||||
const findFormat = () => wrapper.find('#wiki_format');
|
||||
const findContent = () => wrapper.find('#wiki_content');
|
||||
const findMessage = () => wrapper.find('#wiki_message');
|
||||
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
|
||||
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
|
||||
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
|
||||
const findToggleEditingModeButton = () => wrapper.findByTestId('toggle-editing-mode-button');
|
||||
const findTitleHelpLink = () => wrapper.findByText('Learn more.');
|
||||
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
|
||||
const findContentEditor = () => wrapper.findComponent(ContentEditor);
|
||||
const findClassicEditor = () => wrapper.findComponent(MarkdownField);
|
||||
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
|
||||
|
||||
const setFormat = (value) => {
|
||||
const format = findFormat();
|
||||
|
@ -103,11 +94,8 @@ describe('WikiForm', () => {
|
|||
},
|
||||
},
|
||||
stubs: {
|
||||
MarkdownField,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlSegmentedControl,
|
||||
LocalStorageSync: stubComponent(LocalStorageSync),
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
},
|
||||
|
@ -126,6 +114,22 @@ describe('WikiForm', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
it('displays markdown editor', () => {
|
||||
createWrapper({ persisted: true });
|
||||
|
||||
expect(findMarkdownEditor().props()).toEqual(
|
||||
expect.objectContaining({
|
||||
value: pageInfoPersisted.content,
|
||||
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
|
||||
markdownDocsPath: pageInfoPersisted.markdownHelpPath,
|
||||
uploadsPath: pageInfoPersisted.uploadsPath,
|
||||
autofocus: pageInfoPersisted.persisted,
|
||||
formFieldId: 'wiki_content',
|
||||
formFieldName: 'wiki[content]',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
title | persisted | message
|
||||
${'my page'} | ${false} | ${'Create my page'}
|
||||
|
@ -154,7 +158,7 @@ describe('WikiForm', () => {
|
|||
it('does not trim page content by default', () => {
|
||||
createWrapper({ persisted: true });
|
||||
|
||||
expect(findContent().element.value).toBe(' My page content ');
|
||||
expect(findMarkdownEditor().props().value).toBe(' My page content ');
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -168,7 +172,9 @@ describe('WikiForm', () => {
|
|||
|
||||
await setFormat(format);
|
||||
|
||||
expect(findClassicEditor().props('enablePreview')).toBe(enabled);
|
||||
nextTick();
|
||||
|
||||
expect(findMarkdownEditor().props('enablePreview')).toBe(enabled);
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -219,9 +225,7 @@ describe('WikiForm', () => {
|
|||
beforeEach(async () => {
|
||||
createWrapper({ mountFn: mount, persisted: true });
|
||||
|
||||
const input = findContent();
|
||||
|
||||
await input.setValue(' Lorem ipsum dolar sit! ');
|
||||
await findMarkdownEditor().vm.$emit('input', ' Lorem ipsum dolar sit! ');
|
||||
});
|
||||
|
||||
it('sets before unload warning', () => {
|
||||
|
@ -245,7 +249,7 @@ describe('WikiForm', () => {
|
|||
});
|
||||
|
||||
it('does not trim page content', () => {
|
||||
expect(findContent().element.value).toBe(' Lorem ipsum dolar sit! ');
|
||||
expect(findMarkdownEditor().props().value).toBe(' Lorem ipsum dolar sit! ');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -264,7 +268,7 @@ describe('WikiForm', () => {
|
|||
createWrapper({ mountFn: mount });
|
||||
|
||||
await findTitle().setValue(title);
|
||||
await findContent().setValue(content);
|
||||
await findMarkdownEditor().vm.$emit('input', content);
|
||||
|
||||
expect(findSubmitButton().props().disabled).toBe(disabledAttr);
|
||||
},
|
||||
|
@ -296,155 +300,31 @@ describe('WikiForm', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('toggle editing mode control', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ mountFn: mount });
|
||||
});
|
||||
|
||||
it.each`
|
||||
format | exists | action
|
||||
${'markdown'} | ${true} | ${'displays'}
|
||||
${'rdoc'} | ${false} | ${'hides'}
|
||||
${'asciidoc'} | ${false} | ${'hides'}
|
||||
${'org'} | ${false} | ${'hides'}
|
||||
`('$action toggle editing mode button when format is $format', async ({ format, exists }) => {
|
||||
await setFormat(format);
|
||||
|
||||
expect(findToggleEditingModeButton().exists()).toBe(exists);
|
||||
});
|
||||
|
||||
describe('when content editor is not active', () => {
|
||||
it('displays "Source" label in the toggle editing mode button', () => {
|
||||
expect(findToggleEditingModeButton().props().checked).toBe('source');
|
||||
});
|
||||
|
||||
describe('when clicking the toggle editing mode button', () => {
|
||||
beforeEach(async () => {
|
||||
await findToggleEditingModeButton().vm.$emit('input', 'richText');
|
||||
});
|
||||
|
||||
it('hides the classic editor', () => {
|
||||
expect(findClassicEditor().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the content editor', () => {
|
||||
expect(findContentEditor().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown editor type persistance', () => {
|
||||
it('loads content editor by default if it is persisted in local storage', async () => {
|
||||
expect(findClassicEditor().exists()).toBe(true);
|
||||
expect(findContentEditor().exists()).toBe(false);
|
||||
|
||||
// enable content editor
|
||||
await findLocalStorageSync().vm.$emit('input', 'richText');
|
||||
|
||||
expect(findContentEditor().exists()).toBe(true);
|
||||
expect(findClassicEditor().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when content editor is active', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
findToggleEditingModeButton().vm.$emit('input', 'richText');
|
||||
});
|
||||
|
||||
it('displays "Edit Rich" label in the toggle editing mode button', () => {
|
||||
expect(findToggleEditingModeButton().props().checked).toBe('richText');
|
||||
});
|
||||
|
||||
describe('when clicking the toggle editing mode button', () => {
|
||||
beforeEach(async () => {
|
||||
await findToggleEditingModeButton().vm.$emit('input', 'source');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('hides the content editor', () => {
|
||||
expect(findContentEditor().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('displays the classic editor', () => {
|
||||
expect(findClassicEditor().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when content editor is loading', () => {
|
||||
beforeEach(async () => {
|
||||
findContentEditor().vm.$emit('loading');
|
||||
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('disables toggle editing mode button', () => {
|
||||
expect(findToggleEditingModeButton().attributes().disabled).toBe('true');
|
||||
});
|
||||
|
||||
describe('when content editor loads successfully', () => {
|
||||
it('enables toggle editing mode button', async () => {
|
||||
findContentEditor().vm.$emit('loadingSuccess');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findToggleEditingModeButton().attributes().disabled).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when content editor fails to load', () => {
|
||||
it('enables toggle editing mode button', async () => {
|
||||
findContentEditor().vm.$emit('loadingError');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findToggleEditingModeButton().attributes().disabled).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('wiki content editor', () => {
|
||||
describe('clicking "Edit rich text": editor fails to load', () => {
|
||||
beforeEach(async () => {
|
||||
format | enabled | action
|
||||
${'markdown'} | ${true} | ${'enables'}
|
||||
${'rdoc'} | ${false} | ${'disables'}
|
||||
${'asciidoc'} | ${false} | ${'disables'}
|
||||
${'org'} | ${false} | ${'disables'}
|
||||
`('$action content editor when format is $format', async ({ format, enabled }) => {
|
||||
createWrapper({ mountFn: mount });
|
||||
mock.onPost(/preview-markdown/).reply(400);
|
||||
|
||||
await findToggleEditingModeButton().vm.$emit('input', 'richText');
|
||||
setFormat(format);
|
||||
|
||||
// try waiting for content editor to load (but it will never actually load)
|
||||
await waitForPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(findMarkdownEditor().props().enableContentEditor).toBe(enabled);
|
||||
});
|
||||
|
||||
it('disables the submit button', () => {
|
||||
expect(findSubmitButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
describe('toggling editing modes to the classic editor', () => {
|
||||
beforeEach(() => {
|
||||
return findToggleEditingModeButton().vm.$emit('input', 'source');
|
||||
});
|
||||
|
||||
it('switches to classic editor', () => {
|
||||
expect(findContentEditor().exists()).toBe(false);
|
||||
expect(findClassicEditor().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clicking "Edit rich text": editor loads successfully', () => {
|
||||
describe('when markdown editor activates the content editor', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper({ persisted: true, mountFn: mount });
|
||||
createWrapper({ mountFn: mount, persisted: true });
|
||||
|
||||
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
|
||||
|
||||
await findToggleEditingModeButton().vm.$emit('input', 'richText');
|
||||
await waitForPromises();
|
||||
await findMarkdownEditor().vm.$emit('contentEditor');
|
||||
});
|
||||
|
||||
it('shows the rich text editor when loading finishes', async () => {
|
||||
expect(findContentEditor().exists()).toBe(true);
|
||||
it('disables the format dropdown', () => {
|
||||
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
it('sends tracking event when editor loads', async () => {
|
||||
|
@ -453,35 +333,20 @@ describe('WikiForm', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('disables the format dropdown', () => {
|
||||
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
describe('when wiki content is updated', () => {
|
||||
describe('when triggering form submit', () => {
|
||||
const updatedMarkdown = 'hello **world**';
|
||||
|
||||
beforeEach(() => {
|
||||
findContentEditor().vm.$emit('change', {
|
||||
empty: false,
|
||||
changed: true,
|
||||
markdown: updatedMarkdown,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets before unload warning', () => {
|
||||
const e = dispatchBeforeUnload();
|
||||
expect(e.preventDefault).toHaveBeenCalledTimes(1);
|
||||
beforeEach(async () => {
|
||||
findMarkdownEditor().vm.$emit('input', updatedMarkdown);
|
||||
await triggerFormSubmit();
|
||||
});
|
||||
|
||||
it('unsets before unload warning on form submit', async () => {
|
||||
await triggerFormSubmit();
|
||||
|
||||
const e = dispatchBeforeUnload();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('triggers tracking events on form submit', async () => {
|
||||
await triggerFormSubmit();
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
|
||||
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
|
||||
});
|
||||
|
@ -495,11 +360,6 @@ describe('WikiForm', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sets content field to the content editor updated markdown', async () => {
|
||||
expect(findContent().element.value).toBe(updatedMarkdown);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
import { GlSegmentedControl } from '@gitlab/ui';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '~/vue_shared/constants';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import ContentEditor from '~/content_editor/components/content_editor.vue';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
|
||||
jest.mock('~/emoji');
|
||||
|
||||
describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||
let wrapper;
|
||||
const value = 'test markdown';
|
||||
const renderMarkdownPath = '/api/markdown';
|
||||
const markdownDocsPath = '/help/markdown';
|
||||
const uploadsPath = '/uploads';
|
||||
const enableAutocomplete = true;
|
||||
const enablePreview = false;
|
||||
const formFieldId = 'markdown_field';
|
||||
const formFieldName = 'form[markdown_field]';
|
||||
const formFieldPlaceholder = 'Write some markdown';
|
||||
const formFieldAriaLabel = 'Edit your content';
|
||||
let mock;
|
||||
|
||||
const buildWrapper = (propsData = {}) => {
|
||||
wrapper = mountExtended(MarkdownEditor, {
|
||||
propsData: {
|
||||
value,
|
||||
renderMarkdownPath,
|
||||
markdownDocsPath,
|
||||
uploadsPath,
|
||||
enableAutocomplete,
|
||||
enablePreview,
|
||||
formFieldId,
|
||||
formFieldName,
|
||||
formFieldPlaceholder,
|
||||
formFieldAriaLabel,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
|
||||
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
|
||||
const findTextarea = () => wrapper.find('textarea');
|
||||
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
|
||||
const findContentEditor = () => wrapper.findComponent(ContentEditor);
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('displays markdown field by default', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(findMarkdownField().props()).toEqual(
|
||||
expect.objectContaining({
|
||||
markdownPreviewPath: renderMarkdownPath,
|
||||
canAttachFile: true,
|
||||
enableAutocomplete,
|
||||
textareaValue: value,
|
||||
markdownDocsPath,
|
||||
uploadsPath,
|
||||
enablePreview,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders markdown field textarea', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(findTextarea().attributes()).toEqual(
|
||||
expect.objectContaining({
|
||||
id: formFieldId,
|
||||
name: formFieldName,
|
||||
placeholder: formFieldPlaceholder,
|
||||
'aria-label': formFieldAriaLabel,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(findTextarea().element.value).toBe(value);
|
||||
});
|
||||
|
||||
it('renders switch segmented control', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(findSegmentedControl().props()).toEqual({
|
||||
checked: EDITING_MODE_MARKDOWN_FIELD,
|
||||
options: [
|
||||
{
|
||||
text: expect.any(String),
|
||||
value: EDITING_MODE_MARKDOWN_FIELD,
|
||||
},
|
||||
{
|
||||
text: expect.any(String),
|
||||
value: EDITING_MODE_CONTENT_EDITOR,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
editingMode
|
||||
${EDITING_MODE_CONTENT_EDITOR}
|
||||
${EDITING_MODE_MARKDOWN_FIELD}
|
||||
`('when segmented control emits change event with $editingMode value', ({ editingMode }) => {
|
||||
it(`emits ${editingMode} event`, () => {
|
||||
buildWrapper();
|
||||
|
||||
findSegmentedControl().vm.$emit('change', editingMode);
|
||||
|
||||
expect(wrapper.emitted(editingMode)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when editingMode is ${EDITING_MODE_MARKDOWN_FIELD}`, () => {
|
||||
it('emits input event when markdown field textarea changes', async () => {
|
||||
buildWrapper();
|
||||
const newValue = 'new value';
|
||||
|
||||
await findTextarea().setValue(newValue);
|
||||
|
||||
expect(wrapper.emitted('input')).toEqual([[newValue]]);
|
||||
});
|
||||
|
||||
describe(`when segmented control triggers input event with ${EDITING_MODE_CONTENT_EDITOR} value`, () => {
|
||||
beforeEach(() => {
|
||||
buildWrapper();
|
||||
findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
|
||||
});
|
||||
|
||||
it('displays the content editor', () => {
|
||||
expect(findContentEditor().props()).toEqual(
|
||||
expect.objectContaining({
|
||||
renderMarkdown: expect.any(Function),
|
||||
uploadsPath,
|
||||
markdown: value,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('adds hidden field with current markdown', () => {
|
||||
const hiddenField = wrapper.find(`#${formFieldId}`);
|
||||
|
||||
expect(hiddenField.attributes()).toEqual(
|
||||
expect.objectContaining({
|
||||
id: formFieldId,
|
||||
name: formFieldName,
|
||||
}),
|
||||
);
|
||||
expect(hiddenField.element.value).toBe(value);
|
||||
});
|
||||
|
||||
it('hides the markdown field', () => {
|
||||
expect(findMarkdownField().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('updates localStorage value', () => {
|
||||
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_CONTENT_EDITOR);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
|
||||
beforeEach(() => {
|
||||
buildWrapper();
|
||||
findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
|
||||
});
|
||||
|
||||
it('emits input event when content editor emits change event', async () => {
|
||||
const newValue = 'new value';
|
||||
|
||||
await findContentEditor().vm.$emit('change', { markdown: newValue });
|
||||
|
||||
expect(wrapper.emitted('input')).toEqual([[newValue]]);
|
||||
});
|
||||
|
||||
describe(`when segmented control triggers input event with ${EDITING_MODE_MARKDOWN_FIELD} value`, () => {
|
||||
beforeEach(() => {
|
||||
findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
|
||||
});
|
||||
|
||||
it('hides the content editor', () => {
|
||||
expect(findContentEditor().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the markdown field', () => {
|
||||
expect(findMarkdownField().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('updates localStorage value', () => {
|
||||
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when content editor emits loading event', () => {
|
||||
beforeEach(() => {
|
||||
findContentEditor().vm.$emit('loading');
|
||||
});
|
||||
|
||||
it('disables switch editing mode control', () => {
|
||||
// This is the only way that I found to check the segmented control is disabled
|
||||
expect(findSegmentedControl().find('input[disabled]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
event
|
||||
${'loadingSuccess'}
|
||||
${'loadingError'}
|
||||
`('when content editor emits $event event', ({ event }) => {
|
||||
beforeEach(() => {
|
||||
findContentEditor().vm.$emit(event);
|
||||
});
|
||||
it('enables the switch editing mode control', () => {
|
||||
expect(findSegmentedControl().find('input[disabled]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillInternalOnNotes, :migration, schema: 20220920124709 do
|
||||
let(:notes_table) { table(:notes) }
|
||||
|
||||
let!(:confidential_note) { notes_table.create!(id: 1, confidential: true, internal: false) }
|
||||
let!(:non_confidential_note) { notes_table.create!(id: 2, confidential: false, internal: false) }
|
||||
|
||||
describe '#perform' do
|
||||
subject(:perform) do
|
||||
described_class.new(
|
||||
start_id: 1,
|
||||
end_id: 2,
|
||||
batch_table: :notes,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 1,
|
||||
pause_ms: 0,
|
||||
connection: ApplicationRecord.connection
|
||||
).perform
|
||||
end
|
||||
|
||||
it 'backfills internal column on notes when confidential' do
|
||||
expect { perform }
|
||||
.to change { confidential_note.reload.internal }.from(false).to(true)
|
||||
.and not_change { non_confidential_note.reload.internal }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -122,19 +122,17 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
|
|||
context 'when compare_to is branch or tag' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:pipeline_ref, :compare_to, :paths, :ff, :result) do
|
||||
'feature_1' | 'master' | ['file1.txt'] | true | true
|
||||
'feature_1' | 'master' | ['README.md'] | true | false
|
||||
'feature_1' | 'master' | ['xyz.md'] | true | false
|
||||
'feature_2' | 'master' | ['file1.txt'] | true | true
|
||||
'feature_2' | 'master' | ['file2.txt'] | true | true
|
||||
'feature_2' | 'feature_1' | ['file1.txt'] | true | false
|
||||
'feature_2' | 'feature_1' | ['file1.txt'] | false | true
|
||||
'feature_2' | 'feature_1' | ['file2.txt'] | true | true
|
||||
'feature_1' | 'tag_1' | ['file1.txt'] | true | false
|
||||
'feature_1' | 'tag_1' | ['file1.txt'] | false | true
|
||||
'feature_1' | 'tag_1' | ['file2.txt'] | true | true
|
||||
'feature_2' | 'tag_1' | ['file2.txt'] | true | true
|
||||
where(:pipeline_ref, :compare_to, :paths, :result) do
|
||||
'feature_1' | 'master' | ['file1.txt'] | true
|
||||
'feature_1' | 'master' | ['README.md'] | false
|
||||
'feature_1' | 'master' | ['xyz.md'] | false
|
||||
'feature_2' | 'master' | ['file1.txt'] | true
|
||||
'feature_2' | 'master' | ['file2.txt'] | true
|
||||
'feature_2' | 'feature_1' | ['file1.txt'] | false
|
||||
'feature_2' | 'feature_1' | ['file2.txt'] | true
|
||||
'feature_1' | 'tag_1' | ['file1.txt'] | false
|
||||
'feature_1' | 'tag_1' | ['file2.txt'] | true
|
||||
'feature_2' | 'tag_1' | ['file2.txt'] | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
@ -144,10 +142,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
|
|||
build(:ci_pipeline, project: project, ref: pipeline_ref, sha: project.commit(pipeline_ref).sha)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(ci_rules_changes_compare: ff)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(result) }
|
||||
end
|
||||
end
|
||||
|
@ -174,14 +168,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
|
|||
::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref'
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the FF ci_rules_changes_compare is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_rules_changes_compare: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,14 +11,21 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git
|
|||
|
||||
let(:expected_value) { 4 }
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', prefix: 'source_code' } }
|
||||
it_behaves_like 'a correct instrumented metric value', {
|
||||
options: { event: 'pushes', prefix: 'source_code' },
|
||||
time_frame: 'all'
|
||||
}
|
||||
|
||||
it 'raises an exception if event option is not present' do
|
||||
expect { described_class.new(prefix: 'source_code') }.to raise_error(ArgumentError)
|
||||
expect do
|
||||
described_class.new(options: { prefix: 'source_code' }, time_frame: 'all')
|
||||
end.to raise_error(ArgumentError, /'event' option is required/)
|
||||
end
|
||||
|
||||
it 'raises an exception if prefix option is not present' do
|
||||
expect { described_class.new(event: 'pushes') }.to raise_error(ArgumentError)
|
||||
expect do
|
||||
described_class.new(options: { event: 'pushes' }, time_frame: 'all')
|
||||
end.to raise_error(ArgumentError, /'prefix' option is required/)
|
||||
end
|
||||
|
||||
describe 'children classes' do
|
||||
|
@ -55,7 +62,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git
|
|||
end
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', {
|
||||
options: { event: 'merge_requests_count', prefix: 'web_ide', include_usage_prefix: false }
|
||||
options: { event: 'merge_requests_count', prefix: 'web_ide', include_usage_prefix: false },
|
||||
time_frame: 'all'
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe BackfillInternalOnNotes, :migration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of issues' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
table_name: :notes,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5516,4 +5516,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#notes=' do
|
||||
context 'when notes already exist' do
|
||||
it 'does not create duplicate notes', :aggregate_failures do
|
||||
time = Time.zone.now
|
||||
pipeline = create(:ci_pipeline, user: user, project: project)
|
||||
note = Note.new(
|
||||
note: 'note',
|
||||
noteable_type: 'Commit',
|
||||
noteable_id: pipeline.id,
|
||||
commit_id: pipeline.id,
|
||||
author_id: user.id,
|
||||
project_id: pipeline.project_id,
|
||||
created_at: time
|
||||
)
|
||||
another_note = note.dup.tap { |note| note.note = 'another note' }
|
||||
|
||||
expect(project.notes.for_commit_id(pipeline.sha).count).to eq(0)
|
||||
|
||||
pipeline.notes = [note]
|
||||
|
||||
expect(project.notes.for_commit_id(pipeline.sha).count).to eq(1)
|
||||
|
||||
pipeline.notes = [note, note, another_note]
|
||||
|
||||
expect(project.notes.for_commit_id(pipeline.sha).count).to eq(2)
|
||||
expect(project.notes.for_commit_id(pipeline.sha).pluck(:note)).to contain_exactly(note.note, another_note.note)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -544,16 +544,6 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref'
|
||||
])
|
||||
end
|
||||
|
||||
context 'when the FF ci_rules_changes_compare is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_rules_changes_compare: false)
|
||||
end
|
||||
|
||||
it 'ignores compare_to and changes is always true' do
|
||||
expect(build_names).to contain_exactly('job1', 'job2')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the compare_to ref exists' do
|
||||
|
@ -563,16 +553,6 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
it 'creates job1 and job2' do
|
||||
expect(build_names).to contain_exactly('job1', 'job2')
|
||||
end
|
||||
|
||||
context 'when the FF ci_rules_changes_compare is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_rules_changes_compare: false)
|
||||
end
|
||||
|
||||
it 'ignores compare_to and changes is always true' do
|
||||
expect(build_names).to contain_exactly('job1', 'job2')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the rule does not match' do
|
||||
|
@ -581,16 +561,6 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
it 'does not create job1' do
|
||||
expect(build_names).to contain_exactly('job2')
|
||||
end
|
||||
|
||||
context 'when the FF ci_rules_changes_compare is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_rules_changes_compare: false)
|
||||
end
|
||||
|
||||
it 'ignores compare_to and changes is always true' do
|
||||
expect(build_names).to contain_exactly('job1', 'job2')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -616,17 +586,6 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
expect(pipeline).to be_created_successfully
|
||||
expect(build_names).to contain_exactly('job1')
|
||||
end
|
||||
|
||||
context 'when the FF ci_rules_changes_compare is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_rules_changes_compare: false)
|
||||
end
|
||||
|
||||
it 'ignores compare_to and changes is always true' do
|
||||
expect(pipeline).to be_created_successfully
|
||||
expect(build_names).to contain_exactly('job1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the rule does not match' do
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/secret"
|
||||
)
|
||||
|
||||
|
|
|
@ -9,12 +9,12 @@ import (
|
|||
"gitlab.com/gitlab-org/labkit/log"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/httptransport"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/transport"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Transport: httptransport.New(),
|
||||
Transport: transport.NewRestrictedTransport(),
|
||||
}
|
||||
|
||||
type Injector struct {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
package httptransport
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitlab.com/gitlab-org/labkit/correlation"
|
||||
"gitlab.com/gitlab-org/labkit/tracing"
|
||||
)
|
||||
|
||||
type Option func(*http.Transport)
|
||||
|
||||
// Defines a http.Transport with values
|
||||
// that are more restrictive than for http.DefaultTransport,
|
||||
// they define shorter TLS Handshake, and more aggressive connection closing
|
||||
// to prevent the connection hanging and reduce FD usage
|
||||
func New(options ...Option) http.RoundTripper {
|
||||
t := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
// To avoid keep around TCP connections to http servers we're done with
|
||||
t.MaxIdleConns = 2
|
||||
|
||||
// A stricter timeout for fetching from external sources that can be slow
|
||||
t.ResponseHeaderTimeout = 30 * time.Second
|
||||
|
||||
for _, option := range options {
|
||||
option(t)
|
||||
}
|
||||
|
||||
return tracing.NewRoundTripper(correlation.NewInstrumentedRoundTripper(t))
|
||||
}
|
||||
|
||||
func WithDisabledCompression() Option {
|
||||
return func(t *http.Transport) {
|
||||
t.DisableCompression = true
|
||||
}
|
||||
}
|
|
@ -21,9 +21,9 @@ import (
|
|||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/httptransport"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/transport"
|
||||
)
|
||||
|
||||
type Resizer struct {
|
||||
|
@ -69,7 +69,7 @@ const (
|
|||
var envInjector = tracing.NewEnvInjector()
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Transport: httptransport.New(),
|
||||
Transport: transport.NewRestrictedTransport(),
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
"gitlab.com/gitlab-org/labkit/mask"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/httptransport"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/transport"
|
||||
)
|
||||
|
||||
type entry struct{ senddata.Prefix }
|
||||
|
@ -44,7 +44,7 @@ var preserveHeaderKeys = map[string]bool{
|
|||
"Pragma": true, // Support for HTTP 1.0 proxies
|
||||
}
|
||||
|
||||
var httpTransport = httptransport.New()
|
||||
var httpTransport = transport.NewRestrictedTransport()
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Transport: httpTransport,
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitlab.com/gitlab-org/labkit/correlation"
|
||||
"gitlab.com/gitlab-org/labkit/tracing"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/version"
|
||||
)
|
||||
|
||||
// Creates a new default transport that has Workhorse's User-Agent header set.
|
||||
func NewDefaultTransport() http.RoundTripper {
|
||||
return &DefaultTransport{Next: http.DefaultTransport}
|
||||
}
|
||||
|
||||
// Defines a http.Transport with values that are more restrictive than for
|
||||
// http.DefaultTransport, they define shorter TLS Handshake, and more
|
||||
// aggressive connection closing to prevent the connection hanging and reduce
|
||||
// FD usage
|
||||
func NewRestrictedTransport(options ...Option) http.RoundTripper {
|
||||
return &DefaultTransport{Next: newRestrictedTransport(options...)}
|
||||
}
|
||||
|
||||
type DefaultTransport struct {
|
||||
Next http.RoundTripper
|
||||
}
|
||||
|
||||
func (t DefaultTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", version.GetUserAgent())
|
||||
|
||||
return t.Next.RoundTrip(req)
|
||||
}
|
||||
|
||||
type Option func(*http.Transport)
|
||||
|
||||
func WithDisabledCompression() Option {
|
||||
return func(t *http.Transport) {
|
||||
t.DisableCompression = true
|
||||
}
|
||||
}
|
||||
|
||||
func newRestrictedTransport(options ...Option) http.RoundTripper {
|
||||
t := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
// To avoid keep around TCP connections to http servers we're done with
|
||||
t.MaxIdleConns = 2
|
||||
|
||||
// A stricter timeout for fetching from external sources that can be slow
|
||||
t.ResponseHeaderTimeout = 30 * time.Second
|
||||
|
||||
for _, option := range options {
|
||||
option(t)
|
||||
}
|
||||
|
||||
return tracing.NewRoundTripper(correlation.NewInstrumentedRoundTripper(t))
|
||||
}
|
|
@ -8,11 +8,11 @@ import (
|
|||
|
||||
"gitlab.com/gitlab-org/labkit/mask"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/httptransport"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/transport"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Transport: httptransport.New(),
|
||||
Transport: transport.NewRestrictedTransport(),
|
||||
}
|
||||
|
||||
// Object represents an object on a S3 compatible Object Store service.
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package version
|
||||
|
||||
import "fmt"
|
||||
|
||||
var version = "unknown"
|
||||
var build = "unknown"
|
||||
var schema = "gitlab-workhorse (%s)-(%s)"
|
||||
|
||||
func SetVersion(v, b string) {
|
||||
version = v
|
||||
build = b
|
||||
}
|
||||
|
||||
func GetUserAgent() string {
|
||||
return GetApplicationVersion()
|
||||
}
|
||||
|
||||
func GetApplicationVersion() string {
|
||||
return fmt.Sprintf(schema, version, build)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
require.Equal(t, GetApplicationVersion(), "gitlab-workhorse (unknown)-(unknown)")
|
||||
|
||||
SetVersion("15.3", "123.123")
|
||||
|
||||
require.Equal(t, GetApplicationVersion(), "gitlab-workhorse (15.3)-(123.123)")
|
||||
|
||||
SetVersion("", "123.123")
|
||||
|
||||
require.Equal(t, GetApplicationVersion(), "gitlab-workhorse ()-(123.123)")
|
||||
}
|
|
@ -8,16 +8,16 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/httptransport"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/httprs"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/transport"
|
||||
|
||||
zip "gitlab.com/gitlab-org/golang-archive-zip"
|
||||
"gitlab.com/gitlab-org/labkit/mask"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Transport: httptransport.New(
|
||||
httptransport.WithDisabledCompression(), // To avoid bugs when serving compressed files from object storage
|
||||
Transport: transport.NewRestrictedTransport(
|
||||
transport.WithDisabledCompression(), // To avoid bugs when serving compressed files from object storage
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/redis"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/secret"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upstream"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/version"
|
||||
)
|
||||
|
||||
// Version is the current version of GitLab Workhorse
|
||||
|
@ -55,8 +56,10 @@ func main() {
|
|||
os.Exit(2)
|
||||
}
|
||||
|
||||
version.SetVersion(Version, BuildTime)
|
||||
|
||||
if boot.printVersion {
|
||||
fmt.Printf("gitlab-workhorse %s-%s\n", Version, BuildTime)
|
||||
fmt.Println(version.GetApplicationVersion())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue