12.7.6-ee 12.7.0-pre
<script>
import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui';
import _ from 'underscore';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import axios from '~/lib/utils/axios_utils';
import { s__, __, sprintf } from '~/locale';
import createFlash from '~/flash';
export default {
COPY_TO_CLIPBOARD: __('Copy'),
RESET_KEY: __('Reset key'),
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlModal,
ClipboardButton,
ToggleButton,
},
directives: {
'gl-modal': GlModalDirective,
},
props: {
initialAuthorizationKey: {
type: String,
required: false,
default: '',
},
formPath: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
learnMoreUrl: {
type: String,
required: false,
default: '',
},
initialActivated: {
type: Boolean,
required: true,
},
},
data() {
return {
activated: this.initialActivated,
loadingActivated: false,
authorizationKey: this.initialAuthorizationKey,
};
},
computed: {
learnMoreDescription() {
return sprintf(
s__(
'AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts.',
),
{
linkStart: `<a href="${_.escape(
this.learnMoreUrl,
)}" target="_blank" rel="noopener noreferrer">`,
linkEnd: '</a>',
},
false,
);
},
sectionDescription() {
const desc = s__(
'AlertService|Each alert source must be authorized using the following URL and authorization key.',
);
const learnMoreDesc = this.learnMoreDescription ? ` ${this.learnMoreDescription}` : '';
return `${desc}${learnMoreDesc}`;
},
},
watch: {
activated() {
this.updateIcon();
},
},
methods: {
updateIcon() {
return document.querySelectorAll('.js-service-active-status').forEach(icon => {
if (icon.dataset.value === this.activated.toString()) {
icon.classList.remove('d-none');
} else {
icon.classList.add('d-none');
}
});
},
resetKey() {
return axios
.put(this.formPath, { service: { token: '' } })
.then(res => {
this.authorizationKey = res.data.token;
})
.catch(() => {
createFlash(__('Failed to reset key. Please try again.'));
});
},
toggleActivated(value) {
this.loadingActivated = true;
return axios
.put(this.formPath, { service: { active: value } })
.then(() => {
this.activated = value;
this.loadingActivated = false;
})
.catch(() => {
createFlash(__('Update failed. Please try again.'));
this.loadingActivated = false;
});
},
},
};
</script>
<template>
<div>
<p v-html="sectionDescription"></p>
<gl-form-group :label="__('Active')" label-for="activated" label-class="label-bold">
<toggle-button
id="activated"
:disabled-input="loadingActivated"
:is-loading="loadingActivated"
:value="activated"
@change="toggleActivated"
/>
</gl-form-group>
<gl-form-group :label="__('URL')" label-for="url" label-class="label-bold">
<div class="input-group">
<gl-form-input id="url" :readonly="true" :value="url" />
<span class="input-group-append">
<clipboard-button :text="url" :title="$options.COPY_TO_CLIPBOARD" />
</span>
</div>
</gl-form-group>
<gl-form-group
:label="__('Authorization key')"
label-for="authorization-key"
label-class="label-bold"
>
<div class="input-group">
<gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" />
<span class="input-group-append">
<clipboard-button :text="authorizationKey" :title="$options.COPY_TO_CLIPBOARD" />
</span>
</div>
<gl-button v-gl-modal.authKeyModal class="mt-2">{{ $options.RESET_KEY }}</gl-button>
<gl-modal
modal-id="authKeyModal"
:title="$options.RESET_KEY"
:ok-title="$options.RESET_KEY"
ok-variant="danger"
@ok="resetKey"
>
{{
__(
'Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
)
}}
</gl-modal>
</gl-form-group>
</div>
</template>
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import AlertsServiceForm from './components/alerts_service_form.vue';
export default el => {
if (!el) {
return null;
}
const { activated: activatedStr, formPath, authorizationKey, url, learnMoreUrl } = el.dataset;
const activated = parseBoolean(activatedStr);
return new Vue({
el,
render(createElement) {
return createElement(AlertsServiceForm, {
props: {
initialActivated: activated,
formPath,
learnMoreUrl,
initialAuthorizationKey: authorizationKey,
url,
},
});
},
});
};
...@@ -24,6 +24,7 @@ const Api = { ...@@ -24,6 +24,7 @@ const Api = {
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners', projectRunnersPath: '/api/:version/projects/:id/runners',
projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches',
mergeRequestsPath: '/api/:version/merge_requests', mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels', groupLabelsPath: '/groups/:namespace_path/-/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
...@@ -44,6 +45,8 @@ const Api = { ...@@ -44,6 +45,8 @@ const Api = {
releasePath: '/api/:version/projects/:id/releases/:tag_name', releasePath: '/api/:version/projects/:id/releases/:tag_name',
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics', adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
...@@ -218,6 +221,22 @@ const Api = { ...@@ -218,6 +221,22 @@ const Api = {
return axios.get(url, config); return axios.get(url, config);
}, },
projectProtectedBranches(id, query = '') {
const url = Api.buildUrl(Api.projectProtectedBranchesPath).replace(
':id',
encodeURIComponent(id),
);
return axios
.get(url, {
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
},
})
.then(({ data }) => data);
},
mergeRequests(params = {}) { mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath); const url = Api.buildUrl(Api.mergeRequestsPath);
...@@ -448,6 +467,22 @@ const Api = { ...@@ -448,6 +467,22 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
pipelineSingle(id, pipelineId) {
const url = Api.buildUrl(this.pipelineSinglePath)
.replace(':id', encodeURIComponent(id))
.replace(':pipeline_id', encodeURIComponent(pipelineId));
return axios.get(url);
},
lsifData(projectPath, commitId, path) {
const url = Api.buildUrl(this.lsifPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':commit_id', commitId);
return axios.get(url, { params: { path } });
},
buildUrl(url) { buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version)); return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
}, },
... ...
......
// https://prosemirror.net/docs/ref/#model.ParseRule.priority
export const DEFAULT_PARSE_RULE_PRIORITY = 50;
export const HIGHER_PARSE_RULE_PRIORITY = 1 + DEFAULT_PARSE_RULE_PRIORITY;
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import { Mark } from 'tiptap'; import { Mark } from 'tiptap';
import _ from 'underscore'; import { escape as esc } from 'lodash';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter // Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class InlineHTML extends Mark { export default class InlineHTML extends Mark {
...@@ -35,7 +35,7 @@ export default class InlineHTML extends Mark { ...@@ -35,7 +35,7 @@ export default class InlineHTML extends Mark {
mixable: true, mixable: true,
open(state, mark) { open(state, mark) {
return `<${mark.attrs.tag}${ return `<${mark.attrs.tag}${
mark.attrs.title ? ` title="${state.esc(_.escape(mark.attrs.title))}"` : '' mark.attrs.title ? ` title="${state.esc(esc(mark.attrs.title))}"` : ''
}>`; }>`;
}, },
close(state, mark) { close(state, mark) {
... ...
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { Mark } from 'tiptap'; import { Mark } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown'; import { defaultMarkdownSerializer } from 'prosemirror-markdown';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter // Transforms generated HTML back to GFM for Banzai::Filter::MathFilter
export default class MathMark extends Mark { export default class MathMark extends Mark {
...@@ -15,7 +16,7 @@ export default class MathMark extends Mark { ...@@ -15,7 +16,7 @@ export default class MathMark extends Mark {
// Matches HTML generated by Banzai::Filter::MathFilter // Matches HTML generated by Banzai::Filter::MathFilter
{ {
tag: 'code.code.math[data-math-style=inline]', tag: 'code.code.math[data-math-style=inline]',
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
}, },
// Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js // Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
{ {
... ...
......
/* eslint-disable class-methods-use-this */ import Playable from './playable';
import { Node } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
// Transforms generated HTML back to GFM for Banzai::Filter::AudioLinkFilter // Transforms generated HTML back to GFM for Banzai::Filter::AudioLinkFilter
export default class Audio extends Node { export default class Audio extends Playable {
get name() { constructor() {
return 'audio'; super();
} this.mediaType = 'audio';
get schema() {
return {
attrs: {
src: {},
alt: {
default: null,
},
},
group: 'block',
draggable: true,
parseDOM: [
{
tag: '.audio-container',
skip: true,
},
{
tag: '.audio-container p',
priority: 51,
ignore: true,
},
{
tag: 'audio[src]',
getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }),
},
],
toDOM: node => [
'audio',
{
src: node.attrs.src,
controls: true,
'data-setup': '{}',
'data-title': node.attrs.alt,
},
],
};
}
toMarkdown(state, node) {
defaultMarkdownSerializer.nodes.image(state, node);
state.closeBlock(node);
} }
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import { Image as BaseImage } from 'tiptap-extensions'; import { Image as BaseImage } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown'; import { defaultMarkdownSerializer } from 'prosemirror-markdown';
import { placeholderImage } from '~/lazy_loader'; import { placeholderImage } from '~/lazy_loader';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
export default class Image extends BaseImage { export default class Image extends BaseImage {
get schema() { get schema() {
...@@ -23,7 +24,7 @@ export default class Image extends BaseImage { ...@@ -23,7 +24,7 @@ export default class Image extends BaseImage {
// Matches HTML generated by Banzai::Filter::ImageLinkFilter // Matches HTML generated by Banzai::Filter::ImageLinkFilter
{ {
tag: 'a.no-attachment-icon', tag: 'a.no-attachment-icon',
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
skip: true, skip: true,
}, },
// Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter // Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter
... ...
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import { Node } from 'tiptap'; import { Node } from 'tiptap';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter // Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
export default class OrderedTaskList extends Node { export default class OrderedTaskList extends Node {
...@@ -14,7 +15,7 @@ export default class OrderedTaskList extends Node { ...@@ -14,7 +15,7 @@ export default class OrderedTaskList extends Node {
content: '(task_list_item|list_item)+', content: '(task_list_item|list_item)+',
parseDOM: [ parseDOM: [
{ {
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
tag: 'ol.task-list', tag: 'ol.task-list',
}, },
], ],
... ...
......
/* eslint-disable class-methods-use-this */
/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
import { Node } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
/**
* Abstract base class for playable media, like video and audio.
* Must not be instantiated directly. Subclasses must set
* the `mediaType` property in their constructors.
* @abstract
*/
export default class Playable extends Node {
constructor() {
super();
this.mediaType = '';
this.extraElementAttrs = {};
}
get name() {
return this.mediaType;
}
get schema() {
const attrs = {
src: {},
alt: {
default: null,
},
};
const parseDOM = [
{
tag: `.${this.mediaType}-container`,
skip: true,
},
{
tag: `.${this.mediaType}-container p`,
priority: HIGHER_PARSE_RULE_PRIORITY,
ignore: true,
},
{
tag: `${this.mediaType}[src]`,
getAttrs: el => ({ src: el.src, alt: el.dataset.title }),
},
];
const toDOM = node => [
this.mediaType,
{
src: node.attrs.src,
controls: true,
'data-setup': '{}',
'data-title': node.attrs.alt,
...this.extraElementAttrs,
},
];
return {
attrs,
group: 'block',
draggable: true,
parseDOM,
toDOM,
};
}
toMarkdown(state, node) {
defaultMarkdownSerializer.nodes.image(state, node);
state.closeBlock(node);
}
}
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import { Node } from 'tiptap'; import { Node } from 'tiptap';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses // Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses
export default class Reference extends Node { export default class Reference extends Node {
...@@ -23,7 +24,7 @@ export default class Reference extends Node { ...@@ -23,7 +24,7 @@ export default class Reference extends Node {
parseDOM: [ parseDOM: [
{ {
tag: 'a.gfm:not([data-link=true])', tag: 'a.gfm:not([data-link=true])',
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
getAttrs: el => ({ getAttrs: el => ({
className: el.className, className: el.className,
referenceType: el.dataset.referenceType, referenceType: el.dataset.referenceType,
... ...
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import TableRow from './table_row'; import TableRow from './table_row';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
const CENTER_ALIGN = 'center'; const CENTER_ALIGN = 'center';
...@@ -16,7 +17,7 @@ export default class TableHeaderRow extends TableRow { ...@@ -16,7 +17,7 @@ export default class TableHeaderRow extends TableRow {
parseDOM: [ parseDOM: [
{ {
tag: 'thead tr', tag: 'thead tr',
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
}, },
], ],
toDOM: () => ['tr', 0], toDOM: () => ['tr', 0],
... ...
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { Node } from 'tiptap'; import { Node } from 'tiptap';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter // Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
export default class TableOfContents extends Node { export default class TableOfContents extends Node {
...@@ -16,11 +17,11 @@ export default class TableOfContents extends Node { ...@@ -16,11 +17,11 @@ export default class TableOfContents extends Node {
parseDOM: [ parseDOM: [
{ {
tag: 'ul.section-nav', tag: 'ul.section-nav',
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
}, },
{ {
tag: 'p.table-of-contents', tag: 'p.table-of-contents',
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
}, },
], ],
toDOM: () => ['p', { class: 'table-of-contents' }, __('Table of Contents')], toDOM: () => ['p', { class: 'table-of-contents' }, __('Table of Contents')],
... ...
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import { Node } from 'tiptap'; import { Node } from 'tiptap';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter // Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
export default class TaskList extends Node { export default class TaskList extends Node {
...@@ -14,7 +15,7 @@ export default class TaskList extends Node { ...@@ -14,7 +15,7 @@ export default class TaskList extends Node {
content: '(task_list_item|list_item)+', content: '(task_list_item|list_item)+',
parseDOM: [ parseDOM: [
{ {
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
tag: 'ul.task-list', tag: 'ul.task-list',
}, },
], ],
... ...
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import { Node } from 'tiptap'; import { Node } from 'tiptap';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter // Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
export default class TaskListItem extends Node { export default class TaskListItem extends Node {
...@@ -20,7 +21,7 @@ export default class TaskListItem extends Node { ...@@ -20,7 +21,7 @@ export default class TaskListItem extends Node {
content: 'paragraph block*', content: 'paragraph block*',
parseDOM: [ parseDOM: [
{ {
priority: 51, priority: HIGHER_PARSE_RULE_PRIORITY,
tag: 'li.task-list-item', tag: 'li.task-list-item',
getAttrs: el => { getAttrs: el => {
const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox'); const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
... ...
......
This diff is collapsed.
import flash from '~/flash'; import flash from '~/flash';
import $ from 'jquery';
import { sprintf, __ } from '../../locale'; import { sprintf, __ } from '../../locale';
// Renders diagrams and flowcharts from text using Mermaid in any element with the // Renders diagrams and flowcharts from text using Mermaid in any element with the
...@@ -18,9 +19,12 @@ import { sprintf, __ } from '../../locale'; ...@@ -18,9 +19,12 @@ import { sprintf, __ } from '../../locale';
// This is an arbitrary number; Can be iterated upon when suitable. // This is an arbitrary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000; const MAX_CHAR_LIMIT = 5000;
export default function renderMermaid($els) { function renderMermaids($els) {
if (!$els.length) return; if (!$els.length) return;
// A diagram may have been truncated in search results which will cause errors, so abort the render.
if (document.querySelector('body').dataset.page === 'search:show') return;
import(/* webpackChunkName: 'mermaid' */ 'mermaid') import(/* webpackChunkName: 'mermaid' */ 'mermaid')
.then(mermaid => { .then(mermaid => {
mermaid.initialize({ mermaid.initialize({
...@@ -92,3 +96,19 @@ export default function renderMermaid($els) { ...@@ -92,3 +96,19 @@ export default function renderMermaid($els) {
flash(`Can't load mermaid module: ${err}`); flash(`Can't load mermaid module: ${err}`);
}); });
} }
export default function renderMermaid($els) {
if (!$els.length) return;
const visibleMermaids = $els.filter(function filter() {
return $(this).closest('details').length === 0;
});
renderMermaids(visibleMermaids);
$els.closest('details').one('toggle', function toggle() {
if (this.open) {
renderMermaids($(this).find('.js-render-mermaid'));
}
});
}
This diff is collapsed.
This diff is collapsed.