From 3ff3d897d6529aabb21aa6aed54eb430a9cf0fe2 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 7 Mar 2023 18:07:59 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/package-and-test/main.gitlab-ci.yml | 2 +- .gitlab/ci/review-apps/qa.gitlab-ci.yml | 2 +- Gemfile | 3 - Gemfile.checksum | 1 - Gemfile.lock | 3 - .../components/alerts_settings_wrapper.vue | 2 +- .../alerts_settings/utils/cache_updates.js | 2 +- .../bubble_menus/formatting_bubble_menu.vue | 3 +- .../components/bubble_menus/media_bubble_menu.vue | 35 +++++- .../components/toolbar_more_dropdown.vue | 4 + .../javascripts/content_editor/constants/index.js | 1 + .../content_editor/extensions/drawio_diagram.js | 41 ++++++ .../javascripts/content_editor/extensions/image.js | 4 +- .../services/create_content_editor.js | 2 + .../content_editor/services/markdown_serializer.js | 5 + .../content_editor/services/upload_helpers.js | 36 ++++-- app/assets/javascripts/drawio/constants.js | 2 + .../javascripts/drawio/content_editor_facade.js | 80 ++++++++++++ app/assets/javascripts/drawio/drawio_editor.js | 31 ++--- .../drawio/markdown_field_editor_facade.js | 1 + .../components/new_environments_dropdown.vue | 2 +- .../issuable/components/csv_export_modal.vue | 13 +- .../components/csv_import_export_buttons.vue | 4 +- app/assets/javascripts/issuable/constants.js | 10 -- .../components/incidents/create_timeline_event.vue | 2 +- .../components/incidents/timeline_events_form.vue | 6 +- .../components/details/pypi_installation.vue | 5 +- .../queries/get_package_details.query.graphql | 1 + .../pages/projects/merge_requests/index/index.js | 3 +- .../branch_rules/components/view/index.vue | 4 +- .../set_status_modal/set_status_modal_wrapper.vue | 2 +- .../confidential/sidebar_confidentiality_form.vue | 2 +- .../sidebar_confidentiality_widget.vue | 2 +- .../components/date/sidebar_date_widget.vue | 2 +- .../incidents/sidebar_escalation_status.vue | 2 +- .../labels/labels_select_vue/store/actions.js | 2 +- .../dropdown_contents_create_view.vue | 2 +- .../dropdown_contents_labels_view.vue | 2 +- .../labels_select_widget/labels_select_root.vue | 2 +- .../sidebar/components/lock/edit_form_buttons.vue | 6 +- .../sidebar/components/lock/issuable_lock_form.vue | 6 +- .../sidebar/components/sidebar_dropdown.vue | 2 +- .../sidebar/components/sidebar_dropdown_widget.vue | 2 +- .../subscriptions/sidebar_subscriptions_widget.vue | 2 +- .../javascripts/snippets/components/edit.vue | 8 +- .../snippets/components/snippet_blob_edit.vue | 6 +- .../snippets/components/snippet_header.vue | 2 +- app/assets/javascripts/zen_mode.js | 8 ++ app/assets/stylesheets/page_bundles/wiki.scss | 17 +++ app/graphql/types/packages/package_details_type.rb | 4 +- app/helpers/packages_helper.rb | 11 +- app/models/dependency_proxy/registry.rb | 2 + .../project_error_tracking_setting.rb | 2 +- app/models/project_feature.rb | 6 + .../issues/service_desk/_nav_btns.html.haml | 2 +- .../projects/merge_requests/_nav_btns.html.haml | 2 +- app/views/shared/empty_states/_issues.html.haml | 3 +- ...edule_temporary_partitioning_indexes_removal.rb | 32 +++++ db/schema_migrations/20230303120531 | 1 + doc/architecture/blueprints/runner_tokens/index.md | 5 +- doc/ci/pipelines/downstream_pipelines.md | 42 +++++-- doc/development/database/required_stops.md | 30 +++++ doc/user/group/saml_sso/index.md | 35 +++--- doc/user/markdown.md | 28 ++++- lib/gitlab/checks/base_single_checker.rb | 2 +- lib/gitlab/checks/changes_access.rb | 10 +- lib/gitlab/checks/diff_check.rb | 4 + lib/gitlab/checks/single_change_access.rb | 10 ++ locale/gitlab.pot | 28 ++++- package.json | 2 +- spec/fixtures/diagram.drawio.svg | 38 ++++++ .../bubble_menus/media_bubble_menu_spec.js | 93 ++++++++++---- .../components/toolbar_more_dropdown_spec.js | 21 ++-- .../content_editor/extensions/attachment_spec.js | 19 ++- .../extensions/drawio_diagram_spec.js | 103 +++++++++++++++ .../services/create_content_editor_spec.js | 10 ++ .../services/markdown_serializer_spec.js | 9 ++ spec/frontend/content_editor/test_constants.js | 6 + spec/frontend/content_editor/test_utils.js | 2 + spec/frontend/drawio/content_editor_facade_spec.js | 138 +++++++++++++++++++++ spec/frontend/drawio/drawio_editor_spec.js | 54 ++++++-- .../drawio/markdown_field_editor_facade_spec.js | 1 + .../issuable/components/csv_export_modal_spec.js | 14 +-- spec/frontend/jobs/components/log/log_spec.js | 15 +-- .../components/details/pypi_installation_spec.js | 13 +- .../package_registry/mock_data.js | 1 + spec/frontend/zen_mode_spec.js | 29 ++++- spec/helpers/packages_helper_spec.rb | 15 ++- spec/lib/gitlab/checks/changes_access_spec.rb | 12 +- spec/lib/gitlab/checks/diff_check_spec.rb | 12 +- .../project_error_tracking_setting_spec.rb | 8 +- spec/models/project_feature_spec.rb | 34 +++++ spec/requests/api/graphql/packages/package_spec.rb | 25 ++++ .../features/content_editor_shared_examples.rb | 50 ++++++-- spec/tooling/danger/stable_branch_spec.rb | 16 ++- tooling/danger/stable_branch.rb | 32 ++--- yarn.lock | 8 +- 97 files changed, 1158 insertions(+), 268 deletions(-) create mode 100644 app/assets/javascripts/content_editor/extensions/drawio_diagram.js create mode 100644 app/assets/javascripts/drawio/content_editor_facade.js create mode 100644 db/post_migrate/20230303120531_schedule_temporary_partitioning_indexes_removal.rb create mode 100644 db/schema_migrations/20230303120531 create mode 100644 spec/fixtures/diagram.drawio.svg create mode 100644 spec/frontend/content_editor/extensions/drawio_diagram_spec.js create mode 100644 spec/frontend/drawio/content_editor_facade_spec.js diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 9bcbb780dae..966c1c2f502 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -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 diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index 6c81bc1972b..12a7ddebc45 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.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 diff --git a/Gemfile b/Gemfile index 1a55f9e1e3c..dd8f0245d88 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.checksum b/Gemfile.checksum index f70a84109a5..38a57d2b85c 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -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"}, diff --git a/Gemfile.lock b/Gemfile.lock index 36440bde526..9ae73215463 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue index 7dd33da435a..cc8913c2f45 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -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'; diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js index 2e64312b0e0..e03ebffd17a 100644 --- a/app/assets/javascripts/alerts_settings/utils/cache_updates.js +++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js @@ -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'; diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue index 354db88f11c..06b80a65528 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue @@ -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)) && diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue index 310bb1be81f..a14d49922fb 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue @@ -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" > @@ -240,6 +260,19 @@ export default { @click="startEditingMedia" /> + this.insert('diagram', { language: 'plantuml' }), }, + { + text: __('Create or edit diagram'), + action: () => this.execute('createOrEditDiagram', 'drawioDiagram'), + }, { text: __('Table of contents'), action: () => this.execute('insertTableOfContents', 'tableOfContents'), diff --git a/app/assets/javascripts/content_editor/constants/index.js b/app/assets/javascripts/content_editor/constants/index.js index 14862727811..6a3740a5952 100644 --- a/app/assets/javascripts/content_editor/constants/index.js +++ b/app/assets/javascripts/content_editor/constants/index.js @@ -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; diff --git a/app/assets/javascripts/content_editor/extensions/drawio_diagram.js b/app/assets/javascripts/content_editor/extensions/drawio_diagram.js new file mode 100644 index 00000000000..8c3012ecf59 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/drawio_diagram.js @@ -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 }), + }), + }); + }, + }; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js index fc4c108b773..58c16297886 100644 --- a/app/assets/javascripts/content_editor/extensions/image.js +++ b/app/assets/javascripts/content_editor/extensions/image.js @@ -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', }, { diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index 61c6be574d0..c9ed0bb2757 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -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, diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 4e29f85004b..e27a427372c 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -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(); diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js index 09f0738b51b..abfb73183dd 100644 --- a/app/assets/javascripts/content_editor/services/upload_helpers.js +++ b/app/assets/javascripts/content_editor/services/upload_helpers.js @@ -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; diff --git a/app/assets/javascripts/drawio/constants.js b/app/assets/javascripts/drawio/constants.js index a5f1d1e71d2..2e1e074db3b 100644 --- a/app/assets/javascripts/drawio/constants.js +++ b/app/assets/javascripts/drawio/constants.js @@ -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 diff --git a/app/assets/javascripts/drawio/content_editor_facade.js b/app/assets/javascripts/drawio/content_editor_facade.js new file mode 100644 index 00000000000..1c41194c1f5 --- /dev/null +++ b/app/assets/javascripts/drawio/content_editor_facade.js @@ -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; + }, +}); diff --git a/app/assets/javascripts/drawio/drawio_editor.js b/app/assets/javascripts/drawio/drawio_editor.js index 06e7f536426..9668c2835ce 100644 --- a/app/assets/javascripts/drawio/drawio_editor.js +++ b/app/assets/javascripts/drawio/drawio_editor.js @@ -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); diff --git a/app/assets/javascripts/drawio/markdown_field_editor_facade.js b/app/assets/javascripts/drawio/markdown_field_editor_facade.js index b2506ce6bf8..4ef203c7aa0 100644 --- a/app/assets/javascripts/drawio/markdown_field_editor_facade.js +++ b/app/assets/javascripts/drawio/markdown_field_editor_facade.js @@ -32,6 +32,7 @@ export const create = ({ textArea, markdownPreviewPath, uploadsPath }) => ({ const contentType = response.headers['content-type']; return { + diagramURL: imageURL, diagramMarkdown: imageMarkdown, filename, diagramSvg, diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue index 89400bc4742..420c34a88f1 100644 --- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue +++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue @@ -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'; diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue index 736da92fa9f..c1de507cd80 100644 --- a/app/assets/javascripts/issuable/components/csv_export_modal.vue +++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue @@ -1,7 +1,7 @@