Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
764ecdaf4d
commit
cb25fb1a8e
11 changed files with 534 additions and 99 deletions
|
@ -62,7 +62,8 @@ class ApplicationController < ActionController::Base
|
||||||
:bitbucket_import_enabled?, :bitbucket_import_configured?,
|
:bitbucket_import_enabled?, :bitbucket_import_configured?,
|
||||||
:bitbucket_server_import_enabled?, :fogbugz_import_enabled?,
|
:bitbucket_server_import_enabled?, :fogbugz_import_enabled?,
|
||||||
:git_import_enabled?, :gitlab_project_import_enabled?,
|
:git_import_enabled?, :gitlab_project_import_enabled?,
|
||||||
:manifest_import_enabled?, :phabricator_import_enabled?
|
:manifest_import_enabled?, :phabricator_import_enabled?,
|
||||||
|
:masked_page_url
|
||||||
|
|
||||||
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
|
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
|
||||||
# concerns due to caching private data.
|
# concerns due to caching private data.
|
||||||
|
|
|
@ -16,6 +16,7 @@ module GitlabRoutingHelper
|
||||||
include ::Routing::SnippetsHelper
|
include ::Routing::SnippetsHelper
|
||||||
include ::Routing::WikiHelper
|
include ::Routing::WikiHelper
|
||||||
include ::Routing::GraphqlHelper
|
include ::Routing::GraphqlHelper
|
||||||
|
include ::Routing::PseudonymizationHelper
|
||||||
included do
|
included do
|
||||||
Gitlab::Routing.includes_helpers(self)
|
Gitlab::Routing.includes_helpers(self)
|
||||||
end
|
end
|
||||||
|
|
50
app/helpers/routing/pseudonymization_helper.rb
Normal file
50
app/helpers/routing/pseudonymization_helper.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Routing
|
||||||
|
module PseudonymizationHelper
|
||||||
|
def masked_page_url
|
||||||
|
return unless Feature.enabled?(:mask_page_urls, type: :ops)
|
||||||
|
|
||||||
|
mask_params(Rails.application.routes.recognize_path(request.original_fullpath))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mask_params(request_params)
|
||||||
|
return if request_params[:action] == 'new'
|
||||||
|
|
||||||
|
namespace_type = request_params[:controller].split('/')[1]
|
||||||
|
|
||||||
|
namespace_type.present? ? url_with_namespace_type(request_params, namespace_type) : url_without_namespace_type(request_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_without_namespace_type(request_params)
|
||||||
|
masked_url = "#{request.protocol}#{request.host_with_port}/"
|
||||||
|
|
||||||
|
masked_url += case request_params[:controller]
|
||||||
|
when 'groups'
|
||||||
|
"namespace:#{group.id}/"
|
||||||
|
when 'projects'
|
||||||
|
"namespace:#{project.namespace.id}/project:#{project.id}/"
|
||||||
|
when 'root'
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
masked_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_with_namespace_type(request_params, namespace_type)
|
||||||
|
masked_url = "#{request.protocol}#{request.host_with_port}/"
|
||||||
|
|
||||||
|
if request_params.has_key?(:project_id)
|
||||||
|
masked_url += "namespace:#{project.namespace.id}/project:#{project.id}/-/#{namespace_type}/"
|
||||||
|
end
|
||||||
|
|
||||||
|
if request_params.has_key?(:id)
|
||||||
|
masked_url += namespace_type == 'blob' ? ':repository_path' : request_params[:id]
|
||||||
|
end
|
||||||
|
|
||||||
|
masked_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
8
config/feature_flags/ops/mask_page_urls.yml
Normal file
8
config/feature_flags/ops/mask_page_urls.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: mask_page_urls
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69448
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340181
|
||||||
|
milestone: '14.3'
|
||||||
|
type: ops
|
||||||
|
group: group::product intelligence
|
||||||
|
default_enabled: false
|
|
@ -161,6 +161,10 @@ deprovisions
|
||||||
dequarantine
|
dequarantine
|
||||||
dequarantined
|
dequarantined
|
||||||
dequarantining
|
dequarantining
|
||||||
|
deserialization
|
||||||
|
deserialize
|
||||||
|
deserializers
|
||||||
|
deserializes
|
||||||
DevOps
|
DevOps
|
||||||
Dhall
|
Dhall
|
||||||
disambiguates
|
disambiguates
|
||||||
|
|
|
@ -199,7 +199,7 @@ Example responses:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) also see the `new_epic`
|
Users on [GitLab Ultimate](https://about.gitlab.com/pricing/) also see the `new_epic`
|
||||||
parameter:
|
parameter:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
@ -23,11 +23,20 @@ There are two types of redirects:
|
||||||
- [GitLab Pages redirects](../../user/project/pages/redirects.md),
|
- [GitLab Pages redirects](../../user/project/pages/redirects.md),
|
||||||
for users who view the docs on [`docs.gitlab.com`](https://docs.gitlab.com).
|
for users who view the docs on [`docs.gitlab.com`](https://docs.gitlab.com).
|
||||||
|
|
||||||
The Technical Writing team manages the [process](https://gitlab.com/gitlab-org/technical-writing/-/blob/main/.gitlab/issue_templates/tw-monthly-tasks.md)
|
The Technical Writing team manages the [process](https://gitlab.com/gitlab-org/technical-writing/-/blob/main/.gitlab/issue_templates/tw-monthly-tasks.md)
|
||||||
to regularly update and [clean up the redirects](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/raketasks.md#clean-up-redirects).
|
to regularly update and [clean up the redirects](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/raketasks.md#clean-up-redirects).
|
||||||
If you're a contributor, you may add a new redirect, but you don't need to delete
|
If you're a contributor, you may add a new redirect, but you don't need to delete
|
||||||
the old ones. This process is automatic and handled by the Technical
|
the old ones. This process is automatic and handled by the Technical
|
||||||
Writing team.
|
Writing team.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
If the old page you're renaming doesn't exist in a stable branch, skip the
|
||||||
|
following steps and ask a Technical Writer to add the redirect in
|
||||||
|
[`redirects.yaml`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/content/_data/redirects.yaml).
|
||||||
|
For example, if you add a new page on the 3rd of the month and then rename it before it gets
|
||||||
|
added in the stable branch on the 18th, the old page will never be part of the internal `/help`.
|
||||||
|
In that case, you can jump straight to the
|
||||||
|
[Pages redirect](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/maintenance.md#pages-redirects).
|
||||||
|
|
||||||
To add a redirect:
|
To add a redirect:
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@ group: Editor
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Content Editor **(FREE)**
|
# Content Editor development guidelines **(FREE)**
|
||||||
|
|
||||||
The Content Editor is a UI component that provides a WYSIWYG editing
|
The Content Editor is a UI component that provides a WYSIWYG editing
|
||||||
experience for [GitLab Flavored Markdown](../../user/markdown.md) (GFM) in the GitLab application.
|
experience for [GitLab Flavored Markdown](../../user/markdown.md) in the GitLab application.
|
||||||
It also serves as the foundation for implementing Markdown-focused editors
|
It also serves as the foundation for implementing Markdown-focused editors
|
||||||
that target other engines, like static site generators.
|
that target other engines, like static site generators.
|
||||||
|
|
||||||
|
@ -16,103 +16,339 @@ to build the Content Editor. These frameworks provide a level of abstraction on
|
||||||
the native
|
the native
|
||||||
[`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content) web technology.
|
[`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content) web technology.
|
||||||
|
|
||||||
## Architecture remarks
|
## Usage guide
|
||||||
|
|
||||||
At a high level, the Content Editor:
|
Follow these instructions to include the Content Editor in a feature.
|
||||||
|
|
||||||
- Imports arbitrary Markdown.
|
1. [Include the Content Editor component](#include-the-content-editor-component).
|
||||||
- Renders it in a HTML editing area.
|
1. [Set and get Markdown](#set-and-get-markdown).
|
||||||
- Exports it back to Markdown with changes introduced by the user.
|
1. [Listen for changes](#listen-for-changes).
|
||||||
|
|
||||||
The Content Editor relies on the
|
### Include the Content Editor component
|
||||||
[Markdown API endpoint](../../api/markdown.md) to transform Markdown
|
|
||||||
into HTML. It sends the Markdown input to the REST API and displays the API's
|
|
||||||
HTML output in the editing area. The editor exports the content back to Markdown
|
|
||||||
using a client-side library that serializes editable documents into Markdown.
|
|
||||||
|
|
||||||
![Content Editor high level diagram](img/content_editor_highlevel_diagram.png)
|
Import the `ContentEditor` Vue component. We recommend using asynchronous named imports to
|
||||||
|
take advantage of caching, as the ContentEditor is a big dependency.
|
||||||
Check the [Content Editor technical design document](https://docs.google.com/document/d/1fKOiWpdHned4KOLVOOFYVvX1euEjMP5rTntUhpapdBg)
|
|
||||||
for more information about the design decisions that drive the development of the editor.
|
|
||||||
|
|
||||||
**NOTE**: We also designed the Content Editor to be extensible. We intend to provide
|
|
||||||
more information about extension development for supporting new types of content in upcoming
|
|
||||||
milestones.
|
|
||||||
|
|
||||||
## GitLab Flavored Markdown support
|
|
||||||
|
|
||||||
The [GitLab Flavored Markdown](../../user/markdown.md) extends
|
|
||||||
the [CommonMark specification](https://spec.commonmark.org/0.29/) with support for a
|
|
||||||
variety of content types like diagrams, math expressions, and tables. Supporting
|
|
||||||
all GitLab Flavored Markdown content types in the Content Editor is a work in progress. For
|
|
||||||
the status of the ongoing development for CommonMark and GitLab Flavored Markdown support, read:
|
|
||||||
|
|
||||||
- [Basic Markdown formatting extensions](https://gitlab.com/groups/gitlab-org/-/epics/5404) epic.
|
|
||||||
- [GitLab Flavored Markdown extensions](https://gitlab.com/groups/gitlab-org/-/epics/5438) epic.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
To include the Content Editor in your feature, import the `createContentEditor` factory
|
|
||||||
function and the `ContentEditor` Vue component. `createContentEditor` sets up an instance
|
|
||||||
of [tiptap's Editor class](https://www.tiptap.dev/api/editor/) with all the necessary
|
|
||||||
extensions to support editing GitLab Flavored Markdown content. It also creates
|
|
||||||
a Markdown serializer that allows exporting tiptap's document format to Markdown.
|
|
||||||
|
|
||||||
`createContentEditor` requires a `renderMarkdown` parameter invoked
|
|
||||||
by the editor every time it needs to convert Markdown to HTML. The Content Editor
|
|
||||||
does not provide a default value for this function yet.
|
|
||||||
|
|
||||||
**NOTE**: The Content Editor is in an early development stage. Usage and development
|
|
||||||
guidelines are subject to breaking changes in the upcoming months.
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script>
|
<script>
|
||||||
import { GlButton } from '@gitlab/ui';
|
|
||||||
import { createContentEditor, ContentEditor } from '~/content_editor';
|
|
||||||
import { __ } from '~/locale';
|
|
||||||
import createFlash from '~/flash';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ContentEditor,
|
ContentEditor: () =>
|
||||||
GlButton,
|
import(
|
||||||
|
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data() {
|
// rest of the component definition
|
||||||
return {
|
}
|
||||||
contentEditor: null,
|
</script>
|
||||||
}
|
```
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.contentEditor = createContentEditor({
|
|
||||||
renderMarkdown: (markdown) => Api.markdown({ text: markdown }),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
The Content Editor requires two properties:
|
||||||
await this.contentEditor.setSerializedContent(this.content);
|
|
||||||
} catch (e) {
|
- `renderMarkdown` is an asynchronous function that returns the response (String) of invoking the
|
||||||
createFlash({
|
[Markdown API](../../api/markdown.md).
|
||||||
message: __('There was an error loading content in the editor'), error: e
|
- `uploadsPath` is a URL that points to a [GitLab upload service](../uploads.md#upload-encodings)
|
||||||
});
|
with `multipart/form-data` support.
|
||||||
}
|
|
||||||
},
|
See the [`WikiForm.vue`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue#L207)
|
||||||
|
component for a production example of these two properties.
|
||||||
|
|
||||||
|
### Set and get Markdown
|
||||||
|
|
||||||
|
The `ContentEditor` Vue component doesn't implement Vue data binding flow (`v-model`)
|
||||||
|
because setting and getting Markdown are expensive operations. Data binding would
|
||||||
|
trigger these operations every time the user interacts with the component.
|
||||||
|
|
||||||
|
Instead, you should obtain an instance of the `ContentEditor` class by listening to the
|
||||||
|
`initialized` event:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
|
||||||
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
async save() {
|
async loadInitialContent(contentEditor) {
|
||||||
await Api.updateContent({
|
this.contentEditor = contentEditor;
|
||||||
content: this.contentEditor.getSerializedContent(),
|
|
||||||
});
|
try {
|
||||||
|
await this.contentEditor.setSerializedContent(this.content);
|
||||||
|
} catch (e) {
|
||||||
|
createFlash(__('Could not load initial document'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitChanges() {
|
||||||
|
const markdown = this.contentEditor.getSerializedContent();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<template>
|
||||||
|
<content-editor
|
||||||
|
:render-markdown="renderMarkdown"
|
||||||
|
:uploads-path="pageInfo.uploadsPath"
|
||||||
|
@initialized="loadInitialContent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen for changes
|
||||||
|
|
||||||
|
You can still react to changes in the Content Editor. Reacting to changes helps
|
||||||
|
you know if the document is empty or dirty. Use the `@change` event handler for
|
||||||
|
this purpose.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
empty: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleContentEditorChange({ empty }) {
|
||||||
|
this.empty = empty;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<content-editor :content-editor="contentEditor" />
|
<content-editor
|
||||||
<gl-button @click="save()">Save</gl-button>
|
:render-markdown="renderMarkdown"
|
||||||
|
:uploads-path="pageInfo.uploadsPath"
|
||||||
|
@initialized="loadInitialContent"
|
||||||
|
@change="handleContentEditorChange"
|
||||||
|
/>
|
||||||
|
<gl-button :disabled="empty" @click="submitChanges">
|
||||||
|
{{ __('Submit changes') }}
|
||||||
|
</gl-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
Call `setSerializedContent` to set initial Markdown in the Editor. This method is
|
## Implementation guide
|
||||||
asynchronous because it makes an API request to render the Markdown input.
|
|
||||||
`getSerializedContent` returns a Markdown string that represents the serialized
|
The Content Editor is composed of three main layers:
|
||||||
version of the editable document.
|
|
||||||
|
- **The editing tools UI**, like the toolbar and the table structure editor. They
|
||||||
|
display the editor's state and mutate it by dispatching commands.
|
||||||
|
- **The Tiptap Editor object** manages the editor's state,
|
||||||
|
and exposes business logic as commands executed by the editing tools UI.
|
||||||
|
- **The Markdown serializer** transforms a Markdown source string into a ProseMirror
|
||||||
|
document and vice versa.
|
||||||
|
|
||||||
|
### Editing tools UI
|
||||||
|
|
||||||
|
The editing tools UI are Vue components that display the editor's state and
|
||||||
|
dispatch [commands](https://www.tiptap.dev/api/commands/#commands) to mutate it.
|
||||||
|
They are located in the `~/content_editor/components` directory. For example,
|
||||||
|
the **Bold** toolbar button displays the editor's state by becoming active when
|
||||||
|
the user selects bold text. This button also dispatches the `toggleBold` command
|
||||||
|
to format text as bold:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as Editing tools UI
|
||||||
|
participant B as Tiptap object
|
||||||
|
A->>B: queries state/dispatches commands
|
||||||
|
B--)A: notifies state changes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Node views
|
||||||
|
|
||||||
|
We implement [node views](https://www.tiptap.dev/guide/node-views/vue/#node-views-with-vue)
|
||||||
|
to provide inline editing tools for some content types, like tables and images. Node views
|
||||||
|
allow separating the presentation of a content type from its
|
||||||
|
[model](https://prosemirror.net/docs/guide/#doc.data_structures). Using a Vue component in
|
||||||
|
the presentation layer enables sophisticated editing experiences in the Content Editor.
|
||||||
|
Node views are located in `~/content_editor/components/wrappers`.
|
||||||
|
|
||||||
|
#### Dispatch commands
|
||||||
|
|
||||||
|
You can inject the Tiptap Editor object to Vue components to dispatch
|
||||||
|
commands.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
Do not implement logic that changes the editor's
|
||||||
|
state in Vue components. Encapsulate this logic in commands, and dispatch
|
||||||
|
the command from the component's methods.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inject: ['tiptapEditor'],
|
||||||
|
methods: {
|
||||||
|
execute() {
|
||||||
|
//Incorrect
|
||||||
|
const { state, view } = this.tiptapEditor.state;
|
||||||
|
const { tr, schema } = state;
|
||||||
|
tr.addMark(state.selection.from, state.selection.to, null, null, schema.mark('bold'));
|
||||||
|
|
||||||
|
// Correct
|
||||||
|
this.tiptapEditor.chain().toggleBold().focus().run();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Query editor's state
|
||||||
|
|
||||||
|
Use the `EditorStateObserver` renderless component to react to changes in the
|
||||||
|
editor's state, such as when the document or the selection changes. You can listen to
|
||||||
|
the following events:
|
||||||
|
|
||||||
|
- `docUpdate`
|
||||||
|
- `selectionUpdate`
|
||||||
|
- `transaction`
|
||||||
|
- `focus`
|
||||||
|
- `blur`
|
||||||
|
- `error`.
|
||||||
|
|
||||||
|
Learn more about these events in [Tiptap's event guide](https://www.tiptap.dev/api/events/).
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
// Parts of the code has been hidden for efficiency
|
||||||
|
import EditorStateObserver from './editor_state_observer.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
EditorStateObserver,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
displayError({ message }) {
|
||||||
|
this.error = message;
|
||||||
|
},
|
||||||
|
dismissError() {
|
||||||
|
this.error = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<editor-state-observer @error="displayError">
|
||||||
|
<gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
|
||||||
|
{{ error }}
|
||||||
|
</gl-alert>
|
||||||
|
</editor-state-observer>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Tiptap editor object
|
||||||
|
|
||||||
|
The Tiptap [Editor](https://www.tiptap.dev/api/editor) class manages
|
||||||
|
the editor's state and encapsulates all the business logic that powers
|
||||||
|
the Content Editor. The Content Editor constructs a new instance of this class and
|
||||||
|
provides all the necessary extensions to support
|
||||||
|
[GitLab Flavored Markdown](../../user/markdown.md).
|
||||||
|
|
||||||
|
#### Implement new extensions
|
||||||
|
|
||||||
|
Extensions are the building blocks of the Content Editor. You can learn how to implement
|
||||||
|
new ones by reading [Tiptap's guide](https://www.tiptap.dev/guide/custom-extensions).
|
||||||
|
We recommend checking the list of built-in [nodes](https://www.tiptap.dev/api/nodes) and
|
||||||
|
[marks](https://www.tiptap.dev/api/marks) before implementing a new extension
|
||||||
|
from scratch.
|
||||||
|
|
||||||
|
Store the Content Editor extensions in the `~/content_editor/extensions` directory.
|
||||||
|
When using a Tiptap's built-in extension, wrap it in a ES6 module inside this directory:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export { Bold as default } from '@tiptap/extension-bold';
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the `extend` method to customize the Extension's behavior:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { HardBreak } from '@tiptap/extension-hard-break';
|
||||||
|
|
||||||
|
export default HardBreak.extend({
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
'Shift-Enter': () => this.editor.commands.setHardBreak(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Register extensions
|
||||||
|
|
||||||
|
Register the new extension in `~/content_editor/services/create_content_editor.js`. Import
|
||||||
|
the extension module and add it to the `builtInContentEditorExtensions` array:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import Emoji from '../extensions/emoji';
|
||||||
|
|
||||||
|
const builtInContentEditorExtensions = [
|
||||||
|
Code,
|
||||||
|
CodeBlockHighlight,
|
||||||
|
Document,
|
||||||
|
Dropcursor,
|
||||||
|
Emoji,
|
||||||
|
// Other extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Markdown serializer
|
||||||
|
|
||||||
|
The Markdown Serializer transforms a Markdown String to a
|
||||||
|
[ProseMirror document](https://prosemirror.net/docs/guide/#doc) and vice versa.
|
||||||
|
|
||||||
|
#### Deserialization
|
||||||
|
|
||||||
|
Deserialization is the process of converting Markdown to a ProseMirror document.
|
||||||
|
We take advantage of ProseMirror's
|
||||||
|
[HTML parsing and serialization capabilities](https://prosemirror.net/docs/guide/#schema.serialization_and_parsing)
|
||||||
|
by first rendering the Markdown as HTML using the [Markdown API endpoint](../../api/markdown.md):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as Content Editor
|
||||||
|
participant E as Tiptap Object
|
||||||
|
participant B as Markdown Serializer
|
||||||
|
participant C as Markdown API
|
||||||
|
participant D as ProseMirror Parser
|
||||||
|
A->>B: deserialize(markdown)
|
||||||
|
B->>C: render(markdown)
|
||||||
|
C-->>B: html
|
||||||
|
B->>D: to document(html)
|
||||||
|
D-->>A: document
|
||||||
|
A->>E: setContent(document)
|
||||||
|
```
|
||||||
|
|
||||||
|
Deserializers live in the extension modules. Read Tiptap's
|
||||||
|
[parseHTML](https://www.tiptap.dev/guide/custom-extensions#parse-html) and
|
||||||
|
[addAttributes](https://www.tiptap.dev/guide/custom-extensions#attributes) documentation to
|
||||||
|
learn how to implement them. Titap's API is a wrapper around ProseMirror's
|
||||||
|
[schema spec API](https://prosemirror.net/docs/ref/#model.SchemaSpec).
|
||||||
|
|
||||||
|
#### Serialization
|
||||||
|
|
||||||
|
Serialization is the process of converting a ProseMirror document to Markdown. The Content
|
||||||
|
Editor uses [`prosemirror-markdown`](https://github.com/ProseMirror/prosemirror-markdown)
|
||||||
|
to serialize documents. We recommend reading the
|
||||||
|
[MarkdownSerializer](https://github.com/ProseMirror/prosemirror-markdown#class-markdownserializer)
|
||||||
|
and [MarkdownSerializerState](https://github.com/ProseMirror/prosemirror-markdown#class-markdownserializerstate)
|
||||||
|
classes documentation before implementing a serializer:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as Content Editor
|
||||||
|
participant B as Markdown Serializer
|
||||||
|
participant C as ProseMirror Markdown
|
||||||
|
A->>B: serialize(document)
|
||||||
|
B->>C: serialize(document, serializers)
|
||||||
|
C-->>A: markdown string
|
||||||
|
```
|
||||||
|
|
||||||
|
`prosemirror-markdown` requires implementing a serializer function for each content type supported
|
||||||
|
by the Content Editor. We implement serializers in `~/content_editor/services/markdown_serializer.js`.
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
|
@ -64,7 +64,7 @@ graph LR
|
||||||
click 1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8100542&udv=0"
|
click 1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8100542&udv=0"
|
||||||
1-2["docs-lint markdown (1.5 minutes)"];
|
1-2["docs-lint markdown (1.5 minutes)"];
|
||||||
click 1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=10224335&udv=0"
|
click 1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=10224335&udv=0"
|
||||||
1-3["docs-lint links (6 minutes)"];
|
1-3["docs-lint links (5 minutes)"];
|
||||||
click 1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356757&udv=0"
|
click 1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356757&udv=0"
|
||||||
1-4["ui-docs-links lint (2.5 minutes)"];
|
1-4["ui-docs-links lint (2.5 minutes)"];
|
||||||
click 1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=10823717&udv=1020379"
|
click 1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=10823717&udv=1020379"
|
||||||
|
@ -104,7 +104,7 @@ graph RL;
|
||||||
1-18["kubesec-sast"];
|
1-18["kubesec-sast"];
|
||||||
1-19["nodejs-scan-sast"];
|
1-19["nodejs-scan-sast"];
|
||||||
1-20["secrets-sast"];
|
1-20["secrets-sast"];
|
||||||
1-21["static-analysis (30 minutes)"];
|
1-21["static-analysis (14 minutes)"];
|
||||||
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
||||||
|
|
||||||
class 1-3 criticalPath;
|
class 1-3 criticalPath;
|
||||||
|
@ -123,7 +123,7 @@ graph RL;
|
||||||
2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
|
2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
|
||||||
end
|
end
|
||||||
|
|
||||||
2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (11 minutes)"];
|
2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"];
|
||||||
class 2_2-2 criticalPath;
|
class 2_2-2 criticalPath;
|
||||||
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
|
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
|
||||||
2_2-4["memory-on-boot (3.5 minutes)"];
|
2_2-4["memory-on-boot (3.5 minutes)"];
|
||||||
|
@ -152,14 +152,14 @@ graph RL;
|
||||||
click 2_5-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations"
|
click 2_5-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations"
|
||||||
end
|
end
|
||||||
|
|
||||||
3_1-1["jest (16 minutes)"];
|
3_1-1["jest (14.5 minutes)"];
|
||||||
class 3_1-1 criticalPath;
|
class 3_1-1 criticalPath;
|
||||||
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
|
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
|
||||||
subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
|
subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
|
||||||
3_1-1 --> 2_2-2;
|
3_1-1 --> 2_2-2;
|
||||||
end
|
end
|
||||||
|
|
||||||
3_2-1["rspec:coverage (5.3 minutes)"];
|
3_2-1["rspec:coverage (4 minutes)"];
|
||||||
subgraph "Depends on `rspec` jobs";
|
subgraph "Depends on `rspec` jobs";
|
||||||
3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1;
|
3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1;
|
||||||
click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0"
|
click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0"
|
||||||
|
@ -206,7 +206,7 @@ graph RL;
|
||||||
1-18["kubesec-sast"];
|
1-18["kubesec-sast"];
|
||||||
1-19["nodejs-scan-sast"];
|
1-19["nodejs-scan-sast"];
|
||||||
1-20["secrets-sast"];
|
1-20["secrets-sast"];
|
||||||
1-21["static-analysis (30 minutes)"];
|
1-21["static-analysis (14 minutes)"];
|
||||||
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
||||||
|
|
||||||
class 1-3 criticalPath;
|
class 1-3 criticalPath;
|
||||||
|
@ -226,7 +226,7 @@ graph RL;
|
||||||
2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
|
2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
|
||||||
end
|
end
|
||||||
|
|
||||||
2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (11 minutes)"];
|
2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"];
|
||||||
class 2_2-2 criticalPath;
|
class 2_2-2 criticalPath;
|
||||||
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
|
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
|
||||||
2_2-4["memory-on-boot (3.5 minutes)"];
|
2_2-4["memory-on-boot (3.5 minutes)"];
|
||||||
|
@ -263,14 +263,14 @@ graph RL;
|
||||||
click 2_6-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914314&udv=0"
|
click 2_6-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914314&udv=0"
|
||||||
end
|
end
|
||||||
|
|
||||||
3_1-1["jest (16 minutes)"];
|
3_1-1["jest (14.5 minutes)"];
|
||||||
class 3_1-1 criticalPath;
|
class 3_1-1 criticalPath;
|
||||||
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
|
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
|
||||||
subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
|
subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
|
||||||
3_1-1 --> 2_2-2;
|
3_1-1 --> 2_2-2;
|
||||||
end
|
end
|
||||||
|
|
||||||
3_2-1["rspec:coverage (5.3 minutes)"];
|
3_2-1["rspec:coverage (4 minutes)"];
|
||||||
subgraph "Depends on `rspec` jobs";
|
subgraph "Depends on `rspec` jobs";
|
||||||
3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1;
|
3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1;
|
||||||
click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0"
|
click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0"
|
||||||
|
@ -283,7 +283,7 @@ graph RL;
|
||||||
click 4_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910777&udv=0"
|
click 4_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910777&udv=0"
|
||||||
end
|
end
|
||||||
|
|
||||||
3_3-1["review-deploy (10.5 minutes)"];
|
3_3-1["review-deploy (9 minutes)"];
|
||||||
subgraph "Played by `review-build-cng`";
|
subgraph "Played by `review-build-cng`";
|
||||||
3_3-1 --> 2_6-1;
|
3_3-1 --> 2_6-1;
|
||||||
class 3_3-1 criticalPath;
|
class 3_3-1 criticalPath;
|
||||||
|
@ -332,7 +332,7 @@ graph RL;
|
||||||
1-18["kubesec-sast"];
|
1-18["kubesec-sast"];
|
||||||
1-19["nodejs-scan-sast"];
|
1-19["nodejs-scan-sast"];
|
||||||
1-20["secrets-sast"];
|
1-20["secrets-sast"];
|
||||||
1-21["static-analysis (30 minutes)"];
|
1-21["static-analysis (14 minutes)"];
|
||||||
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
||||||
|
|
||||||
class 1-5 criticalPath;
|
class 1-5 criticalPath;
|
||||||
|
@ -350,7 +350,7 @@ graph RL;
|
||||||
class 2_3-1 criticalPath;
|
class 2_3-1 criticalPath;
|
||||||
end
|
end
|
||||||
|
|
||||||
2_4-1["package-and-qa (140 minutes)"];
|
2_4-1["package-and-qa (113 minutes)"];
|
||||||
subgraph "Needs `build-qa-image` & `build-assets-image`";
|
subgraph "Needs `build-qa-image` & `build-assets-image`";
|
||||||
2_4-1 --> 1-2 & 2_3-1;
|
2_4-1 --> 1-2 & 2_3-1;
|
||||||
class 2_4-1 criticalPath;
|
class 2_4-1 criticalPath;
|
||||||
|
|
126
spec/helpers/routing/pseudonymization_helper_spec.rb
Normal file
126
spec/helpers/routing/pseudonymization_helper_spec.rb
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ::Routing::PseudonymizationHelper do
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||||
|
let_it_be(:project) { create(:project, group: group) }
|
||||||
|
let_it_be(:issue) { create(:issue, project: project) }
|
||||||
|
|
||||||
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(mask_page_urls: true)
|
||||||
|
allow(helper).to receive(:group).and_return(group)
|
||||||
|
allow(helper).to receive(:project).and_return(project)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'masked url' do
|
||||||
|
it 'generates masked page url' do
|
||||||
|
expect(helper.masked_page_url).to eq(masked_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when url has params to mask' do
|
||||||
|
context 'with controller for MR' do
|
||||||
|
let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/merge_requests/#{merge_request.id}" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Rails.application.routes).to receive(:recognize_path).and_return({
|
||||||
|
controller: "projects/merge_requests",
|
||||||
|
action: "show",
|
||||||
|
namespace_id: group.name,
|
||||||
|
project_id: project.name,
|
||||||
|
id: merge_request.id.to_s
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'masked url'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with controller for issue' do
|
||||||
|
let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/issues/#{issue.id}" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Rails.application.routes).to receive(:recognize_path).and_return({
|
||||||
|
controller: "projects/issues",
|
||||||
|
action: "show",
|
||||||
|
namespace_id: group.name,
|
||||||
|
project_id: project.name,
|
||||||
|
id: issue.id.to_s
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'masked url'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with controller for groups with subgroups and project' do
|
||||||
|
let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/project:#{project.id}/"}
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:group).and_return(subgroup)
|
||||||
|
allow(helper.project).to receive(:namespace).and_return(subgroup)
|
||||||
|
allow(Rails.application.routes).to receive(:recognize_path).and_return({
|
||||||
|
controller: 'projects',
|
||||||
|
action: 'show',
|
||||||
|
namespace_id: subgroup.name,
|
||||||
|
id: project.name
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'masked url'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with controller for groups and subgroups' do
|
||||||
|
let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/"}
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:group).and_return(subgroup)
|
||||||
|
allow(Rails.application.routes).to receive(:recognize_path).and_return({
|
||||||
|
controller: 'groups',
|
||||||
|
action: 'show',
|
||||||
|
id: subgroup.name
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'masked url'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with controller for blob with file path' do
|
||||||
|
let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/blob/:repository_path" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Rails.application.routes).to receive(:recognize_path).and_return({
|
||||||
|
controller: 'projects/blob',
|
||||||
|
action: 'show',
|
||||||
|
namespace_id: group.name,
|
||||||
|
project_id: project.name,
|
||||||
|
id: 'master/README.md'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'masked url'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when url has no params to mask' do
|
||||||
|
let(:root_url) { 'http://test.host/' }
|
||||||
|
|
||||||
|
context 'returns root url' do
|
||||||
|
it 'masked_page_url' do
|
||||||
|
expect(helper.masked_page_url).to eq(root_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when feature flag is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(mask_page_urls: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(helper.masked_page_url).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue