diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-23 09:09:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-23 09:09:23 +0300 |
commit | 8b4276f873461953ee5a1fc46f084779f5847e3a (patch) | |
tree | cc3435570e15234453e711c2ddcc9b0895d87eb4 /app | |
parent | f34b26bb882947bcc1126de19fa55eb8763af32e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
23 files changed, 520 insertions, 50 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 19ebab36481..36317444af9 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -164,7 +164,7 @@ export class CopyAsGFM { static nodeToGFM(node) { return Promise.all([ - import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'), + import(/* webpackChunkName: 'gfm_copy_extra' */ '@tiptap/pm/model'), import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'), import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'), ]) diff --git a/app/assets/javascripts/behaviors/markdown/schema.js b/app/assets/javascripts/behaviors/markdown/schema.js index 1b0f46ff4cb..31bab23c8b0 100644 --- a/app/assets/javascripts/behaviors/markdown/schema.js +++ b/app/assets/javascripts/behaviors/markdown/schema.js @@ -1,4 +1,4 @@ -import { Schema } from 'prosemirror-model'; +import { Schema } from '@tiptap/pm/model'; import editorExtensions from './editor_extensions'; const nodes = editorExtensions.nodes.reduce( diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue index 6456540a0dd..4d948f4ec05 100644 --- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue +++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue @@ -1,7 +1,7 @@ <script> import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'; -import { selectedRect as getSelectedRect } from '@_ueberdosis/prosemirror-tables'; +import { selectedRect as getSelectedRect } from '@tiptap/pm/tables'; import { __ } from '~/locale'; const TABLE_CELL_HEADER = 'th'; diff --git a/app/assets/javascripts/content_editor/extensions/attachment.js b/app/assets/javascripts/content_editor/extensions/attachment.js index 9634730f637..0d5b8e56a6c 100644 --- a/app/assets/javascripts/content_editor/extensions/attachment.js +++ b/app/assets/javascripts/content_editor/extensions/attachment.js @@ -1,5 +1,5 @@ import { Extension } from '@tiptap/core'; -import { Plugin, PluginKey } from 'prosemirror-state'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; import { handleFileEvent } from '../services/upload_helpers'; export default Extension.create({ diff --git a/app/assets/javascripts/content_editor/extensions/color_chip.js b/app/assets/javascripts/content_editor/extensions/color_chip.js index deb5029a1f0..c49b541bbaf 100644 --- a/app/assets/javascripts/content_editor/extensions/color_chip.js +++ b/app/assets/javascripts/content_editor/extensions/color_chip.js @@ -1,6 +1,6 @@ import { Node } from '@tiptap/core'; -import { Plugin, PluginKey } from 'prosemirror-state'; -import { Decoration, DecorationSet } from 'prosemirror-view'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; +import { Decoration, DecorationSet } from '@tiptap/pm/view'; import { isValidColorExpression } from '~/lib/utils/color_utils'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; diff --git a/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js b/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js index e940614083e..e48100c15a7 100644 --- a/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js +++ b/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js @@ -1,5 +1,5 @@ import { Extension } from '@tiptap/core'; -import { Plugin, PluginKey } from 'prosemirror-state'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; import { KEYDOWN_EVENT } from '../constants'; /** diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/paste_markdown.js index 848c4c12a9a..fa465160598 100644 --- a/app/assets/javascripts/content_editor/extensions/paste_markdown.js +++ b/app/assets/javascripts/content_editor/extensions/paste_markdown.js @@ -1,5 +1,5 @@ import { Extension } from '@tiptap/core'; -import { Plugin, PluginKey } from 'prosemirror-state'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; import { __ } from '~/locale'; import { VARIANT_DANGER } from '~/flash'; import createMarkdownDeserializer from '../services/gl_api_markdown_deserializer'; diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js index a9628c78add..eb53a3a61b3 100644 --- a/app/assets/javascripts/content_editor/extensions/suggestions.js +++ b/app/assets/javascripts/content_editor/extensions/suggestions.js @@ -2,7 +2,7 @@ import { Node } from '@tiptap/core'; import { VueRenderer } from '@tiptap/vue-2'; import tippy from 'tippy.js'; import Suggestion from '@tiptap/suggestion'; -import { PluginKey } from 'prosemirror-state'; +import { PluginKey } from '@tiptap/pm/state'; import { isFunction, uniqueId, memoize } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import { initEmojiMap, getAllEmoji } from '~/emoji'; diff --git a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js index 796dc06ad93..e65644d2c5c 100644 --- a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js @@ -1,4 +1,4 @@ -import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model'; +import { DOMParser as ProseMirrorDOMParser } from '@tiptap/pm/model'; import { replaceCommentsWith } from '~/lib/utils/dom_utils'; export default ({ render }) => { diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js index 28a50adca6b..c8972515c25 100644 --- a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js +++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js @@ -19,7 +19,7 @@ * visit-parents documentation: https://github.com/syntax-tree/unist-util-visit-parents */ -import { Mark } from 'prosemirror-model'; +import { Mark } from '@tiptap/pm/model'; import { visitParents, SKIP } from 'unist-util-visit-parents'; import { isFunction, isString, noop, mapValues } from 'lodash'; diff --git a/app/assets/javascripts/drawio/constants.js b/app/assets/javascripts/drawio/constants.js new file mode 100644 index 00000000000..a5f1d1e71d2 --- /dev/null +++ b/app/assets/javascripts/drawio/constants.js @@ -0,0 +1,13 @@ +/* + * TODO: Make this URL configurable + */ +export const DRAWIO_EDITOR_URL = + 'https://embed.diagrams.net/?ui=sketch&noSaveBtn=1&saveAndExit=1&keepmodified=1&spin=1&embed=1&libraries=1&configure=1&proto=json&toSvg=1'; // TODO Make it configurable + +export const DRAWIO_FRAME_ID = 'drawio-frame'; + +export const DARK_BACKGROUND_COLOR = '#202020'; + +export const DIAGRAM_BACKGROUND_COLOR = '#ffffff'; + +export const DRAWIO_IFRAME_TIMEOUT = 4000; diff --git a/app/assets/javascripts/drawio/drawio_editor.js b/app/assets/javascripts/drawio/drawio_editor.js new file mode 100644 index 00000000000..06e7f536426 --- /dev/null +++ b/app/assets/javascripts/drawio/drawio_editor.js @@ -0,0 +1,274 @@ +import _ from 'lodash'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { darkModeEnabled } from '~/lib/utils/color_utils'; +import { __ } from '~/locale'; +import { setAttributes } from '~/lib/utils/dom_utils'; +import { + DARK_BACKGROUND_COLOR, + DRAWIO_EDITOR_URL, + DRAWIO_FRAME_ID, + DIAGRAM_BACKGROUND_COLOR, + DRAWIO_IFRAME_TIMEOUT, +} from './constants'; + +function updateDrawioEditorState(drawIOEditorState, data) { + Object.assign(drawIOEditorState, data); +} + +function postMessageToDrawioEditor(drawIOEditorState, message) { + const { origin } = new URL(DRAWIO_EDITOR_URL); + + drawIOEditorState.iframe.contentWindow.postMessage(JSON.stringify(message), origin); +} + +function disposeDrawioEditor(drawIOEditorState) { + drawIOEditorState.disposeEventListener(); + drawIOEditorState.iframe.remove(); +} + +function getSvg(data) { + const svgPath = atob(data.substring(data.indexOf(',') + 1)); + + return `<?xml version="1.0" encoding="UTF-8"?>\n\ + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n\ + ${svgPath}`; +} + +async function saveDiagram(drawIOEditorState, editorFacade) { + const { newDiagram, diagramMarkdown, filename, diagramSvg } = drawIOEditorState; + const filenameWithExt = filename.endsWith('.drawio.svg') ? filename : `${filename}.drawio.svg`; + + postMessageToDrawioEditor(drawIOEditorState, { + action: 'spinner', + show: true, + messageKey: 'saving', + }); + + try { + const uploadResults = await editorFacade.uploadDiagram({ + filename: filenameWithExt, + diagramSvg, + }); + + if (newDiagram) { + editorFacade.insertDiagram({ uploadResults }); + } else { + editorFacade.updateDiagram({ diagramMarkdown, uploadResults }); + } + + createAlert({ + message: __('Diagram saved successfully.'), + variant: VARIANT_SUCCESS, + fadeTransition: true, + }); + setTimeout(() => disposeDrawioEditor(drawIOEditorState), 10); + } catch { + postMessageToDrawioEditor(drawIOEditorState, { action: 'spinner', show: false }); + postMessageToDrawioEditor(drawIOEditorState, { + action: 'dialog', + titleKey: 'error', + modified: true, + buttonKey: 'close', + messageKey: 'errorSavingFile', + }); + } +} + +function promptName(drawIOEditorState, name, errKey) { + postMessageToDrawioEditor(drawIOEditorState, { + action: 'prompt', + titleKey: 'filename', + okKey: 'save', + defaultValue: name || '', + }); + + if (errKey !== null) { + postMessageToDrawioEditor(drawIOEditorState, { + action: 'dialog', + titleKey: 'error', + messageKey: errKey, + buttonKey: 'ok', + }); + } +} + +function sendLoadDiagramMessage(drawIOEditorState) { + postMessageToDrawioEditor(drawIOEditorState, { + action: 'load', + xml: drawIOEditorState.diagramSvg, + border: 8, + background: DIAGRAM_BACKGROUND_COLOR, + dark: drawIOEditorState.dark, + title: drawIOEditorState.filename, + }); +} + +async function loadExistingDiagram(drawIOEditorState, editorFacade) { + let diagram = null; + + try { + diagram = await editorFacade.getDiagram(); + } catch (e) { + throw new Error(__('Cannot load the diagram into the draw.io editor')); + } + + if (diagram) { + const { diagramMarkdown, filename, diagramSvg, contentType } = diagram; + + if (contentType !== 'image/svg+xml') { + throw new Error(__('The selected image is not a diagram')); + } + + updateDrawioEditorState(drawIOEditorState, { + newDiagram: false, + filename, + diagramMarkdown, + diagramSvg, + }); + } else { + updateDrawioEditorState(drawIOEditorState, { + newDiagram: true, + }); + } + + sendLoadDiagramMessage(drawIOEditorState); +} + +async function prepareEditor(drawIOEditorState, editorFacade) { + const { iframe } = drawIOEditorState; + + iframe.style.cursor = 'wait'; + + try { + await loadExistingDiagram(drawIOEditorState, editorFacade); + + iframe.style.visibility = ''; + iframe.style.cursor = ''; + window.scrollTo(0, 0); + } catch (e) { + createAlert({ + message: e.message, + error: e, + }); + disposeDrawioEditor(drawIOEditorState); + } +} + +function configureDrawIOEditor(drawIOEditorState) { + postMessageToDrawioEditor(drawIOEditorState, { + action: 'configure', + config: { + darkColor: DARK_BACKGROUND_COLOR, + settingsName: 'gitlab', + }, + colorSchemeMeta: drawIOEditorState.dark, // For transparent iframe background in dark mode + }); + updateDrawioEditorState(drawIOEditorState, { + initialized: true, + }); +} + +function onDrawIOEditorMessage(drawIOEditorState, editorFacade, evt) { + if (_.isNil(evt) || evt.source !== drawIOEditorState.iframe.contentWindow) { + return; + } + + const msg = JSON.parse(evt.data); + + if (msg.event === 'configure') { + configureDrawIOEditor(drawIOEditorState); + } else if (msg.event === 'init') { + prepareEditor(drawIOEditorState, editorFacade); + } else if (msg.event === 'exit') { + disposeDrawioEditor(drawIOEditorState); + } else if (msg.event === 'prompt') { + updateDrawioEditorState(drawIOEditorState, { + filename: msg.value, + }); + + if (!drawIOEditorState.filename) { + promptName(drawIOEditorState, 'diagram.drawio.svg', 'filenameShort'); + } else { + saveDiagram(drawIOEditorState, editorFacade); + } + } else if (msg.event === 'export') { + updateDrawioEditorState(drawIOEditorState, { + diagramSvg: getSvg(msg.data), + }); + // TODO Add this to draw.io editor configuration + sendLoadDiagramMessage(drawIOEditorState); // Save removes diagram from the editor, so we need to reload it. + postMessageToDrawioEditor(drawIOEditorState, { action: 'status', modified: true }); // And set editor modified flag to true. + if (!drawIOEditorState.filename) { + promptName(drawIOEditorState, 'diagram.drawio.svg', null); + } else { + saveDiagram(drawIOEditorState, editorFacade); + } + } +} + +function createEditorIFrame(drawIOEditorState) { + const iframe = document.createElement('iframe'); + + setAttributes(iframe, { + id: DRAWIO_FRAME_ID, + src: DRAWIO_EDITOR_URL, + }); + + 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.') }); + } + }, DRAWIO_IFRAME_TIMEOUT); + + updateDrawioEditorState(drawIOEditorState, { + iframe, + }); +} + +function attachDrawioIFrameMessageListener(drawIOEditorState, editorFacade) { + const evtHandler = (evt) => { + onDrawIOEditorMessage(drawIOEditorState, editorFacade, evt); + }; + + window.addEventListener('message', evtHandler); + + // Stores a function in the editor state object that allows disposing + // the message event listener when the editor exits. + updateDrawioEditorState(drawIOEditorState, { + disposeEventListener: () => { + window.removeEventListener('message', evtHandler); + }, + }); +} + +const createDrawioEditorState = ({ filename = null }) => ({ + newDiagram: true, + filename, + diagramSvg: null, + diagramMarkdown: null, + iframe: null, + isBusy: false, + initialized: false, + dark: darkModeEnabled(), + disposeEventListener: null, +}); + +export function launchDrawioEditor({ editorFacade, filename }) { + const drawIOEditorState = createDrawioEditorState({ filename }); + + // The execution order of these two functions matter + attachDrawioIFrameMessageListener(drawIOEditorState, editorFacade); + createEditorIFrame(drawIOEditorState); +} diff --git a/app/assets/javascripts/drawio/markdown_field_editor_facade.js b/app/assets/javascripts/drawio/markdown_field_editor_facade.js new file mode 100644 index 00000000000..b2506ce6bf8 --- /dev/null +++ b/app/assets/javascripts/drawio/markdown_field_editor_facade.js @@ -0,0 +1,71 @@ +import { insertMarkdownText, resolveSelectedImage } from '~/lib/utils/text_markdown'; +import axios from '~/lib/utils/axios_utils'; + +/** + * A set of functions to decouple the markdown_field 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 Markdown Field implementation details + * + * @param {Object} params Factory function parameters + * @param {Object} params.textArea Textarea used to edit and display markdown source + * @param {String} params.markdownPreviewPath API endpoint to render Markdown + * @param {String} params.uploadsPath API endpoint to upload files + * + * @returns A markdown_field_facade object with operations + * with operations to get a selected diagram, upload a diagram, + * insert a new one in the Markdown Field, and update + * an existing’s diagram URL. + */ +export const create = ({ textArea, markdownPreviewPath, uploadsPath }) => ({ + getDiagram: async () => { + const image = await resolveSelectedImage(textArea, markdownPreviewPath); + + if (!image) { + return null; + } + + const { imageURL, imageMarkdown, filename } = image; + const response = await axios.get(imageURL, { responseType: 'text' }); + const diagramSvg = response.data; + const contentType = response.headers['content-type']; + + return { + diagramMarkdown: imageMarkdown, + filename, + diagramSvg, + contentType, + }; + }, + updateDiagram: ({ uploadResults, diagramMarkdown }) => { + textArea.focus(); + + // eslint-disable-next-line no-param-reassign + textArea.value = textArea.value.replace(diagramMarkdown, uploadResults.link.markdown); + textArea.dispatchEvent(new Event('input')); + }, + insertDiagram: ({ uploadResults }) => { + textArea.focus(); + const markdown = textArea.value; + const selectedMD = markdown.substring(textArea.selectionStart, textArea.selectionEnd); + + // This method dispatches the input event. + insertMarkdownText({ + textArea, + text: markdown, + tag: uploadResults.link.markdown, + selected: selectedMD, + }); + }, + 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; + }, +}); diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 05ed08931bb..ebc118c0d87 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Shortcuts from '~/behaviors/shortcuts/shortcuts'; import { insertText } from '~/lib/utils/common_utils'; +import axios from '~/lib/utils/axios_utils'; const LINK_TAG_PATTERN = '[{text}](url)'; const INDENT_CHAR = ' '; @@ -669,3 +670,50 @@ export function removeMarkdownListeners(form) { // eslint-disable-next-line @gitlab/no-global-event-off return $('.js-md', form).off('click'); } + +/** + * If the textarea cursor is positioned in a Markdown image declaration, + * it uses the Markdown API to resolve the image’s absolute URL. + * @param {Object} textarea Textarea DOM element + * @param {String} markdownPreviewPath Markdown API path + * @returns {Object} an object containing the image’s absolute URL, filename, + * and the markdown declaration. If the textarea cursor is not positioned + * in an image, it returns null. + */ +export const resolveSelectedImage = async (textArea, markdownPreviewPath = '') => { + const { lines, startPos } = linesFromSelection(textArea); + + // image declarations can’t span more than one line in Markdown + if (lines > 0) { + return null; + } + + const selectedLine = lines[0]; + + if (!/!\[.+?\]\(.+?\)/.test(selectedLine)) return null; + + const lineSelectionStart = textArea.selectionStart - startPos; + const preExlm = selectedLine.substring(0, lineSelectionStart).lastIndexOf('!'); + const postClose = selectedLine.substring(lineSelectionStart).indexOf(')'); + + if (preExlm >= 0 && postClose >= 0) { + const imageMarkdown = selectedLine.substring(preExlm, lineSelectionStart + postClose + 1); + const { data } = await axios.post(markdownPreviewPath, { text: imageMarkdown }); + const parser = new DOMParser(); + + const dom = parser.parseFromString(data.body, 'text/html'); + const imageURL = dom.body.querySelector('a').getAttribute('href'); + + if (imageURL) { + const filename = imageURL.substring(imageURL.lastIndexOf('/') + 1); + + return { + imageMarkdown, + imageURL, + filename, + }; + } + } + + return null; +}; diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue index 0d2bbfbbc43..549c964cce4 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -351,6 +351,7 @@ export default { :enable-content-editor="isMarkdownFormat" :enable-preview="isMarkdownFormat" :autofocus="pageInfo.persisted" + :drawio-enabled="true" @contentEditor="notifyContentEditorActive" @markdownField="notifyContentEditorInactive" @keydown.ctrl.enter="submitFormShortcut" diff --git a/app/assets/javascripts/vue_shared/components/markdown/drawio_toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/drawio_toolbar_button.vue new file mode 100644 index 00000000000..a66becb5c92 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/drawio_toolbar_button.vue @@ -0,0 +1,48 @@ +<script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { launchDrawioEditor } from '~/drawio/drawio_editor'; +import { create } from '~/drawio/markdown_field_editor_facade'; + +export default { + components: { + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + uploadsPath: { + type: String, + required: true, + }, + markdownPreviewPath: { + type: String, + required: true, + }, + }, + methods: { + getTextArea() { + return document.querySelector('.js-gfm-input'); + }, + launchDrawioEditor() { + launchDrawioEditor({ + editorFacade: create({ + uploadsPath: this.uploadsPath, + textArea: this.getTextArea(), + markdownPreviewPath: this.markdownPreviewPath, + }), + }); + }, + }, +}; +</script> +<template> + <gl-button + v-gl-tooltip + :title="__('Insert or edit diagram')" + :aria-label="__('Insert or edit diagram')" + category="tertiary" + icon="diagram" + @click="launchDrawioEditor" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 6f4cddbdfa2..bca7d90135e 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -132,6 +132,11 @@ export default { required: false, default: false, }, + drawioEnabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -355,6 +360,10 @@ export default { :enable-preview="enablePreview" :show-suggest-popover="showSuggestPopover" :suggestion-start-index="suggestionsStartIndex" + :uploads-path="uploadsPath" + :markdown-preview-path="markdownPreviewPath" + :drawio-enabled="drawioEnabled" + data-testid="markdownHeader" :restricted-tool-bar-items="restrictedToolBarItems" @preview-markdown="showPreviewTab" @write-markdown="showWriteTab" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index e83441e59a2..5d692c69fc0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -15,6 +15,7 @@ import { getSelectedFragment } from '~/lib/utils/common_utils'; import { s__, __ } from '~/locale'; import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; import ToolbarButton from './toolbar_button.vue'; +import DrawioToolbarButton from './drawio_toolbar_button.vue'; export default { components: { @@ -23,6 +24,7 @@ export default { GlButton, GlTabs, GlTab, + DrawioToolbarButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -62,6 +64,21 @@ export default { required: false, default: () => [], }, + uploadsPath: { + type: String, + required: false, + default: '', + }, + markdownPreviewPath: { + type: String, + required: false, + default: '', + }, + drawioEnabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -369,8 +386,12 @@ export default { icon="paperclip" @click="handleAttachFile" /> + <drawio-toolbar-button + v-if="drawioEnabled" + :uploads-path="uploadsPath" + :markdown-preview-path="markdownPreviewPath" + /> <toolbar-button - v-if="!restrictedToolBarItems.includes('full-screen')" class="js-zen-enter" :prepend="true" :button-title="__('Go full screen')" diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index 7e6b0e4a63b..2a82986e90b 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -76,6 +76,11 @@ export default { required: false, default: false, }, + drawioEnabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -147,6 +152,7 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :uploads-path="uploadsPath" :enable-preview="enablePreview" + :drawio-enabled="drawioEnabled" show-content-editor-switcher class="bordered-box" @enableContentEditor="onEditingModeChange('contentEditor')" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue index 6974804523a..80802cb3858 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue @@ -70,7 +70,7 @@ export default { <item-milestone v-if="milestone" :milestone="milestone" - class="gl-display-flex gl-align-items-center gl-mr-5 gl-max-w-15 gl-text-secondary! gl-cursor-help! gl-text-decoration-none!" + class="gl-display-flex gl-align-items-center gl-mr-5 gl-max-w-15 gl-line-height-normal gl-text-secondary! gl-cursor-help! gl-text-decoration-none!" /> <gl-avatars-inline v-if="assignees.length" @@ -97,7 +97,7 @@ export default { :background-color="label.color" :description="label.description" :scoped="showScopedLabel(label)" - class="gl-mt-3 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm" + class="gl-my-2 gl-mr-2 gl-mb-auto gl-label-sm" tooltip-placement="top" /> </div> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index acd2792a806..e2956c18678 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -63,6 +63,9 @@ export default { id: this.issuableGid, }; }, + context: { + isSingleRequest: true, + }, skip() { return !this.issuableId; }, diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql index 7d7bb9c7fc5..b9d4bb29bbf 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql @@ -1,40 +1,7 @@ +#import "./work_item.fragment.graphql" + query workItemLinksQuery($id: WorkItemID!) { workItem(id: $id) { - id - workItemType { - id - name - } - title - userPermissions { - deleteWorkItem - updateWorkItem - } - confidential - widgets { - type - ... on WorkItemWidgetHierarchy { - type - parent { - id - } - children { - nodes { - id - iid - confidential - workItemType { - id - name - iconName - } - title - state - createdAt - closedAt - } - } - } - } + ...WorkItem } } diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb index 46303cefb60..0e160ce2ea0 100644 --- a/app/controllers/concerns/wiki_actions.rb +++ b/app/controllers/concerns/wiki_actions.rb @@ -11,6 +11,15 @@ module WikiActions RESCUE_GIT_TIMEOUTS_IN = %w[show edit history diff pages].freeze included do + content_security_policy do |p| + next if p.directives.blank? + + default_frame_src = p.directives['frame-src'] || p.directives['default-src'] + frame_src_values = Array.wrap(default_frame_src) | ['https://embed.diagrams.net'].compact + + p.frame_src(*frame_src_values) + end + before_action { respond_to :html } before_action :authorize_read_wiki! |