Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-07 18:07:59 +00:00
parent 807c4eae46
commit 3ff3d897d6
97 changed files with 1161 additions and 271 deletions

View File

@ -8,7 +8,7 @@ include:
- local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml
- local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml
- project: gitlab-org/quality/pipeline-common
ref: 2.1.1
ref: 2.2.0
file:
- /ci/base.gitlab-ci.yml
- /ci/allure-report.yml

View File

@ -1,6 +1,6 @@
include:
- project: gitlab-org/quality/pipeline-common
ref: 2.1.1
ref: 2.2.0
file:
- /ci/base.gitlab-ci.yml
- /ci/allure-report.yml

View File

@ -33,9 +33,6 @@ gem 'sprockets', '~> 3.7.0'
gem 'view_component', '~> 2.74.1'
# Default values for AR models
gem 'default_value_for', '~> 3.4.0'
# Supported DBs
gem 'pg', '~> 1.4.5'

View File

@ -104,7 +104,6 @@
{"name":"deckar01-task_list","version":"2.3.2","platform":"ruby","checksum":"5a19092548d24309d8b2c2704d64cdc08a4a615823c9a722f4142edec1de8805"},
{"name":"declarative","version":"0.0.20","platform":"ruby","checksum":"8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9"},
{"name":"declarative_policy","version":"1.1.0","platform":"ruby","checksum":"9af4cf299ade03f2bbf63908f2ce6a117d132fc714c39a128596667fb13331cb"},
{"name":"default_value_for","version":"3.4.0","platform":"ruby","checksum":"35d2dc51675a6bedfa875778628d44b823e0d7336da9432519477174ebb0f40f"},
{"name":"deprecation_toolkit","version":"1.5.1","platform":"ruby","checksum":"a8a1ab1a19ae40ea12560b65010e099f3459ebde390b76621ef0c21c516a04ba"},
{"name":"derailed_benchmarks","version":"2.1.2","platform":"ruby","checksum":"eaadc6206ceeb5538ff8f5e04a0023d54ebdd95d04f33e8960fb95a5f189a14f"},
{"name":"descendants_tracker","version":"0.0.4","platform":"ruby","checksum":"e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897"},

View File

@ -339,8 +339,6 @@ GEM
html-pipeline
declarative (0.0.20)
declarative_policy (1.1.0)
default_value_for (3.4.0)
activerecord (>= 3.2.0, < 7.0)
deprecation_toolkit (1.5.1)
activesupport (>= 4.2)
derailed_benchmarks (2.1.2)
@ -1645,7 +1643,6 @@ DEPENDENCIES
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.3.2)
declarative_policy (~> 1.1.0)
default_value_for (~> 3.4.0)
deprecation_toolkit (~> 1.5.1)
derailed_benchmarks
device_detector

View File

@ -2,7 +2,7 @@
import { GlButton, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { fetchPolicies } from '~/lib/graphql';
import { HTTP_STATUS_FORBIDDEN } from '~/lib/utils/http_status';
import { typeSet, i18n, tabIndices } from '../constants';

View File

@ -1,5 +1,5 @@
import produce from 'immer';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { DELETE_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR } from './error_messages';

View File

@ -7,6 +7,7 @@ import Heading from '../../extensions/heading';
import Audio from '../../extensions/audio';
import Video from '../../extensions/video';
import Image from '../../extensions/image';
import DrawioDiagram from '../../extensions/drawio_diagram';
import ToolbarButton from '../toolbar_button.vue';
import BubbleMenu from './bubble_menu.vue';
@ -26,7 +27,7 @@ export default {
if (from === to) return false;
const includes = [Paragraph.name, Heading.name];
const excludes = [Image.name, Audio.name, Video.name];
const excludes = [Image.name, Audio.name, Video.name, DrawioDiagram.name];
return (
includes.some((type) => editor.isActive(type)) &&

View File

@ -11,23 +11,26 @@ import {
} from '@gitlab/ui';
import { __ } from '~/locale';
import Audio from '../../extensions/audio';
import DrawioDiagram from '../../extensions/drawio_diagram';
import Image from '../../extensions/image';
import Video from '../../extensions/video';
import EditorStateObserver from '../editor_state_observer.vue';
import { acceptedMimes } from '../../services/upload_helpers';
import BubbleMenu from './bubble_menu.vue';
const MEDIA_TYPES = [Audio.name, Image.name, Video.name];
const MEDIA_TYPES = [Audio.name, Image.name, Video.name, DrawioDiagram.name];
export default {
i18n: {
copySourceLabels: {
[Audio.name]: __('Copy audio URL'),
[DrawioDiagram.name]: __('Copy diagram URL'),
[Image.name]: __('Copy image URL'),
[Video.name]: __('Copy video URL'),
},
editLabels: {
[Audio.name]: __('Edit audio description'),
[DrawioDiagram.name]: __('Edit diagram description'),
[Image.name]: __('Edit image description'),
[Video.name]: __('Edit video description'),
},
@ -38,6 +41,7 @@ export default {
},
deleteLabels: {
[Audio.name]: __('Delete audio'),
[DrawioDiagram.name]: __('Delete diagram'),
[Image.name]: __('Delete image'),
[Video.name]: __('Delete video'),
},
@ -86,6 +90,9 @@ export default {
showProgressIndicator() {
return this.isUploading || this.isUpdating;
},
isDrawioDiagram() {
return this.mediaType === DrawioDiagram.name;
},
},
methods: {
shouldShow() {
@ -156,10 +163,21 @@ export default {
this.isUpdating = false;
},
resetMediaInfo() {
this.mediaTitle = null;
this.mediaAlt = null;
this.mediaCanonicalSrc = null;
this.isUploading = false;
},
replaceMedia() {
this.$refs.fileSelector.click();
},
editDiagram() {
this.tiptapEditor.chain().focus().createOrEditDiagram().run();
},
onFileSelect(e) {
this.tiptapEditor
.chain()
@ -191,6 +209,8 @@ export default {
class="gl-shadow gl-rounded-base gl-bg-white"
plugin-key="bubbleMenuMedia"
:should-show="shouldShow"
@show="updateMediaInfoToState"
@hidden="resetMediaInfo"
>
<editor-state-observer @transaction="updateMediaInfoToState">
<gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">
@ -240,6 +260,19 @@ export default {
@click="startEditingMedia"
/>
<gl-button
v-if="isDrawioDiagram"
v-gl-tooltip
variant="default"
category="tertiary"
size="medium"
data-testid="edit-diagram"
:aria-label="replaceLabel"
title="Edit diagram"
icon="diagram"
@click="editDiagram"
/>
<gl-button
v-else
v-gl-tooltip
variant="default"
category="tertiary"

View File

@ -53,6 +53,10 @@ export default {
text: __('PlantUML diagram'),
action: () => this.insert('diagram', { language: 'plantuml' }),
},
{
text: __('Create or edit diagram'),
action: () => this.execute('createOrEditDiagram', 'drawioDiagram'),
},
{
text: __('Table of contents'),
action: () => this.execute('insertTableOfContents', 'tableOfContents'),

View File

@ -47,6 +47,7 @@ export const KEYDOWN_EVENT = 'keydown';
export const PARSE_HTML_PRIORITY_LOWEST = 1;
export const PARSE_HTML_PRIORITY_DEFAULT = 50;
export const PARSE_HTML_PRIORITY_HIGH = 75;
export const PARSE_HTML_PRIORITY_HIGHEST = 100;
export const EXTENSION_PRIORITY_LOWER = 75;

View File

@ -0,0 +1,41 @@
import { create } from '~/drawio/content_editor_facade';
import { launchDrawioEditor } from '~/drawio/drawio_editor';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import createAssetResolver from '../services/asset_resolver';
import Image from './image';
export default Image.extend({
name: 'drawioDiagram',
addOptions() {
return {
...this.parent?.(),
uploadsPath: null,
renderMarkdown: null,
};
},
parseHTML() {
return [
{
priority: PARSE_HTML_PRIORITY_HIGHEST,
tag: 'a.no-attachment-icon[data-canonical-src$="drawio.svg"]',
},
{
tag: 'img[src]',
},
];
},
addCommands() {
return {
createOrEditDiagram: () => () => {
launchDrawioEditor({
editorFacade: create({
tiptapEditor: this.editor,
drawioNodeName: this.name,
uploadsPath: this.options.uploadsPath,
assetResolver: createAssetResolver({ renderMarkdown: this.options.renderMarkdown }),
}),
});
},
};
},
});

View File

@ -1,5 +1,5 @@
import { Image } from '@tiptap/extension-image';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import { PARSE_HTML_PRIORITY_HIGH } from '../constants';
const resolveImageEl = (element) =>
element.nodeName === 'IMG' ? element : element.querySelector('img');
@ -77,7 +77,7 @@ export default Image.extend({
parseHTML() {
return [
{
priority: PARSE_HTML_PRIORITY_HIGHEST,
priority: PARSE_HTML_PRIORITY_HIGH,
tag: 'a.no-attachment-icon',
},
{

View File

@ -16,6 +16,7 @@ import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details';
import DetailsContent from '../extensions/details_content';
import Diagram from '../extensions/diagram';
import DrawioDiagram from '../extensions/drawio_diagram';
import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
@ -109,6 +110,7 @@ export const createContentEditor = ({
DetailsContent,
Document,
Diagram,
DrawioDiagram.configure({ uploadsPath, renderMarkdown }),
Dropcursor,
Emoji,
Figure,

View File

@ -12,6 +12,7 @@ import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details';
import DetailsContent from '../extensions/details_content';
import DrawioDiagram from '../extensions/drawio_diagram';
import Comment from '../extensions/comment';
import Diagram from '../extensions/diagram';
import Emoji from '../extensions/emoji';
@ -134,6 +135,10 @@ const defaultSerializerConfig = {
[CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
[Comment.name]: renderComment,
[Diagram.name]: preserveUnchanged(renderCodeBlock),
[DrawioDiagram.name]: preserveUnchanged({
render: renderImage,
inline: true,
}),
[DescriptionList.name]: renderHTMLNode('dl', true),
[DescriptionItem.name]: (state, node, parent, index) => {
if (index === 1) state.ensureNewLine();

View File

@ -4,17 +4,27 @@ import { __ } from '~/locale';
import { extractFilename, readFileAsDataURL } from './utils';
export const acceptedMimes = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'],
audio: [
'audio/basic',
'audio/mid',
'audio/mpeg',
'audio/x-aiff',
'audio/ogg',
'audio/vorbis',
'audio/vnd.wav',
],
video: ['video/mp4', 'video/quicktime'],
drawioDiagram: {
mimes: ['image/svg+xml'],
ext: 'drawio.svg',
},
image: {
mimes: ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'],
},
audio: {
mimes: [
'audio/basic',
'audio/mid',
'audio/mpeg',
'audio/x-aiff',
'audio/ogg',
'audio/vorbis',
'audio/vnd.wav',
],
},
video: {
mimes: ['video/mp4', 'video/quicktime'],
},
};
const extractAttachmentLinkUrl = (html) => {
@ -128,8 +138,8 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown, eve
export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
if (!file) return false;
for (const [type, mimes] of Object.entries(acceptedMimes)) {
if (mimes.includes(file?.type)) {
for (const [type, { mimes, ext }] of Object.entries(acceptedMimes)) {
if (mimes.includes(file?.type) && (!ext || file?.name.endsWith(ext))) {
uploadContent({ type, editor, file, uploadsPath, renderMarkdown, eventHub });
return true;

View File

@ -11,3 +11,5 @@ export const DARK_BACKGROUND_COLOR = '#202020';
export const DIAGRAM_BACKGROUND_COLOR = '#ffffff';
export const DRAWIO_IFRAME_TIMEOUT = 4000;
export const DIAGRAM_MAX_SIZE = 10 * 1024 * 1024; // 1MB

View File

@ -0,0 +1,80 @@
import axios from '~/lib/utils/axios_utils';
/**
* A set of functions to decouple the content_editor component from
* the draw.io editor.
* It allows the draw.io editor to obtain a selected drawio_diagram
* and replace it or insert a new drawio_diagram node without coupling
* the drawio_editor to the Content Editor implementation details
* *
* @param {Object} params Factory function parameters
* @param {Object} params.tiptapEditor See https://tiptap.dev/api/editor
* @param {String} params.drawioNodeName Name of the drawio_diagram node in
* the ProseMirror document
* @param {String} params.uploadsPath API endpoint to upload files
* @param {Object} params.assetResolver See
* app/assets/javascripts/content_editor/services/asset_resolver.js
*
* @returns A content_editor_facade object with operations
* to get a selected diagram, upload a diagram, insert a new one in the
* Content Editor, and update an existings diagram URL.
*/
export const create = ({ tiptapEditor, drawioNodeName, uploadsPath, assetResolver }) => ({
getDiagram: async () => {
const { node } = tiptapEditor.state.selection;
if (!node || node.type.name !== drawioNodeName) {
return null;
}
const { src } = node.attrs;
const response = await axios.get(src, { responseType: 'text' });
const diagramSvg = response.data;
const contentType = response.headers['content-type'];
const filename = src.split('/').pop();
return {
diagramURL: src,
filename,
diagramSvg,
contentType,
};
},
updateDiagram: async ({ uploadResults: { file_path: canonicalSrc } }) => {
const src = await assetResolver.resolveUrl(canonicalSrc);
tiptapEditor
.chain()
.focus()
.updateAttributes(drawioNodeName, {
src,
canonicalSrc,
})
.run();
},
insertDiagram: async ({ uploadResults: { file_path: canonicalSrc } }) => {
const src = await assetResolver.resolveUrl(canonicalSrc);
tiptapEditor
.chain()
.focus()
.insertContent({
type: drawioNodeName,
attrs: {
src,
canonicalSrc,
},
})
.run();
},
uploadDiagram: async ({ filename, diagramSvg }) => {
const blob = new Blob([diagramSvg], { type: 'image/svg+xml' });
const formData = new FormData();
formData.append('file', blob, filename);
const response = await axios.post(uploadsPath, formData);
return response.data;
},
});

View File

@ -9,6 +9,7 @@ import {
DRAWIO_FRAME_ID,
DIAGRAM_BACKGROUND_COLOR,
DRAWIO_IFRAME_TIMEOUT,
DIAGRAM_MAX_SIZE,
} from './constants';
function updateDrawioEditorState(drawIOEditorState, data) {
@ -109,14 +110,24 @@ async function loadExistingDiagram(drawIOEditorState, editorFacade) {
try {
diagram = await editorFacade.getDiagram();
} catch (e) {
throw new Error(__('Cannot load the diagram into the draw.io editor'));
throw new Error(__('Cannot load the diagram into the diagrams.net editor'));
}
if (diagram) {
const { diagramMarkdown, filename, diagramSvg, contentType } = diagram;
const { diagramMarkdown, filename, diagramSvg, contentType, diagramURL } = diagram;
const resolvedURL = new URL(diagramURL, window.location.origin);
const diagramSvgSize = new Blob([diagramSvg]).size;
if (contentType !== 'image/svg+xml') {
throw new Error(__('The selected image is not a diagram'));
throw new Error(__('The selected image is not a valid SVG diagram'));
}
if (resolvedURL.origin !== window.location.origin) {
throw new Error(__('The selected image is not an asset uploaded in the application'));
}
if (diagramSvgSize > DIAGRAM_MAX_SIZE) {
throw new Error(__('The selected image is too large.'));
}
updateDrawioEditorState(drawIOEditorState, {
@ -142,7 +153,7 @@ async function prepareEditor(drawIOEditorState, editorFacade) {
try {
await loadExistingDiagram(drawIOEditorState, editorFacade);
iframe.style.visibility = '';
iframe.style.visibility = 'visible';
iframe.style.cursor = '';
window.scrollTo(0, 0);
} catch (e) {
@ -212,23 +223,15 @@ function createEditorIFrame(drawIOEditorState) {
setAttributes(iframe, {
id: DRAWIO_FRAME_ID,
src: DRAWIO_EDITOR_URL,
class: 'drawio-editor',
});
iframe.style.position = 'absolute';
iframe.style.border = '0';
iframe.style.top = '0px';
iframe.style.left = '0px';
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.zIndex = '1100';
iframe.style.visibility = 'hidden';
document.body.appendChild(iframe);
setTimeout(() => {
if (drawIOEditorState.initialized === false) {
disposeDrawioEditor(drawIOEditorState);
createAlert({ message: __('The draw.io editor could not be loaded.') });
createAlert({ message: __('The diagrams.net editor could not be loaded.') });
}
}, DRAWIO_IFRAME_TIMEOUT);

View File

@ -32,6 +32,7 @@ export const create = ({ textArea, markdownPreviewPath, uploadsPath }) => ({
const contentType = response.headers['content-type'];
return {
diagramURL: imageURL,
diagramMarkdown: imageMarkdown,
filename,
diagramSvg,

View File

@ -8,7 +8,7 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';

View File

@ -1,7 +1,7 @@
<script>
import { GlModal, GlSprintf, GlIcon } from '@gitlab/ui';
import { TYPE_ISSUE } from '~/issues/constants';
import { __, n__ } from '~/locale';
import { ISSUABLE_TYPE } from '../constants';
export default {
actionCancel: {
@ -19,7 +19,7 @@ export default {
},
inject: {
issuableType: {
default: ISSUABLE_TYPE.issues,
default: TYPE_ISSUE,
},
email: {
default: '',
@ -47,14 +47,17 @@ export default {
href: this.exportCsvPath,
variant: 'confirm',
'data-method': 'post',
'data-qa-selector': `export_${this.issuableType}_button`,
'data-qa-selector': `export_issues_button`,
'data-track-action': 'click_button',
'data-track-label': `export_${this.issuableType}_csv`,
'data-track-label': this.dataTrackLabel,
},
};
},
isIssue() {
return this.issuableType === ISSUABLE_TYPE.issues;
return this.issuableType === TYPE_ISSUE;
},
dataTrackLabel() {
return this.isIssue ? 'export_issues_csv' : 'export_merge-requests_csv';
},
exportText() {
return this.isIssue ? __('Export issues') : __('Export merge requests');

View File

@ -7,8 +7,8 @@ import {
GlTooltipDirective,
GlModalDirective,
} from '@gitlab/ui';
import { TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
import { ISSUABLE_TYPE } from '../constants';
import CsvExportModal from './csv_export_modal.vue';
import CsvImportModal from './csv_import_modal.vue';
@ -34,7 +34,7 @@ export default {
},
inject: {
issuableType: {
default: ISSUABLE_TYPE.issues,
default: TYPE_ISSUE,
},
showExportButton: {
default: false,

View File

@ -1,11 +1 @@
export const EVENT_ISSUABLE_VUE_APP_CHANGE = 'issuable_vue_app:change';
export const ISSUABLE_TYPE = {
issues: 'issues',
mergeRequests: 'merge-requests',
};
export const ISSUABLE_INDEX = {
ISSUE: 'issue_',
MERGE_REQUEST: 'merge_request_',
};

View File

@ -113,7 +113,7 @@ export default {
>
<div
v-if="hasTimelineEvents"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-w-8 gl-h-8 gl-z-index-1"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-w-8 gl-h-8 gl-flex-shrink-0 gl-p-3 gl-z-index-1"
>
<gl-icon name="comment" class="note-icon" />
</div>

View File

@ -255,11 +255,10 @@ export default {
</gl-form-group>
</div>
<gl-form-group class="gl-mb-0">
<div class="gl-display-flex">
<div class="gl-display-flex gl-flex-wrap gl-gap-3">
<gl-button
variant="confirm"
category="primary"
class="gl-mr-3"
data-testid="save-button"
:disabled="!isTimelineTextValid"
:loading="isEventProcessed"
@ -271,7 +270,6 @@ export default {
v-if="showSaveAndAdd"
variant="confirm"
category="secondary"
class="gl-mr-3 gl-ml-n2"
data-testid="save-and-add-button"
:disabled="!isTimelineTextValid"
:loading="isEventProcessed"
@ -279,7 +277,7 @@ export default {
>
{{ $options.i18n.saveAndAdd }}
</gl-button>
<gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
<gl-button :disabled="isEventProcessed" @click="$emit('cancel')">
{{ $options.i18n.cancel }}
</gl-button>
<gl-button

View File

@ -28,6 +28,9 @@ export default {
},
},
computed: {
isPrivatePackage() {
return !this.packageEntity.publicPackage;
},
pypiPipCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `pip install ${this.packageEntity.name} --index-url ${this.packageEntity.pypiUrl}`;
@ -75,7 +78,7 @@ password = <your personal access token>`;
:tracking-action="$options.tracking.TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND"
:tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<template #description>
<template v-if="isPrivatePackage" #description>
<gl-sprintf :message="$options.i18n.tokenText">
<template #link="{ content }">
<gl-link

View File

@ -15,6 +15,7 @@ query getPackageDetails(
updatedAt
status
canDestroy
publicPackage
npmUrl
mavenUrl
conanUrl

View File

@ -3,10 +3,9 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { initBulkUpdateSidebar, initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
initBulkUpdateSidebar('merge_request_');
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');

View File

@ -71,7 +71,7 @@ export default {
},
computed: {
forcePushAttributes() {
const { allowForcePush } = this.branchProtection;
const { allowForcePush } = this.branchProtection || {};
const icon = allowForcePush ? REQUIRED_ICON : NOT_REQUIRED_ICON;
const iconClass = allowForcePush ? REQUIRED_ICON_CLASS : NOT_REQUIRED_ICON_CLASS;
const title = allowForcePush
@ -81,7 +81,7 @@ export default {
return { icon, iconClass, title };
},
codeOwnersApprovalAttributes() {
const { codeOwnerApprovalRequired } = this.branchProtection;
const { codeOwnerApprovalRequired } = this.branchProtection || {};
const icon = codeOwnerApprovalRequired ? REQUIRED_ICON : NOT_REQUIRED_ICON;
const iconClass = codeOwnerApprovalRequired ? REQUIRED_ICON_CLASS : NOT_REQUIRED_ICON_CLASS;
const title = codeOwnerApprovalRequired

View File

@ -1,7 +1,7 @@
<script>
import { GlToast, GlTooltipDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';

View File

@ -1,6 +1,6 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '../../constants';

View File

@ -1,7 +1,7 @@
<script>
import produce from 'immer';
import Vue from 'vue';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __, sprintf } from '~/locale';
import { confidentialityQueries, Tracking } from '../../constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';

View File

@ -1,6 +1,6 @@
<script>
import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';

View File

@ -1,6 +1,6 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { logError } from '~/lib/logger';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
import {

View File

@ -1,4 +1,4 @@
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';

View File

@ -8,7 +8,7 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import produce from 'immer';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { WORKSPACE_GROUP } from '~/issues/constants';
import { __ } from '~/locale';
import { workspaceLabelsQueries } from '../../../constants';

View File

@ -1,7 +1,7 @@
<script>
import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { workspaceLabelsQueries } from '../../../constants';

View File

@ -2,7 +2,7 @@
import { debounce } from 'lodash';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants';

View File

@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions } from 'vuex';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __, sprintf } from '~/locale';
import eventHub from '../../event_hub';
@ -49,11 +49,11 @@ export default {
fullPath: this.fullPath,
})
.catch(() => {
const flashMessage = __(
const alertMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
createAlert({
message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
message: sprintf(alertMessage, { issuableDisplayName: this.issuableDisplayName }),
});
})
.finally(() => {

View File

@ -4,7 +4,7 @@ import { mapGetters, mapActions } from 'vuex';
import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
import eventHub from '../../event_hub';
import EditForm from './edit_form.vue';
@ -92,11 +92,11 @@ export default {
}
})
.catch(() => {
const flashMessage = __(
const alertMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
createAlert({
message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
message: sprintf(alertMessage, { issuableDisplayName: this.issuableDisplayName }),
});
})
.finally(() => {

View File

@ -27,7 +27,7 @@ import {
LocalizedIssuableAttributeType,
noAttributeId,
} from 'ee_else_ce/sidebar/constants';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { PathIdSeparator } from '~/related_issues/constants';
export default {

View File

@ -1,7 +1,7 @@
<script>
import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';

View File

@ -1,6 +1,6 @@
<script>
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { TYPE_EPIC, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';

View File

@ -2,7 +2,7 @@
import { GlButton, GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@ -141,7 +141,7 @@ export default {
Object.assign(e, { returnValue });
return returnValue;
},
flashAPIFailure(err) {
alertAPIFailure(err) {
const defaultErrorMsg = this.newSnippet
? SNIPPET_CREATE_MUTATION_ERROR
: SNIPPET_UPDATE_MUTATION_ERROR;
@ -190,7 +190,7 @@ export default {
const errors = baseObj?.errors;
if (errors?.length) {
this.flashAPIFailure(errors[0]);
this.alertAPIFailure(errors[0]);
} else {
redirectTo(baseObj.snippet.webUrl);
}
@ -199,7 +199,7 @@ export default {
// eslint-disable-next-line no-console
console.error('[gitlab] unexpected error while updating snippet', e);
this.flashAPIFailure(getErrorMessage(e));
this.alertAPIFailure(getErrorMessage(e));
});
},
updateActions(actions) {

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
@ -60,9 +60,9 @@ export default {
.then((res) => {
this.notifyAboutUpdates({ content: res.data });
})
.catch((e) => this.flashAPIFailure(e));
.catch((e) => this.alertAPIFailure(e));
},
flashAPIFailure(err) {
alertAPIFailure(err) {
createAlert({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
},
},

View File

@ -19,7 +19,7 @@ import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/alert';
import DeleteSnippetMutation from '../mutations/delete_snippet.mutation.graphql';

View File

@ -5,6 +5,7 @@
/*= provides zen_mode:enter */
/*= provides zen_mode:leave */
import autosize from 'autosize';
import Dropzone from 'dropzone';
import $ from 'jquery';
import Mousetrap from 'mousetrap';
@ -39,6 +40,7 @@ export default class ZenMode {
constructor() {
this.active_backdrop = null;
this.active_textarea = null;
this.storedStyle = null;
$(document).on('click', '.js-zen-enter', (e) => {
e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:enter');
@ -68,6 +70,7 @@ export default class ZenMode {
this.active_backdrop.addClass('fullscreen');
this.active_textarea = this.active_backdrop.find('textarea');
// Prevent a user-resized textarea from persisting to fullscreen
this.storedStyle = this.active_textarea.attr('style');
this.active_textarea.removeAttr('style');
this.active_textarea.focus();
}
@ -77,6 +80,11 @@ export default class ZenMode {
Mousetrap.unpause();
this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen');
scrollToElement(this.active_textarea, { duration: 0, offset: -100 });
this.active_textarea.attr('style', this.storedStyle);
autosize(this.active_textarea);
autosize.update(this.active_textarea);
this.active_textarea = null;
this.active_backdrop = null;

View File

@ -195,3 +195,20 @@ ul.wiki-pages-list.content-list {
display: none;
}
}
.drawio-editor {
position: fixed;
top: calc(var(--header-height, 48px));
left: 0;
bottom: 0;
width: 100%;
height: calc(100% - var(--header-height, 48px));
border: 0;
z-index: 1100;
visibility: hidden;
}
.with-performance-bar .drawio-editor {
top: calc(var(--header-height, 48px) + 35px);
height: calc(100% - var(--header-height, 48px) - 35px);
}

View File

@ -63,11 +63,11 @@ module Types
end
def pypi_url
pypi_registry_url(object.project.id)
pypi_registry_url(object.project)
end
def public_package
object.project.public? || object.project.project_feature.package_registry_access_level == ProjectFeature::PUBLIC
object.project.project_feature.public_packages?
end
end
end

View File

@ -27,9 +27,14 @@ module PackagesHelper
presenter.detail_view.to_json
end
def pypi_registry_url(project_id)
full_url = expose_url(api_v4_projects_packages_pypi_simple_package_name_path({ id: project_id, package_name: '' }, true))
full_url.sub!('://', '://__token__:<your_personal_token>@')
def pypi_registry_url(project)
full_url = expose_url(api_v4_projects_packages_pypi_simple_package_name_path({ id: project.id, package_name: '' }, true))
if project.project_feature.public_packages?
full_url
else
full_url.sub!('://', '://__token__:<your_personal_token>@')
end
end
def composer_registry_url(group_id)

View File

@ -33,3 +33,5 @@ class DependencyProxy::Registry
end
end
end
::DependencyProxy::Registry.prepend_mod

View File

@ -145,7 +145,7 @@ module ErrorTracking
ensure_issue_belongs_to_project!(issue_to_be_updated.project_id)
handle_exceptions do
{ updated: sentry_client.update_issue(opts) }
{ updated: sentry_client.update_issue(**opts) }
end
end

View File

@ -162,6 +162,12 @@ class ProjectFeature < ApplicationRecord
end
end
def public_packages?
return false unless Gitlab.config.packages.enabled
package_registry_access_level == PUBLIC || project.public?
end
private
def set_pages_access_level

View File

@ -1,7 +1,7 @@
- show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true)
- show_import_button = local_assigns.fetch(:show_import_button, true) && can?(current_user, :import_issues, @project)
- show_export_button = local_assigns.fetch(:show_export_button, true)
- issuable_type = 'issues'
- issuable_type = 'issue'
- can_edit = can?(current_user, :admin_project, @project)
- notification_email = @current_user.present? ? @current_user.notification_email_or_default : nil

View File

@ -1,4 +1,4 @@
- issuable_type = 'merge-requests'
- issuable_type = 'merge_request'
- notification_email = @current_user.present? ? @current_user.notification_email_or_default : nil
= render 'shared/issuable/feed_buttons', show_calendar_button: false

View File

@ -4,7 +4,6 @@
- opened_issues_count = issuables_count_for_state(:issues, :opened)
- is_opened_state = params[:state] == 'opened'
- is_closed_state = params[:state] == 'closed'
- issuable_type = 'issues'
- can_edit = can?(current_user, :admin_project, @project)
.row.empty-state
@ -43,7 +42,7 @@
= link_to _('New issue'), button_path, class: 'gl-button btn btn-confirm', id: 'new_issue_link'
- if show_import_button
.js-csv-import-export-buttons{ data: { show_import_button: 'true', issuable_type: issuable_type, import_csv_issues_path: import_csv_namespace_project_issues_path, can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes), container_class: 'gl-w-full gl-sm-w-auto gl-sm-mr-3 gl-display-inline-flex gl-vertical-align-middle', show_label: 'true' } }
.js-csv-import-export-buttons{ data: { show_import_button: 'true', issuable_type: 'issue', import_csv_issues_path: import_csv_namespace_project_issues_path, can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes), container_class: 'gl-w-full gl-sm-w-auto gl-sm-mr-3 gl-display-inline-flex gl-vertical-align-middle', show_label: 'true' } }
%hr
%p.gl-text-center.gl-mb-0
%strong

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class ScheduleTemporaryPartitioningIndexesRemoval < Gitlab::Database::Migration[2.1]
INDEXES = [
[:ci_pipelines, :tmp_index_ci_pipelines_on_partition_id_and_id],
[:ci_stages, :tmp_index_ci_stages_on_partition_id_and_id],
[:ci_builds, :tmp_index_ci_builds_on_partition_id_and_id],
[:ci_build_needs, :tmp_index_ci_build_needs_on_partition_id_and_id],
[:ci_build_report_results, :tmp_index_ci_build_report_results_on_partition_id_and_build_id],
[:ci_build_trace_metadata, :tmp_index_ci_build_trace_metadata_on_partition_id_and_id],
[:ci_job_artifacts, :tmp_index_ci_job_artifacts_on_partition_id_and_id],
[:ci_pipeline_variables, :tmp_index_ci_pipeline_variables_on_partition_id_and_id],
[:ci_job_variables, :tmp_index_ci_job_variables_on_partition_id_and_id],
[:ci_sources_pipelines, :tmp_index_ci_sources_pipelines_on_partition_id_and_id],
[:ci_sources_pipelines, :tmp_index_ci_sources_pipelines_on_source_partition_id_and_id],
[:ci_running_builds, :tmp_index_ci_running_builds_on_partition_id_and_id],
[:ci_pending_builds, :tmp_index_ci_pending_builds_on_partition_id_and_id],
[:ci_builds_runner_session, :tmp_index_ci_builds_runner_session_on_partition_id_and_id]
]
def up
INDEXES.each do |table_name, index_name|
prepare_async_index_removal table_name, nil, name: index_name
end
end
def down
INDEXES.each do |table_name, index_name|
unprepare_async_index table_name, nil, name: index_name
end
end
end

View File

@ -0,0 +1 @@
6af890fe88f25be54d18cf3b3caa14830a3d627e7ff256d7a4ae03f9f1c7170c

View File

@ -393,12 +393,15 @@ scope.
| GitLab Rails app | `%15.9` | Implement new GraphQL user-authenticated API to create a new runner. |
| GitLab Rails app | `%15.10` | Return token and runner ID information from `/runners/verify` REST endpoint. |
| GitLab Runner | `%15.10` | [Modify register command to allow new flow with glrt- prefixed authentication tokens](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29613). |
| GitLab Runner | `%15.10` | Make the `gitlab-runner register` command happen in a single operation. |
| GitLab Rails app | `%15.10` | Define feature flag and policies for "New Runner creation workflow" for groups and projects. |
| GitLab Rails app | `%15.11` | Only update runner `contacted_at` and `status` when polled for jobs. |
| GitLab Rails app | `%15.11` | Update service and mutation to accept groups and projects. |
| GitLab Rails app | `%15.11` | Implement UI to create new runner. |
| GitLab Rails app | `%15.11` | GraphQL changes to `CiRunner` type. (?) |
| GitLab Rails app | `%15.11` | UI changes to runner details view (listing of platform, architecture, IP address, etc.) (?) |
| GitLab Rails app | `%15.11` | Adapt `POST /api/v4/runners` REST endpoint to accept a request from an authorized user with a scope instead of a registration token. || GitLab Rails app | `%15.9` | Implement new GraphQL user-authenticated API to create a new runner. |
| GitLab Rails app | `%15.11` | Adapt `POST /api/v4/runners` REST endpoint to accept a request from an authorized user with a scope instead of a registration token. |
| GitLab Runner | `%15.11` | Handle glrt- runner tokens in `unregister` command. |
### Stage 5 - Optional disabling of registration token

View File

@ -231,37 +231,43 @@ configuration for jobs that use the Windows runner, like scripts, use <code>&#92
### Run child pipelines with merge request pipelines
To trigger a child pipeline as a [merge request pipeline](merge_request_pipelines.md):
Pipelines, including child pipelines, run as branch pipelines by default when not using
[`rules`](../yaml/index.md#rules) or [`workflow:rules`](../yaml/index.md#workflowrules).
To configure child pipelines to run when triggered from a [merge request (parent) pipeline](merge_request_pipelines.md), use `rules` or `workflow:rules`.
For example, using `rules`:
1. Set the trigger job to run on merge requests in the parent pipeline's configuration file:
1. Set the parent pipeline's trigger job to run on merge requests:
```yaml
microservice_a:
trigger-child-pipeline-job:
trigger:
include: path/to/microservice_a.yml
include: path/to/child-pipeline-configuration.yml
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```
1. Configure the child pipeline jobs to run in merge request pipelines with [`rules`](../yaml/index.md#rules)
or [`workflow:rules`](../yaml/index.md#workflowrules).
For example, with `rules` in a child pipeline's configuration file:
1. Use `rules` to configure the child pipeline jobs to run when triggered by the parent pipeline:
```yaml
job1:
script: echo "Child pipeline job 1"
script: echo "This child pipeline job runs any time the parent pipeline triggers it."
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_PIPELINE_SOURCE == "parent_pipeline"
job2:
script: echo "Child pipeline job 2"
script: echo "This child pipeline job runs only when the parent pipeline is a merge request pipeline"
rules:
- if: $CI_MERGE_REQUEST_ID
```
In child pipelines, `$CI_PIPELINE_SOURCE` always has a value of `parent_pipeline`
and cannot be used to identify merge request pipelines. Use `$CI_MERGE_REQUEST_ID`
instead, which is always present in merge request pipelines.
In child pipelines, `$CI_PIPELINE_SOURCE` always has a value of `parent_pipeline`, so:
- You can use `if: $CI_PIPELINE_SOURCE == "parent_pipeline"` to ensure child pipeline jobs always run.
- You _can't_ use `if: $CI_PIPELINE_SOURCE == "merge_request_event"` to configure child pipeline
jobs to run for merge request pipelines. Instead, use `if: $CI_MERGE_REQUEST_ID`
to set child pipeline jobs to run only when the parent pipeline is a merge request pipeline. The parent pipeline's
[`CI_MERGE_REQUEST_*` predefined variables](../variables/predefined_variables.md#predefined-variables-for-merge-request-pipelines)
are passed to the child pipeline jobs.
### Specify a branch for multi-project pipelines
@ -657,6 +663,16 @@ With multi-project pipelines, the trigger job fails and does not create the down
to run pipelines against the protected branch. See [pipeline security for protected branches](index.md#pipeline-security-on-protected-branches)
for more information.
### Job in child pipeline is not created when the pipeline runs
If the parent pipeline is a [merge request pipeline](merge_request_pipelines.md),
the child pipeline must [use `workflow:rules` or `rules` to ensure the jobs run](#run-child-pipelines-with-merge-request-pipelines).
If no jobs in the child pipeline can run due to missing or incorrect `rules` configuration:
- The child pipeline fails to start.
- The parent pipeline's trigger job fails with: `downstream pipeline can not be creaed, Pipeline will not run for the selected trigger. The rules configuration prevented any jobs from being added to the pipeline.`
### `Ref is ambiguous`
You cannot trigger a multi-project pipeline with a tag when a branch exists with the same

View File

@ -11,6 +11,36 @@ disruptive effect on customers. Before adding a required stop, consider if any
alternative approaches exist to avoid a required stop. Sometimes a required
stop is unavoidable. In those cases, follow the instructions below.
## Common scenarios that require stops
### Long running migrations being finalized
If a migration takes a long time, it could cause a large number of customers to encounter timeouts
during upgrades. The increased support volume may cause us to introduce a required stop. While any
background migration may cause these issues with particularly large customers, we typically only
introduce stops when the impact is widespread.
- **Cause:** When an upgrade takes more than an hour, omnibus times out.
- **Mitigation:** Schedule finalization for the first minor version after the next required stop.
### Improperly finalized background migrations
You may need to introduce a required stop for mitigation when:
- A background migration is not finalized, and
- A migration is written that depends on that background migration.
- **Cause:** The dependent migration may fail if the background migration is incomplete.
- **Mitigation:** Ensure that all background migrations are finalized before authoring dependent migrations.
### Bugs in migration related tooling
In a few circumstances, bugs in migration related tooling has required us to introduce stops. While we aim
to prevent these in testing, sometimes they happen.
- **Cause:** There have been a few different causes where we recognized these too late.
- **Mitigation:** Typically we try to backport fixes for migrations, but in some cases this is not possible.
## Before the required stop is released
Before releasing a known required stop, complete these steps. If the required stop

View File

@ -52,29 +52,28 @@ If you have any questions on configuring the SAML app, contact your provider's s
### Set up Azure
Follow the Azure documentation on [configuring single sign-on to applications](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-sso), and use the following notes when needed.
1. [Use Azure to configure SSO for an application](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-sso). The following GitLab settings correspond to the Azure fields.
| GitLab setting | Azure field |
| ------------------------------------ | ------------------------------------------ |
| Identifier | Identifier (Entity ID) |
| Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) |
| GitLab single sign-on URL | Sign on URL |
| Identity provider single sign-on URL | Login URL |
| Certificate fingerprint | Thumbprint |
1. You should set the following attributes:
- **Unique User Identifier (Name identifier)** to `user.objectID`.
- **nameid-format** to persistent.
- **Additional claims** to [supported attributes](#user-attributes).
1. Optional. If you use [Group Sync](#group-sync), customize the name of the
group claim to match the required attribute.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azure Using SAML SSO for Groups Demo](https://youtu.be/24-ZxmTeEBU).
The video is outdated in regard to objectID mapping and you should follow the [SCIM documentation](scim_setup.md#configure-azure-active-directory).
View a demo of [SCIM provisioning on Azure using SAML SSO for groups](https://youtu.be/24-ZxmTeEBU). The `objectID` mapping is outdated in this video. Follow the [SCIM documentation](scim_setup.md#configure-azure-active-directory) instead.
| GitLab Setting | Azure Field |
| ------------------------------------ | ------------------------------------------ |
| Identifier | Identifier (Entity ID) |
| Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) |
| GitLab single sign-on URL | Sign on URL |
| Identity provider single sign-on URL | Login URL |
| Certificate fingerprint | Thumbprint |
You should set the following attributes:
- **Unique User Identifier (Name identifier)** to `user.objectID`.
- **nameid-format** to persistent.
- Additional claims to [supported attributes](#user-attributes).
If using [Group Sync](#group-sync), customize the name of the group claim to match the required attribute.
See our [example configuration page](example_saml_config.md#azure-active-directory).
View an [example configuration page](example_saml_config.md#azure-active-directory).
### Set up Google Workspace

View File

@ -559,25 +559,45 @@ On wikis, you can use the [diagrams.net](https://www.diagrams.net/) editor
to create diagrams. You can also edit diagrams previously created with the
editor.
To create a diagram:
To create a diagram in the Markdown editor:
1. Select **Insert or edit diagram** in the Markdown editor.
1. Select **Insert or edit diagram** (**{diagram}**) in the editor's toolbar.
1. Use the diagrams.net editor to build the diagram.
1. Select **Save & exit**.
A Markdown image declaration pointing to the diagram is inserted in the wiki content.
To edit a diagram:
To edit a diagram in the Markdown editor:
1. Place the Markdown editors text field cursor in a Markdown image declaration
that contains the diagram.
1. Select **Insert or edit diagram** in the Markdown editor.
1. Select **Insert or edit diagram** (**{diagram}**) in the Markdown editor.
1. Use the diagrams.net editor to edit the diagram.
1. Select **Save & exit**.
A Markdown image declaration pointing to the diagram is inserted in the wiki content,
replacing the previous diagram.
You can also create and edit diagrams when editing Markdown in the Content Editor.
To create a diagram in the Content Editor:
1. Select **More options** (**{plus}**) in the editors toolbar.
1. Select **Create or edit diagram** in the dropdown menu.
1. Use the diagrams.net editor to build the diagram.
1. Select **Save & exit**.
The diagram as visualized in the diagrams.net editor is inserted in the wiki content.
To edit a diagram in the Content Editor:
1. Select the diagram that you want to edit.
1. Select **Edit diagram** (**{diagram}**) in the floating toolbar.
1. Use the diagrams.net editor to edit the diagram.
1. Select **Save & exit**.
The selected diagram is replaced with an updated version.
## GitLab-specific references
GitLab Flavored Markdown renders GitLab-specific references. For example, you can reference

View File

@ -5,7 +5,7 @@ module Gitlab
class BaseSingleChecker < BaseChecker
attr_reader :change_access
delegate(*SingleChangeAccess::ATTRIBUTES, to: :change_access)
delegate(*SingleChangeAccess::ATTRIBUTES, :branch_ref?, :tag_ref?, to: :change_access)
def initialize(change_access)
@change_access = change_access

View File

@ -89,7 +89,7 @@ module Gitlab
@single_changes_accesses ||=
changes.map do |change|
commits =
if blank_rev?(change[:newrev])
if !commitish_ref?(change[:ref]) || blank_rev?(change[:newrev])
[]
else
Gitlab::Lazy.new { commits_for(change[:oldrev], change[:newrev]) }
@ -122,6 +122,14 @@ module Gitlab
def blank_rev?(rev)
rev.blank? || Gitlab::Git.blank_ref?(rev)
end
# refs/notes/commits contains commits added via `git-notes`. We currently
# have no features that check notes so we can skip them. To future-proof
# we are skipping anything that isn't a branch or tag ref as those are
# the only refs that can contain commits.
def commitish_ref?(ref)
Gitlab::Git.branch_ref?(ref) || Gitlab::Git.tag_ref?(ref)
end
end
end
end

View File

@ -10,6 +10,10 @@ module Gitlab
}.freeze
def validate!
# git-notes stores notes history as commits in refs/notes/commits (by
# default but is configurable) so we restrict the diff checks to tag
# and branch refs
return unless tag_ref? || branch_ref?
return if deletion?
return unless should_run_validations?
return if commits.empty?

View File

@ -14,7 +14,9 @@ module Gitlab
protocol:, logger:, commits: nil
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_ref = Gitlab::Git.branch_ref?(@ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@tag_ref = Gitlab::Git.tag_ref?(@ref)
@tag_name = Gitlab::Git.tag_name(@ref)
@user_access = user_access
@project = project
@ -38,6 +40,14 @@ module Gitlab
@commits ||= project.repository.new_commits(newrev)
end
def branch_ref?
@branch_ref
end
def tag_ref?
@tag_ref
end
protected
def ref_level_checks

View File

@ -8151,7 +8151,7 @@ msgstr ""
msgid "Cannot import because issues are not available in this project."
msgstr ""
msgid "Cannot load the diagram into the draw.io editor"
msgid "Cannot load the diagram into the diagrams.net editor"
msgstr ""
msgid "Cannot make the epic confidential if it contains non-confidential child epics"
@ -11473,6 +11473,9 @@ msgstr ""
msgid "Copy commit SHA"
msgstr ""
msgid "Copy diagram URL"
msgstr ""
msgid "Copy environment"
msgstr ""
@ -11908,6 +11911,9 @@ msgstr ""
msgid "Create or close an issue."
msgstr ""
msgid "Create or edit diagram"
msgstr ""
msgid "Create or import your first project"
msgstr ""
@ -13544,6 +13550,9 @@ msgstr ""
msgid "Delete deploy key"
msgstr ""
msgid "Delete diagram"
msgstr ""
msgid "Delete epic"
msgstr ""
@ -15343,6 +15352,9 @@ msgstr ""
msgid "Edit description"
msgstr ""
msgid "Edit diagram description"
msgstr ""
msgid "Edit environment"
msgstr ""
@ -43170,6 +43182,9 @@ msgstr ""
msgid "The deployment of this job to %{environmentLink} did not succeed."
msgstr ""
msgid "The diagrams.net editor could not be loaded."
msgstr ""
msgid "The directory has been successfully created."
msgstr ""
@ -43182,9 +43197,6 @@ msgstr ""
msgid "The download link will expire in 24 hours."
msgstr ""
msgid "The draw.io editor could not be loaded."
msgstr ""
msgid "The environment tiers must be from %{environment_tiers}."
msgstr ""
@ -43489,7 +43501,13 @@ msgstr ""
msgid "The secret is only available when you create the application or renew the secret."
msgstr ""
msgid "The selected image is not a diagram"
msgid "The selected image is not a valid SVG diagram"
msgstr ""
msgid "The selected image is not an asset uploaded in the application"
msgstr ""
msgid "The selected image is too large."
msgstr ""
msgid "The snippet can be accessed without any authentication."

View File

@ -55,7 +55,7 @@
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
"@gitlab/svgs": "3.23.0",
"@gitlab/svgs": "3.24.0",
"@gitlab/ui": "56.2.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230223005157",

38
spec/fixtures/diagram.drawio.svg vendored Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
width="177px" height="97px" viewBox="-0.5 -0.5 177 97"
content="&lt;mxfile host=&quot;embed.diagrams.net&quot; modified=&quot;2022-11-18T14:21:55.551Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36&quot; version=&quot;20.5.3&quot; etag=&quot;cTK3wL1ch5_8VL-J45NP&quot; type=&quot;embed&quot;&gt;&lt;diagram id=&quot;mWELjHy14aEMRdjyCi3_&quot; name=&quot;Page-1&quot;&gt;jZLBcoQgDIafhrvItPVcu+1eevLQMyOpMAPGYbFqn75Ygq7d2ZmeSL4kkPyEidrNb14O+h0VWFYWambihZUlr554PFayJFI9FAl03ihK2kFjvoFgThuNgsshMSDaYIYjbLHvoQ0HJr3H6Zj2ifb46iA7uAFNK+0t/TAq6D9TrPwMptP5ZV5QxMmcTOCipcLpCokTE7VHDMlycw12FS/rkupe70S3xjz04T8FZSr4knak2ZSRnZeO2gtLntnj2CtYywomnidtAjSDbNfoFH85Mh2cjR6PJt0KPsB8tzO+zRsXBdBB8EtMoQKRNaMdiSD50644fySmr9SuiEn65G67etchGiRFdnfJf2NXiytOPw==&lt;/diagram&gt;&lt;/mxfile&gt;"
style="background-color: rgb(255, 255, 255);">
<defs />
<g>
<rect x="8" y="8" width="160" height="80" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)"
pointer-events="all" />
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml"
style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 48px; margin-left: 9px;">
<div data-drawio-colors="color: rgb(0, 0, 0); "
style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div
style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
diagram</div>
</div>
</div>
</foreignObject>
<text x="88" y="52" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px"
text-anchor="middle">diagram</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" />
<a transform="translate(0,-5)"
xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text>
</a>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -4,24 +4,30 @@ import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
import { stubComponent } from 'helpers/stub_component';
import eventHubFactory from '~/helpers/event_hub_factory';
import Image from '~/content_editor/extensions/image';
import Audio from '~/content_editor/extensions/audio';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
import Video from '~/content_editor/extensions/video';
import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils';
import {
PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../../test_constants';
const TIPTAP_IMAGE_HTML = `<p>
<img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
const TIPTAP_AUDIO_HTML = `<p>
<span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
</p>`;
const TIPTAP_DIAGRAM_HTML = `<p>
<img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
const TIPTAP_IMAGE_HTML = `<p>
<img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
const TIPTAP_VIDEO_HTML = `<p>
<span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
</p>`;
@ -29,10 +35,11 @@ const TIPTAP_VIDEO_HTML = `<p>
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
describe.each`
mediaType | mediaHTML | filePath | mediaOutputHTML
${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
mediaType | mediaHTML | filePath | mediaOutputHTML
${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
${'drawio_diagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${'test-file.drawio.svg'} | ${TIPTAP_DIAGRAM_HTML}
${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
`(
'content_editor/components/bubble_menus/media_bubble_menu ($mediaType)',
({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
@ -43,7 +50,7 @@ describe.each`
let eventHub;
const buildEditor = () => {
tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] });
tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video, DrawioDiagram] });
contentEditor = { resolveUrl: jest.fn() };
eventHub = eventHubFactory();
};
@ -114,6 +121,24 @@ describe.each`
expect(link.text()).toBe(filePath);
});
describe('when BubbleMenu emits hidden event', () => {
it('resets media bubble menu state', async () => {
// Switch to edit mode to access component state in form fields
await wrapper.findByTestId('edit-media').vm.$emit('click');
const mediaSrcInput = wrapper.findByTestId('media-src').vm.$el;
const mediaAltInput = wrapper.findByTestId('media-alt').vm.$el;
expect(mediaSrcInput.value).not.toBe('');
expect(mediaAltInput.value).not.toBe('');
await wrapper.findComponent(BubbleMenu).vm.$emit('hidden');
expect(mediaSrcInput.value).toBe('');
expect(mediaAltInput.value).toBe('');
});
});
describe('copy button', () => {
it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
jest.spyOn(navigator.clipboard, 'writeText');
@ -133,23 +158,39 @@ describe.each`
});
describe(`replace ${mediaType} button`, () => {
it('uploads and replaces the selected image when file input changes', async () => {
const commands = mockChainedCommands(tiptapEditor, [
'focus',
'deleteSelection',
'uploadAttachment',
'run',
]);
const file = new File(['foo'], 'foo.png', { type: 'image/png' });
if (mediaType !== 'drawio_diagram') {
it('uploads and replaces the selected image when file input changes', async () => {
const commands = mockChainedCommands(tiptapEditor, [
'focus',
'deleteSelection',
'uploadAttachment',
'run',
]);
const file = new File(['foo'], 'foo.png', { type: 'image/png' });
await wrapper.findByTestId('replace-media').vm.$emit('click');
await selectFile(file);
await wrapper.findByTestId('replace-media').vm.$emit('click');
await selectFile(file);
expect(commands.focus).toHaveBeenCalled();
expect(commands.deleteSelection).toHaveBeenCalled();
expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
expect(commands.run).toHaveBeenCalled();
});
expect(commands.focus).toHaveBeenCalled();
expect(commands.deleteSelection).toHaveBeenCalled();
expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
expect(commands.run).toHaveBeenCalled();
});
} else {
// draw.io diagrams are replaced using the edit diagram button
it('invokes editDiagram command', async () => {
const commands = mockChainedCommands(tiptapEditor, [
'focus',
'createOrEditDiagram',
'run',
]);
await wrapper.findByTestId('edit-diagram').vm.$emit('click');
expect(commands.focus).toHaveBeenCalled();
expect(commands.createOrEditDiagram).toHaveBeenCalled();
expect(commands.run).toHaveBeenCalled();
});
}
});
describe('edit button', () => {

View File

@ -40,16 +40,17 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
});
describe.each`
name | contentType | command | params
${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
name | contentType | command | params
${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
${'Create or edit diagram'} | ${'drawioDiagram'} | ${'createOrEditDiagram'} | ${[]}
`('when option $name is clicked', ({ name, command, contentType, params }) => {
let commands;
let btn;

View File

@ -2,6 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import Attachment from '~/content_editor/extensions/attachment';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
import Audio from '~/content_editor/extensions/audio';
import Video from '~/content_editor/extensions/video';
@ -16,6 +17,7 @@ import {
PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
PROJECT_WIKI_ATTACHMENT_LINK_HTML,
PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../test_constants';
describe('content_editor/extensions/attachment', () => {
@ -24,6 +26,7 @@ describe('content_editor/extensions/attachment', () => {
let p;
let image;
let audio;
let drawioDiagram;
let video;
let loading;
let link;
@ -35,6 +38,7 @@ describe('content_editor/extensions/attachment', () => {
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
const audioFile = new File(['foo'], 'test-file.mp3', { type: 'audio/mpeg' });
const videoFile = new File(['foo'], 'test-file.mp4', { type: 'video/mp4' });
const drawioDiagramFile = new File(['foo'], 'test-file.drawio.svg', { type: 'image/svg+xml' });
const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' });
const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => {
@ -67,12 +71,13 @@ describe('content_editor/extensions/attachment', () => {
Image,
Audio,
Video,
DrawioDiagram,
Attachment.configure({ renderMarkdown, uploadsPath, eventHub }),
],
});
({
builders: { doc, p, image, audio, video, loading, link },
builders: { doc, p, image, audio, video, loading, link, drawioDiagram },
} = createDocBuilder({
tiptapEditor,
names: {
@ -81,6 +86,7 @@ describe('content_editor/extensions/attachment', () => {
link: { nodeType: Link.name },
audio: { nodeType: Audio.name },
video: { nodeType: Video.name },
drawioDiagram: { nodeType: DrawioDiagram.name },
},
}));
@ -113,10 +119,11 @@ describe('content_editor/extensions/attachment', () => {
});
describe.each`
nodeType | mimeType | html | file | mediaType
${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
nodeType | mimeType | html | file | mediaType
${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
${'drawioDiagram'} | ${'image/svg+xml'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${drawioDiagramFile} | ${(attrs) => drawioDiagram(attrs)}
`('when the file has $nodeType mime type', ({ mimeType, html, file, mediaType }) => {
const base64EncodedFile = `data:${mimeType};base64,Zm9v`;
@ -151,7 +158,7 @@ describe('content_editor/extensions/attachment', () => {
mediaType({
canonicalSrc: file.name,
src: base64EncodedFile,
alt: 'test-file',
alt: expect.stringContaining('test-file'),
uploading: false,
}),
),

View File

@ -0,0 +1,103 @@
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
import createAssetResolver from '~/content_editor/services/asset_resolver';
import { create } from '~/drawio/content_editor_facade';
import { launchDrawioEditor } from '~/drawio/drawio_editor';
import { createTestEditor, createDocBuilder } from '../test_utils';
import {
PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../test_constants';
jest.mock('~/content_editor/services/asset_resolver');
jest.mock('~/drawio/content_editor_facade');
jest.mock('~/drawio/drawio_editor');
describe('content_editor/extensions/drawio_diagram', () => {
let tiptapEditor;
let doc;
let paragraph;
let image;
let drawioDiagram;
const uploadsPath = '/uploads';
const renderMarkdown = () => {};
beforeEach(() => {
tiptapEditor = createTestEditor({
extensions: [Image, DrawioDiagram.configure({ uploadsPath, renderMarkdown })],
});
const { builders } = createDocBuilder({
tiptapEditor,
names: {
image: { nodeType: Image.name },
drawioDiagram: { nodeType: DrawioDiagram.name },
},
});
doc = builders.doc;
paragraph = builders.paragraph;
image = builders.image;
drawioDiagram = builders.drawioDiagram;
});
describe('parsing', () => {
it('distinguishes a drawio diagram from an image', () => {
const expectedDocWithDiagram = doc(
paragraph(
drawioDiagram({
alt: 'test-file',
canonicalSrc: 'test-file.drawio.svg',
src: '/group1/project1/-/wikis/test-file.drawio.svg',
}),
),
);
const expectedDocWithImage = doc(
paragraph(
image({
alt: 'test-file',
canonicalSrc: 'test-file.png',
src: '/group1/project1/-/wikis/test-file.png',
}),
),
);
tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithDiagram.toJSON());
tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithImage.toJSON());
});
});
describe('createOrEditDiagram command', () => {
let editorFacade;
let assetResolver;
beforeEach(() => {
editorFacade = {};
assetResolver = {};
tiptapEditor.commands.createOrEditDiagram();
create.mockReturnValueOnce(editorFacade);
createAssetResolver.mockReturnValueOnce(assetResolver);
});
it('creates a new instance of asset resolver', () => {
expect(createAssetResolver).toHaveBeenCalledWith({ renderMarkdown });
});
it('creates a new instance of the content_editor_facade', () => {
expect(create).toHaveBeenCalledWith({
tiptapEditor,
drawioNodeName: DrawioDiagram.name,
uploadsPath,
assetResolver,
});
});
it('calls launchDrawioEditor and provides content_editor_facade', () => {
expect(launchDrawioEditor).toHaveBeenCalledWith({ editorFacade });
});
});
});

View File

@ -82,4 +82,14 @@ describe('content_editor/services/create_content_editor', () => {
renderMarkdown,
});
});
it('provides uploadsPath and renderMarkdown function to DrawioDiagram extension', () => {
expect(
editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'drawioDiagram')
.options,
).toMatchObject({
uploadsPath,
renderMarkdown,
});
});
});

View File

@ -8,6 +8,7 @@ import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
import FigureCaption from '~/content_editor/extensions/figure_caption';
@ -57,6 +58,7 @@ const {
div,
descriptionItem,
descriptionList,
drawioDiagram,
emoji,
footnoteDefinition,
footnoteReference,
@ -96,6 +98,7 @@ const {
detailsContent: { nodeType: DetailsContent.name },
descriptionItem: { nodeType: DescriptionItem.name },
descriptionList: { nodeType: DescriptionList.name },
drawioDiagram: { nodeType: DrawioDiagram.name },
emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name },
@ -397,6 +400,12 @@ this is not really json:table but just trying out whether this case works or not
);
});
it('correctly serializes a drawio_diagram', () => {
expect(
serialize(paragraph(drawioDiagram({ src: 'diagram.drawio.svg', alt: 'Draw.io Diagram' }))),
).toBe('![Draw.io Diagram](diagram.drawio.svg)');
});
it.each`
width | height | outputAttributes
${300} | ${undefined} | ${'width=300'}

View File

@ -20,6 +20,12 @@ export const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74"
</span>
</p>`;
export const PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
<a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.drawio.svg" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.drawio.svg">
<img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.drawio.svg" data-canonical-src="test-file.drawio.svg">
</a>
</p>`;
export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
<a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
</p>`;

View File

@ -17,6 +17,7 @@ import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
import Diagram from '~/content_editor/extensions/diagram';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
@ -218,6 +219,7 @@ export const createTiptapEditor = (extensions = []) =>
DescriptionList,
Details,
DetailsContent,
DrawioDiagram,
Diagram,
Emoji,
FootnoteDefinition,

View File

@ -0,0 +1,138 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { create } from '~/drawio/content_editor_facade';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import axios from '~/lib/utils/axios_utils';
import { PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML } from '../content_editor/test_constants';
import { createTestEditor } from '../content_editor/test_utils';
describe('drawio/contentEditorFacade', () => {
let tiptapEditor;
let axiosMock;
let contentEditorFacade;
let assetResolver;
const imageURL = '/group1/project1/-/wikis/test-file.drawio.svg';
const diagramSvg = '<svg></svg>';
const contentType = 'image/svg+xml';
const filename = 'test-file.drawio.svg';
const uploadsPath = '/uploads';
const canonicalSrc = '/new-diagram.drawio.svg';
const src = `/uploads${canonicalSrc}`;
beforeEach(() => {
assetResolver = {
resolveUrl: jest.fn(),
};
tiptapEditor = createTestEditor({ extensions: [DrawioDiagram] });
contentEditorFacade = create({
tiptapEditor,
drawioNodeName: DrawioDiagram.name,
uploadsPath,
assetResolver,
});
});
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
axiosMock.restore();
tiptapEditor.destroy();
});
describe('getDiagram', () => {
describe('when there is a selected diagram', () => {
beforeEach(() => {
tiptapEditor
.chain()
.setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML)
.setNodeSelection(1)
.run();
axiosMock
.onGet(imageURL)
.reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType });
});
it('returns diagram information', async () => {
const diagram = await contentEditorFacade.getDiagram();
expect(diagram).toEqual({
diagramURL: imageURL,
filename,
diagramSvg,
contentType,
});
});
});
describe('when there is not a selected diagram', () => {
beforeEach(() => {
tiptapEditor.chain().setContent('<p>text</p>').setNodeSelection(1).run();
});
it('returns null', async () => {
const diagram = await contentEditorFacade.getDiagram();
expect(diagram).toBe(null);
});
});
});
describe('updateDiagram', () => {
beforeEach(() => {
tiptapEditor
.chain()
.setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML)
.setNodeSelection(1)
.run();
assetResolver.resolveUrl.mockReturnValueOnce(src);
contentEditorFacade.updateDiagram({ uploadResults: { file_path: canonicalSrc } });
});
it('updates selected diagram diagram node src and canonicalSrc', () => {
tiptapEditor.commands.setNodeSelection(1);
expect(tiptapEditor.state.selection.node.attrs).toMatchObject({
src,
canonicalSrc,
});
});
});
describe('insertDiagram', () => {
beforeEach(() => {
tiptapEditor.chain().setContent('<p></p>').run();
assetResolver.resolveUrl.mockReturnValueOnce(src);
contentEditorFacade.insertDiagram({ uploadResults: { file_path: canonicalSrc } });
});
it('inserts a new draw.io diagram in the document', () => {
tiptapEditor.commands.setNodeSelection(1);
expect(tiptapEditor.state.selection.node.attrs).toMatchObject({
src,
canonicalSrc,
});
});
});
describe('uploadDiagram', () => {
it('sends a post request to the uploadsPath containing the diagram svg', async () => {
const link = { markdown: '![](diagram.drawio.svg)' };
const blob = new Blob([diagramSvg], { type: 'image/svg+xml' });
const formData = new FormData();
formData.append('file', blob, filename);
axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, {
data: {
link,
},
});
const response = await contentEditorFacade.uploadDiagram({ diagramSvg, filename });
expect(response).not.toBe(link);
});
});
});

View File

@ -4,6 +4,7 @@ import {
DRAWIO_FRAME_ID,
DIAGRAM_BACKGROUND_COLOR,
DRAWIO_IFRAME_TIMEOUT,
DIAGRAM_MAX_SIZE,
} from '~/drawio/constants';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
@ -14,8 +15,10 @@ jest.useFakeTimers();
describe('drawio/drawio_editor', () => {
let editorFacade;
let drawioIFrameReceivedMessages;
const diagramURL = `${window.location.origin}/uploads/diagram.drawio.svg`;
const testSvg = '<svg></svg>';
const testEncodedSvg = `data:image/svg+xml;base64,${btoa(testSvg)}`;
const filename = 'diagram.drawio.svg';
const findDrawioIframe = () => document.getElementById(DRAWIO_FRAME_ID);
const waitForDrawioIFrameMessage = ({ messageNumber = 1 } = {}) =>
@ -71,6 +74,10 @@ describe('drawio/drawio_editor', () => {
it('creates the drawio editor iframe and attaches it to the body', () => {
expect(findDrawioIframe().getAttribute('src')).toBe(DRAWIO_EDITOR_URL);
});
it('sets drawio-editor classname to the iframe', () => {
expect(findDrawioIframe().classList).toContain('drawio-editor');
});
});
describe(`when parent window does not receive configure event after ${DRAWIO_IFRAME_TIMEOUT} ms`, () => {
@ -88,7 +95,7 @@ describe('drawio/drawio_editor', () => {
jest.runAllTimers();
expect(createAlert).toHaveBeenCalledWith({
message: 'The draw.io editor could not be loaded.',
message: 'The diagrams.net editor could not be loaded.',
});
});
});
@ -149,10 +156,10 @@ describe('drawio/drawio_editor', () => {
describe('when there is a diagram selected', () => {
const diagramSvg = '<svg></svg>';
const filename = 'diagram.drawio.svg';
beforeEach(() => {
editorFacade.getDiagram.mockResolvedValueOnce({
diagramURL,
diagramSvg,
filename,
contentType: 'image/svg+xml',
@ -177,14 +184,43 @@ describe('drawio/drawio_editor', () => {
},
});
});
it('sets the drawio iframe as visible and resets cursor', async () => {
await waitForDrawioIFrameMessage();
expect(findDrawioIframe().style.visibility).toBe('visible');
expect(findDrawioIframe().style.cursor).toBe('');
});
it('scrolls window to the top', async () => {
await waitForDrawioIFrameMessage();
expect(window.scrollX).toBe(0);
});
});
describe('when there is an image selected that is not a diagram', () => {
describe.each`
description | errorMessage | diagram
${'when there is an image selected that is not an svg file'} | ${'The selected image is not a valid SVG diagram'} | ${{
diagramURL,
contentType: 'image/png',
filename: 'image.png',
}}
${'when the selected image is not an asset upload'} | ${'The selected image is not an asset uploaded in the application'} | ${{
diagramSvg: '<svg></svg>',
filename,
contentType: 'image/svg+xml',
diagramURL: 'https://example.com/image.drawio.svg',
}}
${'when the selected image is too large'} | ${'The selected image is too large.'} | ${{
diagramSvg: 'x'.repeat(DIAGRAM_MAX_SIZE + 1),
filename,
contentType: 'image/svg+xml',
diagramURL,
}}
`('$description', ({ errorMessage, diagram }) => {
beforeEach(() => {
editorFacade.getDiagram.mockResolvedValueOnce({
contentType: 'image/png',
filename: 'image.png',
});
editorFacade.getDiagram.mockResolvedValueOnce(diagram);
launchDrawioEditor({ editorFacade });
@ -193,7 +229,7 @@ describe('drawio/drawio_editor', () => {
it('displays an error alert indicating that the image is not a diagram', async () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'The selected image is not a diagram',
message: errorMessage,
error: expect.any(Error),
});
});
@ -214,7 +250,7 @@ describe('drawio/drawio_editor', () => {
it('displays an error alert indicating the failure', async () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Cannot load the diagram into the draw.io editor',
message: 'Cannot load the diagram into the diagrams.net editor',
error: expect.any(Error),
});
});

View File

@ -57,6 +57,7 @@ describe('drawio/textareaMarkdownEditor', () => {
);
expect(diagram).toEqual({
diagramURL: imageURL,
diagramMarkdown,
filename,
diagramSvg,

View File

@ -17,7 +17,7 @@ describe('CsvExportModal', () => {
...props,
},
provide: {
issuableType: 'issues',
issuableType: 'issue',
...injectedProperties,
},
stubs: {
@ -38,10 +38,10 @@ describe('CsvExportModal', () => {
describe('template', () => {
describe.each`
issuableType | modalTitle
${'issues'} | ${'Export issues'}
${'merge-requests'} | ${'Export merge requests'}
`('with the issuableType "$issuableType"', ({ issuableType, modalTitle }) => {
issuableType | modalTitle | dataTrackLabel
${'issue'} | ${'Export issues'} | ${'export_issues_csv'}
${'merge_request'} | ${'Export merge requests'} | ${'export_merge-requests_csv'}
`('with the issuableType "$issuableType"', ({ issuableType, modalTitle, dataTrackLabel }) => {
beforeEach(() => {
wrapper = createComponent({ injectedProperties: { issuableType } });
});
@ -57,9 +57,9 @@ describe('CsvExportModal', () => {
href: 'export/csv/path',
variant: 'confirm',
'data-method': 'post',
'data-qa-selector': `export_${issuableType}_button`,
'data-qa-selector': `export_issues_button`,
'data-track-action': 'click_button',
'data-track-label': `export_${issuableType}_csv`,
'data-track-label': dataTrackLabel,
},
});
});

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import Log from '~/jobs/components/log/log.vue';
import LogLineHeader from '~/jobs/components/log/line_header.vue';
import { logLinesParser } from '~/jobs/store/utils';
import { jobLog } from './mock_data';
@ -10,6 +11,7 @@ describe('Job Log', () => {
let actions;
let state;
let store;
let toggleCollapsibleLineMock;
Vue.use(Vuex);
@ -20,8 +22,9 @@ describe('Job Log', () => {
};
beforeEach(() => {
toggleCollapsibleLineMock = jest.fn();
actions = {
toggleCollapsibleLine: () => {},
toggleCollapsibleLine: toggleCollapsibleLineMock,
};
state = {
@ -37,11 +40,7 @@ describe('Job Log', () => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findCollapsibleLine = () => wrapper.find('.collapsible-line');
const findCollapsibleLine = () => wrapper.findComponent(LogLineHeader);
describe('line numbers', () => {
it('renders a line number for each open line', () => {
@ -68,11 +67,9 @@ describe('Job Log', () => {
describe('on click header section', () => {
it('calls toggleCollapsibleLine', () => {
jest.spyOn(wrapper.vm, 'toggleCollapsibleLine');
findCollapsibleLine().trigger('click');
expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled();
expect(toggleCollapsibleLineMock).toHaveBeenCalled();
});
});
});

View File

@ -29,10 +29,13 @@ password = <your personal access token>`;
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
const findSetupDocsLink = () => wrapper.findByTestId('pypi-docs-link');
function createComponent() {
function createComponent(props = {}) {
wrapper = mountExtended(PypiInstallation, {
propsData: {
packageEntity,
packageEntity: {
...packageEntity,
...props,
},
},
stubs: {
GlSprintf,
@ -86,6 +89,12 @@ password = <your personal access token>`;
});
});
it('does not have a link to personal access token docs when package is public', () => {
createComponent({ publicPackage: true });
expect(findAccessTokenLink().exists()).toBe(false);
});
it('has a link to the docs', () => {
expect(findSetupDocsLink().attributes()).toMatchObject({
href: PYPI_HELP_PATH,

View File

@ -147,6 +147,7 @@ export const packageData = (extend) => ({
conanUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/conan',
pypiUrl:
'http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple',
publicPackage: false,
pypiSetupUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/pypi',
...extend,
});

View File

@ -15,6 +15,8 @@ describe('ZenMode', () => {
let dropzoneForElementSpy;
const fixtureName = 'snippets/show.html';
const getTextarea = () => $('.notes-form textarea');
function enterZen() {
$('.notes-form .js-zen-enter').click();
}
@ -24,7 +26,7 @@ describe('ZenMode', () => {
}
function escapeKeydown() {
$('.notes-form textarea').trigger(
getTextarea().trigger(
$.Event('keydown', {
keyCode: 27,
}),
@ -50,6 +52,12 @@ describe('ZenMode', () => {
});
afterEach(() => {
$(document).off('click', '.js-zen-enter');
$(document).off('click', '.js-zen-leave');
$(document).off('zen_mode:enter');
$(document).off('zen_mode:leave');
$(document).off('keydown');
resetHTMLFixture();
});
@ -62,14 +70,14 @@ describe('ZenMode', () => {
$('.div-dropzone').addClass('js-invalid-dropzone');
exitZen();
expect(dropzoneForElementSpy.mock.calls.length).toEqual(0);
expect(dropzoneForElementSpy).not.toHaveBeenCalled();
});
it('should call dropzone if element is dropzone valid', () => {
$('.div-dropzone').removeClass('js-invalid-dropzone');
exitZen();
expect(dropzoneForElementSpy.mock.calls.length).toEqual(2);
expect(dropzoneForElementSpy).toHaveBeenCalledTimes(1);
});
});
@ -82,10 +90,10 @@ describe('ZenMode', () => {
});
it('removes textarea styling', () => {
$('.notes-form textarea').attr('style', 'height: 400px');
getTextarea().attr('style', 'height: 400px');
enterZen();
expect($('.notes-form textarea')).not.toHaveAttr('style');
expect(getTextarea()).not.toHaveAttr('style');
});
});
@ -116,4 +124,15 @@ describe('ZenMode', () => {
expect(utils.scrollToElement).toHaveBeenCalled();
});
});
it('restores textarea style', () => {
const style = 'color: red; overflow-y: hidden;';
getTextarea().attr('style', style);
expect(getTextarea()).toHaveAttr('style', style);
enterZen();
exitZen();
expect(getTextarea()).toHaveAttr('style', style);
});
});

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe PackagesHelper do
RSpec.describe PackagesHelper, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
@ -38,11 +38,18 @@ RSpec.describe PackagesHelper do
describe '#pypi_registry_url' do
let_it_be(:base_url_with_token) { base_url.sub('://', '://__token__:<your_personal_token>@') }
let_it_be(:public_project) { create(:project, :public) }
it 'returns the pypi registry url' do
url = helper.pypi_registry_url(1)
it 'returns the pypi registry url with token when project is private' do
url = helper.pypi_registry_url(project)
expect(url).to eq("#{base_url_with_token}projects/1/packages/pypi/simple")
expect(url).to eq("#{base_url_with_token}projects/#{project.id}/packages/pypi/simple")
end
it 'returns the pypi registry url without token when project is public' do
url = helper.pypi_registry_url(public_project)
expect(url).to eq("#{base_url}projects/#{public_project.id}/packages/pypi/simple")
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Checks::ChangesAccess do
RSpec.describe Gitlab::Checks::ChangesAccess, feature_category: :source_code_management do
include_context 'changes access checks context'
subject { changes_access }
@ -47,6 +47,16 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
expect(subject.commits).to match_array([])
end
context 'when change is for notes ref' do
let(:changes) do
[{ oldrev: oldrev, newrev: newrev, ref: 'refs/notes/commit' }]
end
it 'does not return any commits' do
expect(subject.commits).to match_array([])
end
end
context 'when changes contain empty revisions' do
let(:expected_commit) { instance_double(Commit) }

View File

@ -2,10 +2,20 @@
require 'spec_helper'
RSpec.describe Gitlab::Checks::DiffCheck do
RSpec.describe Gitlab::Checks::DiffCheck, feature_category: :source_code_management do
include_context 'change access checks context'
describe '#validate!' do
context 'when ref is not tag or branch ref' do
let(:ref) { 'refs/notes/commit' }
it 'does not call find_changed_paths' do
expect(project.repository).not_to receive(:find_changed_paths)
subject.validate!
end
end
context 'when commits is empty' do
it 'does not call find_changed_paths' do
expect(project.repository).not_to receive(:find_changed_paths)

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
RSpec.describe ErrorTracking::ProjectErrorTrackingSetting, feature_category: :error_tracking do
include ReactiveCachingHelpers
include Gitlab::Routing
@ -352,7 +352,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
context 'when sentry response is successful' do
before do
allow(sentry_client).to receive(:update_issue).with(opts).and_return(true)
allow(sentry_client).to receive(:update_issue).with(**opts).and_return(true)
end
it 'returns the successful response' do
@ -362,7 +362,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
context 'when sentry raises an error' do
before do
allow(sentry_client).to receive(:update_issue).with(opts).and_raise(StandardError)
allow(sentry_client).to receive(:update_issue).with(**opts).and_raise(StandardError)
end
it 'returns the successful response' do
@ -391,7 +391,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
setting.update!(sentry_project_id: nil)
allow(sentry_client).to receive(:projects).and_return(sentry_projects)
allow(sentry_client).to receive(:update_issue).with(opts).and_return(true)
allow(sentry_client).to receive(:update_issue).with(**opts).and_return(true)
end
it 'tries to backfill it from sentry API' do

View File

@ -314,6 +314,40 @@ RSpec.describe ProjectFeature, feature_category: :projects do
end
end
describe '#public_packages?' do
let_it_be(:public_project) { create(:project, :public) }
context 'with packages config enabled' do
context 'when project is private' do
it 'returns false' do
expect(project.project_feature.public_packages?).to eq(false)
end
context 'with package_registry_access_level set to public' do
before do
project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
end
it 'returns true' do
expect(project.project_feature.public_packages?).to eq(true)
end
end
end
context 'when project is public' do
it 'returns true' do
expect(public_project.project_feature.public_packages?).to eq(true)
end
end
end
it 'returns false if packages config is not enabled' do
stub_config(packages: { enabled: false })
expect(public_project.project_feature.public_packages?).to eq(false)
end
end
# rubocop:disable Gitlab/FeatureAvailableUsage
describe '#feature_available?' do
let(:features) { ProjectFeature::FEATURES }

View File

@ -270,6 +270,31 @@ RSpec.describe 'package details', feature_category: :package_registry do
it 'returns composer_config_repository_url correctly' do
expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
context 'with access to package registry for everyone' do
before do
project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
subject
end
it 'returns pypi_url correctly' do
expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
end
end
context 'when project is public' do
let_it_be(:public_project) { create(:project, :public, group: group) }
let_it_be(:composer_package) { create(:composer_package, project: public_project) }
let(:package_global_id) { global_id_of(composer_package) }
before do
subject
end
it 'returns pypi_url correctly' do
expect(graphql_data_at(:package, :pypi_url)).to eq("http://localhost/api/v4/projects/#{public_project.id}/packages/pypi/simple")
end
end
end
context 'web_path' do

View File

@ -40,6 +40,16 @@ RSpec.shared_examples 'edits content using the content editor' do
expect(page).to have_field('wiki[content]', with: value, type: 'hidden')
end
def display_media_bubble_menu(media_element_selector, fixture_file)
upload_asset fixture_file
wait_for_requests
expect(page).to have_css(media_element_selector)
page.find(media_element_selector).click
end
it 'saves page content in local storage if the user navigates away' do
switch_to_content_editor
@ -92,25 +102,45 @@ RSpec.shared_examples 'edits content using the content editor' do
open_insert_media_dropdown
end
def test_displays_media_bubble_menu(media_element_selector, fixture_file)
upload_asset fixture_file
wait_for_requests
expect(page).to have_css(media_element_selector)
page.find(media_element_selector).click
it 'displays correct media bubble menu for images', :js do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
expect_formatting_menu_to_be_hidden
expect_media_bubble_menu_to_be_visible
end
it 'displays correct media bubble menu for images', :js do
test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
it 'displays correct media bubble menu for video', :js do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
expect_formatting_menu_to_be_hidden
expect_media_bubble_menu_to_be_visible
end
end
describe 'diagrams.net editor' do
def click_edit_diagram_button
page.find('[data-testid="edit-diagram"]').click
end
it 'displays correct media bubble menu for video', :js do
test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
def expect_drawio_editor_is_opened
expect(page).to have_css('#drawio-frame', visible: :hidden)
end
before do
switch_to_content_editor
open_insert_media_dropdown
end
it 'displays correct media bubble menu with edit diagram button' do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'diagram.drawio.svg'
expect_formatting_menu_to_be_hidden
expect_media_bubble_menu_to_be_visible
click_edit_diagram_button
expect_drawio_editor_is_opened
end
end

View File

@ -94,7 +94,7 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
[
{
'name' => 'e2e:package-and-test',
'status' => 'success',
'status' => pipeline_bridge_state,
'downstream_pipeline' => {
'id' => '123',
'status' => package_and_qa_state
@ -103,6 +103,7 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
]
end
let(:pipeline_bridge_state) { 'running' }
let(:package_and_qa_state) { 'success' }
let(:parsed_response) do
@ -183,10 +184,10 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
it_behaves_like 'bypassing when flaky test or docs only'
end
context 'when package-and-test job is in manual state' do
let(:package_and_qa_state) { 'manual' }
context 'when package-and-test job is being created' do
let(:pipeline_bridge_state) { 'created' }
it_behaves_like 'with a failure', described_class::NEEDS_PACKAGE_AND_TEST_MESSAGE
it_behaves_like 'with a warning', described_class::WARN_PACKAGE_AND_TEST_MESSAGE
it_behaves_like 'bypassing when flaky test or docs only'
end
@ -197,6 +198,13 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
it_behaves_like 'bypassing when flaky test or docs only'
end
context 'when package-and-test job is in manual state' do
let(:package_and_qa_state) { 'manual' }
it_behaves_like 'with a failure', described_class::NEEDS_PACKAGE_AND_TEST_MESSAGE
it_behaves_like 'bypassing when flaky test or docs only'
end
context 'when package-and-test job is canceled' do
let(:package_and_qa_state) { 'canceled' }

View File

@ -69,7 +69,7 @@ module Tooling
fail PIPELINE_EXPEDITE_ERROR_MESSAGE if has_pipeline_expedite_label?
status = package_and_test_status
status = package_and_test_bridge_and_pipeline_status
if status.nil? || FAILING_PACKAGE_AND_TEST_STATUSES.include?(status) # rubocop:disable Style/GuardClause
fail NEEDS_PACKAGE_AND_TEST_MESSAGE
@ -91,15 +91,26 @@ module Tooling
!!stable_target_branch && !helper.security_mr?
end
def package_and_test_status
def package_and_test_bridge_and_pipeline_status
mr_head_pipeline_id = gitlab.mr_json.dig('head_pipeline', 'id')
return unless mr_head_pipeline_id
pipeline = package_and_test_pipeline(mr_head_pipeline_id)
bridge = package_and_test_bridge(mr_head_pipeline_id)
return unless pipeline
return unless bridge
pipeline['status']
if bridge['status'] == 'created'
bridge['status']
else
bridge.fetch('downstream_pipeline').fetch('status')
end
end
def package_and_test_bridge(mr_head_pipeline_id)
gitlab
.api
.pipeline_bridges(helper.mr_target_project_id, mr_head_pipeline_id)
&.find { |bridge| bridge['name'] == 'e2e:package-and-test' }
end
def stable_target_branch
@ -202,17 +213,6 @@ module Tooling
def version_to_minor_string(version)
"#{version[:major]}.#{version[:minor]}"
end
def package_and_test_pipeline(mr_head_pipeline_id)
package_and_test_bridge = gitlab
.api
.pipeline_bridges(helper.mr_target_project_id, mr_head_pipeline_id)
&.find { |bridge| bridge['name'] == 'e2e:package-and-test' }
return unless package_and_test_bridge
package_and_test_bridge['downstream_pipeline']
end
end
end
end

View File

@ -1221,10 +1221,10 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
"@gitlab/svgs@3.23.0":
version "3.23.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.23.0.tgz#92ed37ebd2058f1c1ed4651f86d4a20736790afb"
integrity sha512-rq6md86C+2AH75wk3zY0e+aPRRK1QuBdhNPex/Q7IfR8gm+kADhYj1GSS6bnU80rfG6Fk49xi6VpSHWRlQZ0Zg==
"@gitlab/svgs@3.24.0":
version "3.24.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.24.0.tgz#bc8265919aa04b06cd08be91637471bad195936d"
integrity sha512-R4s5qJUFUIbPflknpw1aI/PchiNq65vY7LVsJZnQkY+vi+AgmsETdut/AdferbGWmeWMU0q2wuVu9phE8lDUgA==
"@gitlab/ui@56.2.0":
version "56.2.0"