Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
807c4eae46
commit
3ff3d897d6
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
Gemfile
3
Gemfile
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) &&
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 existing’s 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;
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const create = ({ textArea, markdownPreviewPath, uploadsPath }) => ({
|
|||
const contentType = response.headers['content-type'];
|
||||
|
||||
return {
|
||||
diagramURL: imageURL,
|
||||
diagramMarkdown: imageMarkdown,
|
||||
filename,
|
||||
diagramSvg,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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_',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ query getPackageDetails(
|
|||
updatedAt
|
||||
status
|
||||
canDestroy
|
||||
publicPackage
|
||||
npmUrl
|
||||
mavenUrl
|
||||
conanUrl
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }) });
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -33,3 +33,5 @@ class DependencyProxy::Registry
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
::DependencyProxy::Registry.prepend_mod
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
6af890fe88f25be54d18cf3b3caa14830a3d627e7ff256d7a4ae03f9f1c7170c
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -231,37 +231,43 @@ configuration for jobs that use the Windows runner, like scripts, use <code>\
|
|||
|
||||
### 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 editor’s 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 editor’s 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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="<mxfile host="embed.diagrams.net" modified="2022-11-18T14:21:55.551Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" version="20.5.3" etag="cTK3wL1ch5_8VL-J45NP" type="embed"><diagram id="mWELjHy14aEMRdjyCi3_" name="Page-1">jZLBcoQgDIafhrvItPVcu+1eevLQMyOpMAPGYbFqn75Ygq7d2ZmeSL4kkPyEidrNb14O+h0VWFYWambihZUlr554PFayJFI9FAl03ihK2kFjvoFgThuNgsshMSDaYIYjbLHvoQ0HJr3H6Zj2ifb46iA7uAFNK+0t/TAq6D9TrPwMptP5ZV5QxMmcTOCipcLpCokTE7VHDMlycw12FS/rkupe70S3xjz04T8FZSr4knak2ZSRnZeO2gtLntnj2CtYywomnidtAjSDbNfoFH85Mh2cjR6PJt0KPsB8tzO+zRsXBdBB8EtMoQKRNaMdiSD50644fySmr9SuiEn65G67etchGiRFdnfJf2NXiytOPw==</diagram></mxfile>"
|
||||
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 |
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
});
|
||||
|
||||
it.each`
|
||||
width | height | outputAttributes
|
||||
${300} | ${undefined} | ${'width=300'}
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: '' };
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ describe('drawio/textareaMarkdownEditor', () => {
|
|||
);
|
||||
|
||||
expect(diagram).toEqual({
|
||||
diagramURL: imageURL,
|
||||
diagramMarkdown,
|
||||
filename,
|
||||
diagramSvg,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue