Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
21db5294d4
commit
acb2f0ab94
|
@ -38,7 +38,8 @@ AllCops:
|
||||||
- 'workhorse/**/*'
|
- 'workhorse/**/*'
|
||||||
- 'spec/support/*.git/**/*' # e.g. spec/support/gitlab-git-test.git
|
- 'spec/support/*.git/**/*' # e.g. spec/support/gitlab-git-test.git
|
||||||
- 'db/ci_migrate/*.rb' # since the `db/ci_migrate` is a symlinked to `db/migrate`
|
- 'db/ci_migrate/*.rb' # since the `db/ci_migrate` is a symlinked to `db/migrate`
|
||||||
CacheRootDirectory: tmp
|
# Use absolute path to avoid orphan directories with changed workspace root.
|
||||||
|
CacheRootDirectory: <%= Dir.getwd %>/tmp
|
||||||
MaxFilesInCache: 25000
|
MaxFilesInCache: 25000
|
||||||
|
|
||||||
Cop/AvoidKeywordArgumentsInSidekiqWorkers:
|
Cop/AvoidKeywordArgumentsInSidekiqWorkers:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TableCell } from '@tiptap/extension-table-cell';
|
import { TableCell } from '@tiptap/extension-table-cell';
|
||||||
|
import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
|
||||||
|
|
||||||
export default TableCell.extend({
|
export default TableCell.extend({
|
||||||
content: 'inline*',
|
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TableHeader } from '@tiptap/extension-table-header';
|
import { TableHeader } from '@tiptap/extension-table-header';
|
||||||
|
import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
|
||||||
|
|
||||||
export default TableHeader.extend({
|
export default TableHeader.extend({
|
||||||
content: 'inline*',
|
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function isBlockTablesFeatureEnabled() {
|
||||||
|
return gon.features?.contentEditorBlockTables;
|
||||||
|
}
|
|
@ -30,6 +30,12 @@ import TableRow from '../extensions/table_row';
|
||||||
import TaskItem from '../extensions/task_item';
|
import TaskItem from '../extensions/task_item';
|
||||||
import TaskList from '../extensions/task_list';
|
import TaskList from '../extensions/task_list';
|
||||||
import Text from '../extensions/text';
|
import Text from '../extensions/text';
|
||||||
|
import {
|
||||||
|
renderHardBreak,
|
||||||
|
renderTable,
|
||||||
|
renderTableCell,
|
||||||
|
renderTableRow,
|
||||||
|
} from './serialization_helpers';
|
||||||
|
|
||||||
const defaultSerializerConfig = {
|
const defaultSerializerConfig = {
|
||||||
marks: {
|
marks: {
|
||||||
|
@ -65,6 +71,7 @@ const defaultSerializerConfig = {
|
||||||
expelEnclosingWhitespace: true,
|
expelEnclosingWhitespace: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
nodes: {
|
nodes: {
|
||||||
[Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
|
[Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
|
||||||
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
|
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
|
||||||
|
@ -80,7 +87,7 @@ const defaultSerializerConfig = {
|
||||||
|
|
||||||
state.write(`:${name}:`);
|
state.write(`:${name}:`);
|
||||||
},
|
},
|
||||||
[HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break,
|
[HardBreak.name]: renderHardBreak,
|
||||||
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
|
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
|
||||||
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
|
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
|
||||||
[Image.name]: (state, node) => {
|
[Image.name]: (state, node) => {
|
||||||
|
@ -95,60 +102,10 @@ const defaultSerializerConfig = {
|
||||||
[Reference.name]: (state, node) => {
|
[Reference.name]: (state, node) => {
|
||||||
state.write(node.attrs.originalText || node.attrs.text);
|
state.write(node.attrs.originalText || node.attrs.text);
|
||||||
},
|
},
|
||||||
[Table.name]: (state, node) => {
|
[Table.name]: renderTable,
|
||||||
state.renderContent(node);
|
[TableCell.name]: renderTableCell,
|
||||||
},
|
[TableHeader.name]: renderTableCell,
|
||||||
[TableCell.name]: (state, node) => {
|
[TableRow.name]: renderTableRow,
|
||||||
state.renderInline(node);
|
|
||||||
},
|
|
||||||
[TableHeader.name]: (state, node) => {
|
|
||||||
state.renderInline(node);
|
|
||||||
},
|
|
||||||
[TableRow.name]: (state, node) => {
|
|
||||||
const isHeaderRow = node.child(0).type.name === 'tableHeader';
|
|
||||||
|
|
||||||
const renderRow = () => {
|
|
||||||
const cellWidths = [];
|
|
||||||
|
|
||||||
state.flushClose(1);
|
|
||||||
|
|
||||||
state.write('| ');
|
|
||||||
node.forEach((cell, _, i) => {
|
|
||||||
if (i) state.write(' | ');
|
|
||||||
|
|
||||||
const { length } = state.out;
|
|
||||||
state.render(cell, node, i);
|
|
||||||
cellWidths.push(state.out.length - length);
|
|
||||||
});
|
|
||||||
state.write(' |');
|
|
||||||
|
|
||||||
state.closeBlock(node);
|
|
||||||
|
|
||||||
return cellWidths;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeaderRow = (cellWidths) => {
|
|
||||||
state.flushClose(1);
|
|
||||||
|
|
||||||
state.write('|');
|
|
||||||
node.forEach((cell, _, i) => {
|
|
||||||
if (i) state.write('|');
|
|
||||||
|
|
||||||
state.write(cell.attrs.align === 'center' ? ':' : '-');
|
|
||||||
state.write(state.repeat('-', cellWidths[i]));
|
|
||||||
state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
|
|
||||||
});
|
|
||||||
state.write('|');
|
|
||||||
|
|
||||||
state.closeBlock(node);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isHeaderRow) {
|
|
||||||
renderHeaderRow(renderRow());
|
|
||||||
} else {
|
|
||||||
renderRow();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[TaskItem.name]: (state, node) => {
|
[TaskItem.name]: (state, node) => {
|
||||||
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
|
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
|
||||||
state.renderContent(node);
|
state.renderContent(node);
|
||||||
|
@ -175,7 +132,7 @@ const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
|
||||||
* that parses the Markdown and converts it into HTML.
|
* that parses the Markdown and converts it into HTML.
|
||||||
* @returns a markdown serializer
|
* @returns a markdown serializer
|
||||||
*/
|
*/
|
||||||
export default ({ render = () => null, serializerConfig }) => ({
|
export default ({ render = () => null, serializerConfig = {} } = {}) => ({
|
||||||
/**
|
/**
|
||||||
* Converts a Markdown string into a ProseMirror JSONDocument based
|
* Converts a Markdown string into a ProseMirror JSONDocument based
|
||||||
* on a ProseMirror schema.
|
* on a ProseMirror schema.
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
import { uniq } from 'lodash';
|
||||||
|
import { isBlockTablesFeatureEnabled } from './feature_flags';
|
||||||
|
|
||||||
|
const defaultAttrs = {
|
||||||
|
td: { colspan: 1, rowspan: 1, colwidth: null },
|
||||||
|
th: { colspan: 1, rowspan: 1, colwidth: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableMap = new WeakMap();
|
||||||
|
|
||||||
|
function shouldRenderCellInline(cell) {
|
||||||
|
if (cell.childCount === 1) {
|
||||||
|
const parent = cell.child(0);
|
||||||
|
if (parent.type.name === 'paragraph' && parent.childCount === 1) {
|
||||||
|
const child = parent.child(0);
|
||||||
|
return child.isText && child.marks.length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowsAndCells(table) {
|
||||||
|
const cells = [];
|
||||||
|
const rows = [];
|
||||||
|
table.descendants((n) => {
|
||||||
|
if (n.type.name === 'tableCell' || n.type.name === 'tableHeader') {
|
||||||
|
cells.push(n);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.type.name === 'tableRow') {
|
||||||
|
rows.push(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return { rows, cells };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChildren(node) {
|
||||||
|
const children = [];
|
||||||
|
for (let i = 0; i < node.childCount; i += 1) {
|
||||||
|
children.push(node.child(i));
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRenderHTMLTable(table) {
|
||||||
|
const { rows, cells } = getRowsAndCells(table);
|
||||||
|
|
||||||
|
const cellChildCount = Math.max(...cells.map((cell) => cell.childCount));
|
||||||
|
const maxColspan = Math.max(...cells.map((cell) => cell.attrs.colspan));
|
||||||
|
const maxRowspan = Math.max(...cells.map((cell) => cell.attrs.rowspan));
|
||||||
|
|
||||||
|
const rowChildren = rows.map((row) => uniq(getChildren(row).map((cell) => cell.type.name)));
|
||||||
|
const cellTypeInFirstRow = rowChildren[0];
|
||||||
|
const cellTypesInOtherRows = uniq(rowChildren.slice(1).map(([type]) => type));
|
||||||
|
|
||||||
|
// if the first row has headers, and there are no headers anywhere else, render markdown table
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
cellTypeInFirstRow.length === 1 &&
|
||||||
|
cellTypeInFirstRow[0] === 'tableHeader' &&
|
||||||
|
cellTypesInOtherRows.length === 1 &&
|
||||||
|
cellTypesInOtherRows[0] === 'tableCell'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellChildCount === 1 && maxColspan === 1 && maxRowspan === 1) {
|
||||||
|
// if all rows contain only one paragraph each and no rowspan/colspan, render markdown table
|
||||||
|
const children = uniq(cells.map((cell) => cell.child(0).type.name));
|
||||||
|
if (children.length === 1 && children[0] === 'paragraph') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTag(state, tagName, attrs) {
|
||||||
|
let str = `<${tagName}`;
|
||||||
|
|
||||||
|
str += Object.entries(attrs || {})
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (defaultAttrs[tagName]?.[key] === value) return '';
|
||||||
|
|
||||||
|
return ` ${key}=${state.quote(value?.toString() || '')}`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `${str}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTag(state, tagName) {
|
||||||
|
return `</${tagName}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInBlockTable(node) {
|
||||||
|
return tableMap.get(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInTable(node) {
|
||||||
|
return tableMap.has(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIsInBlockTable(table, value) {
|
||||||
|
tableMap.set(table, value);
|
||||||
|
|
||||||
|
const { rows, cells } = getRowsAndCells(table);
|
||||||
|
rows.forEach((row) => tableMap.set(row, value));
|
||||||
|
cells.forEach((cell) => {
|
||||||
|
tableMap.set(cell, value);
|
||||||
|
if (cell.childCount && cell.child(0).type.name === 'paragraph')
|
||||||
|
tableMap.set(cell.child(0), value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetIsInBlockTable(table) {
|
||||||
|
tableMap.delete(table);
|
||||||
|
|
||||||
|
const { rows, cells } = getRowsAndCells(table);
|
||||||
|
rows.forEach((row) => tableMap.delete(row));
|
||||||
|
cells.forEach((cell) => {
|
||||||
|
tableMap.delete(cell);
|
||||||
|
if (cell.childCount) tableMap.delete(cell.child(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagOpen(state, tagName, attrs) {
|
||||||
|
state.ensureNewLine();
|
||||||
|
state.write(openTag(state, tagName, attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagClose(state, tagName, insertNewline = true) {
|
||||||
|
state.write(closeTag(state, tagName));
|
||||||
|
if (insertNewline) state.ensureNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTableHeaderRowAsMarkdown(state, node, cellWidths) {
|
||||||
|
state.flushClose(1);
|
||||||
|
|
||||||
|
state.write('|');
|
||||||
|
node.forEach((cell, _, i) => {
|
||||||
|
if (i) state.write('|');
|
||||||
|
|
||||||
|
state.write(cell.attrs.align === 'center' ? ':' : '-');
|
||||||
|
state.write(state.repeat('-', cellWidths[i]));
|
||||||
|
state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
|
||||||
|
});
|
||||||
|
state.write('|');
|
||||||
|
|
||||||
|
state.closeBlock(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTableRowAsMarkdown(state, node, isHeaderRow = false) {
|
||||||
|
const cellWidths = [];
|
||||||
|
|
||||||
|
state.flushClose(1);
|
||||||
|
|
||||||
|
state.write('| ');
|
||||||
|
node.forEach((cell, _, i) => {
|
||||||
|
if (i) state.write(' | ');
|
||||||
|
|
||||||
|
const { length } = state.out;
|
||||||
|
state.render(cell, node, i);
|
||||||
|
cellWidths.push(state.out.length - length);
|
||||||
|
});
|
||||||
|
state.write(' |');
|
||||||
|
|
||||||
|
state.closeBlock(node);
|
||||||
|
|
||||||
|
if (isHeaderRow) renderTableHeaderRowAsMarkdown(state, node, cellWidths);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTableRowAsHTML(state, node) {
|
||||||
|
renderTagOpen(state, 'tr');
|
||||||
|
|
||||||
|
node.forEach((cell, _, i) => {
|
||||||
|
const tag = cell.type.name === 'tableHeader' ? 'th' : 'td';
|
||||||
|
|
||||||
|
renderTagOpen(state, tag, cell.attrs);
|
||||||
|
|
||||||
|
if (!shouldRenderCellInline(cell)) {
|
||||||
|
state.closeBlock(node);
|
||||||
|
state.flushClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.render(cell, node, i);
|
||||||
|
state.flushClose(1);
|
||||||
|
|
||||||
|
renderTagClose(state, tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderTagClose(state, 'tr');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderTableCell(state, node) {
|
||||||
|
if (!isBlockTablesFeatureEnabled()) {
|
||||||
|
state.renderInline(node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInBlockTable(node) || shouldRenderCellInline(node)) {
|
||||||
|
state.renderInline(node.child(0));
|
||||||
|
} else {
|
||||||
|
state.renderContent(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderTableRow(state, node) {
|
||||||
|
if (isInBlockTable(node)) {
|
||||||
|
renderTableRowAsHTML(state, node);
|
||||||
|
} else {
|
||||||
|
renderTableRowAsMarkdown(state, node, node.child(0).type.name === 'tableHeader');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderTable(state, node) {
|
||||||
|
if (isBlockTablesFeatureEnabled()) {
|
||||||
|
setIsInBlockTable(node, shouldRenderHTMLTable(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInBlockTable(node)) renderTagOpen(state, 'table');
|
||||||
|
|
||||||
|
state.renderContent(node);
|
||||||
|
|
||||||
|
if (isInBlockTable(node)) renderTagClose(state, 'table');
|
||||||
|
|
||||||
|
// ensure at least one blank line after any table
|
||||||
|
state.closeBlock(node);
|
||||||
|
state.flushClose();
|
||||||
|
|
||||||
|
if (isBlockTablesFeatureEnabled()) {
|
||||||
|
unsetIsInBlockTable(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderHardBreak(state, node, parent, index) {
|
||||||
|
const br = isInTable(parent) ? '<br>' : '\\\n';
|
||||||
|
|
||||||
|
for (let i = index + 1; i < parent.childCount; i += 1) {
|
||||||
|
if (parent.child(i).type !== node.type) {
|
||||||
|
state.write(br);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -133,7 +133,10 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="[$options.userColorScheme, { inline, 'with-codequality': hasCodequalityChanges }]"
|
:class="[
|
||||||
|
$options.userColorScheme,
|
||||||
|
{ 'inline-diff-view': inline, 'with-codequality': hasCodequalityChanges },
|
||||||
|
]"
|
||||||
:data-commit-id="commitId"
|
:data-commit-id="commitId"
|
||||||
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
|
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
|
||||||
@mousedown="handleParallelLineMouseDown"
|
@mousedown="handleParallelLineMouseDown"
|
||||||
|
|
|
@ -613,7 +613,7 @@ table.code {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.inline {
|
&.inline-diff-view {
|
||||||
.diff-grid-comments {
|
.diff-grid-comments {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
@ -5,5 +5,9 @@ class Projects::WikisController < Projects::ApplicationController
|
||||||
|
|
||||||
alias_method :container, :project
|
alias_method :container, :project
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
push_frontend_feature_flag(:content_editor_block_tables, @project, default_enabled: :yaml)
|
||||||
|
end
|
||||||
|
|
||||||
feature_category :wiki
|
feature_category :wiki
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: content_editor_block_tables
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66187
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338937
|
||||||
|
milestone: '14.3'
|
||||||
|
type: development
|
||||||
|
group: group::editor
|
||||||
|
default_enabled: false
|
|
@ -188,7 +188,7 @@ module Gitlab
|
||||||
merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
|
merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
|
||||||
|
|
||||||
unless default_project_filter
|
unless default_project_filter
|
||||||
merge_requests = merge_requests.in_projects(project_ids_relation)
|
merge_requests = merge_requests.of_projects(project_ids_relation)
|
||||||
end
|
end
|
||||||
|
|
||||||
apply_sort(merge_requests, scope: 'merge_requests')
|
apply_sort(merge_requests, scope: 'merge_requests')
|
||||||
|
|
|
@ -0,0 +1,480 @@
|
||||||
|
import Blockquote from '~/content_editor/extensions/blockquote';
|
||||||
|
import Bold from '~/content_editor/extensions/bold';
|
||||||
|
import BulletList from '~/content_editor/extensions/bullet_list';
|
||||||
|
import Code from '~/content_editor/extensions/code';
|
||||||
|
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
|
||||||
|
import Emoji from '~/content_editor/extensions/emoji';
|
||||||
|
import HardBreak from '~/content_editor/extensions/hard_break';
|
||||||
|
import Heading from '~/content_editor/extensions/heading';
|
||||||
|
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
|
||||||
|
import Image from '~/content_editor/extensions/image';
|
||||||
|
import Italic from '~/content_editor/extensions/italic';
|
||||||
|
import Link from '~/content_editor/extensions/link';
|
||||||
|
import ListItem from '~/content_editor/extensions/list_item';
|
||||||
|
import OrderedList from '~/content_editor/extensions/ordered_list';
|
||||||
|
import Paragraph from '~/content_editor/extensions/paragraph';
|
||||||
|
import Strike from '~/content_editor/extensions/strike';
|
||||||
|
import Table from '~/content_editor/extensions/table';
|
||||||
|
import TableCell from '~/content_editor/extensions/table_cell';
|
||||||
|
import TableHeader from '~/content_editor/extensions/table_header';
|
||||||
|
import TableRow from '~/content_editor/extensions/table_row';
|
||||||
|
import Text from '~/content_editor/extensions/text';
|
||||||
|
import markdownSerializer from '~/content_editor/services/markdown_serializer';
|
||||||
|
import { createTestEditor, createDocBuilder } from '../test_utils';
|
||||||
|
|
||||||
|
jest.mock('~/emoji');
|
||||||
|
|
||||||
|
jest.mock('~/content_editor/services/feature_flags', () => ({
|
||||||
|
isBlockTablesFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const tiptapEditor = createTestEditor({
|
||||||
|
extensions: [
|
||||||
|
Blockquote,
|
||||||
|
Bold,
|
||||||
|
BulletList,
|
||||||
|
Code,
|
||||||
|
CodeBlockHighlight,
|
||||||
|
Emoji,
|
||||||
|
HardBreak,
|
||||||
|
Heading,
|
||||||
|
HorizontalRule,
|
||||||
|
Image,
|
||||||
|
Italic,
|
||||||
|
Link,
|
||||||
|
ListItem,
|
||||||
|
OrderedList,
|
||||||
|
Paragraph,
|
||||||
|
Strike,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Text,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
builders: {
|
||||||
|
doc,
|
||||||
|
blockquote,
|
||||||
|
bold,
|
||||||
|
bulletList,
|
||||||
|
code,
|
||||||
|
codeBlock,
|
||||||
|
emoji,
|
||||||
|
heading,
|
||||||
|
hardBreak,
|
||||||
|
horizontalRule,
|
||||||
|
image,
|
||||||
|
italic,
|
||||||
|
link,
|
||||||
|
listItem,
|
||||||
|
orderedList,
|
||||||
|
paragraph,
|
||||||
|
strike,
|
||||||
|
table,
|
||||||
|
tableCell,
|
||||||
|
tableHeader,
|
||||||
|
tableRow,
|
||||||
|
},
|
||||||
|
} = createDocBuilder({
|
||||||
|
tiptapEditor,
|
||||||
|
names: {
|
||||||
|
blockquote: { nodeType: Blockquote.name },
|
||||||
|
bold: { markType: Bold.name },
|
||||||
|
bulletList: { nodeType: BulletList.name },
|
||||||
|
code: { markType: Code.name },
|
||||||
|
codeBlock: { nodeType: CodeBlockHighlight.name },
|
||||||
|
emoji: { markType: Emoji.name },
|
||||||
|
hardBreak: { nodeType: HardBreak.name },
|
||||||
|
heading: { nodeType: Heading.name },
|
||||||
|
horizontalRule: { nodeType: HorizontalRule.name },
|
||||||
|
image: { nodeType: Image.name },
|
||||||
|
italic: { nodeType: Italic.name },
|
||||||
|
link: { markType: Link.name },
|
||||||
|
listItem: { nodeType: ListItem.name },
|
||||||
|
orderedList: { nodeType: OrderedList.name },
|
||||||
|
paragraph: { nodeType: Paragraph.name },
|
||||||
|
strike: { markType: Strike.name },
|
||||||
|
table: { nodeType: Table.name },
|
||||||
|
tableCell: { nodeType: TableCell.name },
|
||||||
|
tableHeader: { nodeType: TableHeader.name },
|
||||||
|
tableRow: { nodeType: TableRow.name },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialize = (...content) =>
|
||||||
|
markdownSerializer({}).serialize({
|
||||||
|
schema: tiptapEditor.schema,
|
||||||
|
content: doc(...content).toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('markdownSerializer', () => {
|
||||||
|
it('correctly serializes a line break', () => {
|
||||||
|
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly serializes a table with inline content', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
// each table cell must contain at least one paragraph
|
||||||
|
tableRow(
|
||||||
|
tableHeader(paragraph('header')),
|
||||||
|
tableHeader(paragraph('header')),
|
||||||
|
tableHeader(paragraph('header')),
|
||||||
|
),
|
||||||
|
tableRow(
|
||||||
|
tableCell(paragraph('cell')),
|
||||||
|
tableCell(paragraph('cell')),
|
||||||
|
tableCell(paragraph('cell')),
|
||||||
|
),
|
||||||
|
tableRow(
|
||||||
|
tableCell(paragraph('cell')),
|
||||||
|
tableCell(paragraph('cell')),
|
||||||
|
tableCell(paragraph('cell')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
| header | header | header |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| cell | cell | cell |
|
||||||
|
| cell | cell | cell |
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly serializes a table with line breaks', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
|
||||||
|
tableRow(
|
||||||
|
tableCell(paragraph('cell with', hardBreak(), 'line', hardBreak(), 'breaks')),
|
||||||
|
tableCell(paragraph('cell')),
|
||||||
|
),
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
| header | header |
|
||||||
|
|--------|--------|
|
||||||
|
| cell with<br>line<br>breaks | cell |
|
||||||
|
| cell | cell |
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly serializes two consecutive tables', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
),
|
||||||
|
table(
|
||||||
|
tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
| header | header |
|
||||||
|
|--------|--------|
|
||||||
|
| cell | cell |
|
||||||
|
| cell | cell |
|
||||||
|
|
||||||
|
| header | header |
|
||||||
|
|--------|--------|
|
||||||
|
| cell | cell |
|
||||||
|
| cell | cell |
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly serializes a table with block content', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
tableRow(
|
||||||
|
tableHeader(paragraph('examples of')),
|
||||||
|
tableHeader(paragraph('block content')),
|
||||||
|
tableHeader(paragraph('in tables')),
|
||||||
|
tableHeader(paragraph('in content editor')),
|
||||||
|
),
|
||||||
|
tableRow(
|
||||||
|
tableCell(heading({ level: 1 }, 'heading 1')),
|
||||||
|
tableCell(heading({ level: 2 }, 'heading 2')),
|
||||||
|
tableCell(paragraph(bold('just bold'))),
|
||||||
|
tableCell(paragraph(bold('bold'), ' ', italic('italic'), ' ', code('code'))),
|
||||||
|
),
|
||||||
|
tableRow(
|
||||||
|
tableCell(
|
||||||
|
paragraph('all marks in three paragraphs:'),
|
||||||
|
paragraph('the ', bold('quick'), ' ', italic('brown'), ' ', code('fox')),
|
||||||
|
paragraph(
|
||||||
|
link({ href: '/home' }, 'jumps'),
|
||||||
|
' over the ',
|
||||||
|
strike('lazy'),
|
||||||
|
' ',
|
||||||
|
emoji({ name: 'dog' }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tableCell(
|
||||||
|
paragraph(image({ src: 'img.jpg', alt: 'some image' }), hardBreak(), 'image content'),
|
||||||
|
),
|
||||||
|
tableCell(
|
||||||
|
blockquote('some text', hardBreak(), hardBreak(), 'in a multiline blockquote'),
|
||||||
|
),
|
||||||
|
tableCell(
|
||||||
|
codeBlock(
|
||||||
|
{ language: 'javascript' },
|
||||||
|
'var a = 2;\nvar b = 3;\nvar c = a + d;\n\nconsole.log(c);',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tableRow(
|
||||||
|
tableCell(bulletList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
|
||||||
|
tableCell(orderedList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
|
||||||
|
tableCell(
|
||||||
|
paragraph('paragraphs separated by'),
|
||||||
|
horizontalRule(),
|
||||||
|
paragraph('a horizontal rule'),
|
||||||
|
),
|
||||||
|
tableCell(
|
||||||
|
table(
|
||||||
|
tableRow(tableHeader(paragraph('table')), tableHeader(paragraph('inside'))),
|
||||||
|
tableRow(tableCell(paragraph('another')), tableCell(paragraph('table'))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>examples of</th>
|
||||||
|
<th>block content</th>
|
||||||
|
<th>in tables</th>
|
||||||
|
<th>in content editor</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
# heading 1
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
## heading 2
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
**just bold**
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
**bold** _italic_ \`code\`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
all marks in three paragraphs:
|
||||||
|
|
||||||
|
the **quick** _brown_ \`fox\`
|
||||||
|
|
||||||
|
[jumps](/home) over the ~~lazy~~ :dog:
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
![some image](img.jpg)<br>image content
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
> some text\\
|
||||||
|
> \\
|
||||||
|
> in a multiline blockquote
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
var a = 2;
|
||||||
|
var b = 3;
|
||||||
|
var c = a + d;
|
||||||
|
|
||||||
|
console.log(c);
|
||||||
|
\`\`\`
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
* item 1
|
||||||
|
* item 2
|
||||||
|
* item 2
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
1. item 1
|
||||||
|
2. item 2
|
||||||
|
3. item 2
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
paragraphs separated by
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
a horizontal rule
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
| table | inside |
|
||||||
|
|-------|--------|
|
||||||
|
| another | table |
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly renders content after a markdown table', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(tableRow(tableHeader(paragraph('header'))), tableRow(tableCell(paragraph('cell')))),
|
||||||
|
heading({ level: 1 }, 'this is a heading'),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
| header |
|
||||||
|
|--------|
|
||||||
|
| cell |
|
||||||
|
|
||||||
|
# this is a heading
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly renders content after an html table', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
tableRow(tableHeader(paragraph('header'))),
|
||||||
|
tableRow(tableCell(blockquote('hi'), paragraph('there'))),
|
||||||
|
),
|
||||||
|
heading({ level: 1 }, 'this is a heading'),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>header</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
> hi
|
||||||
|
|
||||||
|
there
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
# this is a heading
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly serializes tables with misplaced header cells', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
tableRow(tableHeader(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableHeader(paragraph('cell'))),
|
||||||
|
),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>cell</th>
|
||||||
|
<td>cell</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>cell</td>
|
||||||
|
<th>cell</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly serializes table without any headers', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
|
||||||
|
),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>cell</td>
|
||||||
|
<td>cell</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>cell</td>
|
||||||
|
<td>cell</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly serializes table with rowspan and colspan', () => {
|
||||||
|
expect(
|
||||||
|
serialize(
|
||||||
|
table(
|
||||||
|
tableRow(
|
||||||
|
tableHeader(paragraph('header')),
|
||||||
|
tableHeader(paragraph('header')),
|
||||||
|
tableHeader(paragraph('header')),
|
||||||
|
),
|
||||||
|
tableRow(
|
||||||
|
tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2')),
|
||||||
|
tableCell({ rowspan: 2 }, paragraph('cell')),
|
||||||
|
),
|
||||||
|
tableRow(tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2'))),
|
||||||
|
),
|
||||||
|
).trim(),
|
||||||
|
).toBe(
|
||||||
|
`
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>header</th>
|
||||||
|
<th>header</th>
|
||||||
|
<th>header</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">cell with rowspan: 2</td>
|
||||||
|
<td rowspan="2">cell</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">cell with rowspan: 2</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -102,14 +102,10 @@
|
||||||
markdown: |-
|
markdown: |-
|
||||||
| header | header |
|
| header | header |
|
||||||
|--------|--------|
|
|--------|--------|
|
||||||
| cell | cell |
|
| `code` | cell with **bold** |
|
||||||
| cell | cell |
|
| ~~strike~~ | cell with _italic_ |
|
||||||
- name: table_with_alignment
|
|
||||||
markdown: |-
|
# content after table
|
||||||
| header | : header : | header : |
|
|
||||||
|--------|------------|----------|
|
|
||||||
| cell | cell | cell |
|
|
||||||
| cell | cell | cell |
|
|
||||||
- name: emoji
|
- name: emoji
|
||||||
markdown: ':sparkles: :heart: :100:'
|
markdown: ':sparkles: :heart: :100:'
|
||||||
- name: reference
|
- name: reference
|
||||||
|
|
|
@ -148,13 +148,13 @@ RSpec.describe Gitlab::SearchResults do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'includes merge requests from source and target projects' do
|
it 'does not include merge requests from source projects' do
|
||||||
forked_project = fork_project(project, user)
|
forked_project = fork_project(project, user)
|
||||||
merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
|
merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
|
||||||
|
|
||||||
results = described_class.new(user, 'foo', Project.where(id: forked_project.id))
|
results = described_class.new(user, 'foo', Project.where(id: forked_project.id))
|
||||||
|
|
||||||
expect(results.objects('merge_requests')).to include merge_request_2
|
expect(results.objects('merge_requests')).not_to include merge_request_2
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#merge_requests' do
|
describe '#merge_requests' do
|
||||||
|
|
Loading…
Reference in New Issue