Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1e6a926864
commit
62ddd3a005
|
@ -0,0 +1,88 @@
|
|||
export default class PasteMarkdownTable {
|
||||
constructor(clipboardData) {
|
||||
this.data = clipboardData;
|
||||
}
|
||||
|
||||
static maxColumnWidth(rows, columnIndex) {
|
||||
return Math.max.apply(null, rows.map(row => row[columnIndex].length));
|
||||
}
|
||||
|
||||
// To determine whether the cut data is a table, the following criteria
|
||||
// must be satisfied with the clipboard data:
|
||||
//
|
||||
// 1. MIME types "text/plain" and "text/html" exist
|
||||
// 2. The "text/html" data must have a single <table> element
|
||||
static isTable(data) {
|
||||
const types = new Set(data.types);
|
||||
|
||||
if (!types.has('text/html') || !types.has('text/plain')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const htmlData = data.getData('text/html');
|
||||
const doc = new DOMParser().parseFromString(htmlData, 'text/html');
|
||||
|
||||
// We're only looking for exactly one table. If there happens to be
|
||||
// multiple tables, it's possible an application copied data into
|
||||
// the clipboard that is not related to a simple table. It may also be
|
||||
// complicated converting multiple tables into Markdown.
|
||||
if (doc.querySelectorAll('table').length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
convertToTableMarkdown() {
|
||||
const text = this.data.getData('text/plain').trim();
|
||||
this.rows = text.split(/[\n\u0085\u2028\u2029]|\r\n?/g).map(row => row.split('\t'));
|
||||
this.normalizeRows();
|
||||
this.calculateColumnWidths();
|
||||
|
||||
const markdownRows = this.rows.map(
|
||||
row =>
|
||||
// | Name | Title | Email Address |
|
||||
// |--------------|-------|----------------|
|
||||
// | Jane Atler | CEO | jane@acme.com |
|
||||
// | John Doherty | CTO | john@acme.com |
|
||||
// | Sally Smith | CFO | sally@acme.com |
|
||||
`| ${row.map((column, index) => this.formatColumn(column, index)).join(' | ')} |`,
|
||||
);
|
||||
|
||||
// Insert a header break (e.g. -----) to the second row
|
||||
markdownRows.splice(1, 0, this.generateHeaderBreak());
|
||||
|
||||
return markdownRows.join('\n');
|
||||
}
|
||||
|
||||
// Ensure each row has the same number of columns
|
||||
normalizeRows() {
|
||||
const rowLengths = this.rows.map(row => row.length);
|
||||
const maxLength = Math.max(...rowLengths);
|
||||
|
||||
this.rows.forEach(row => {
|
||||
while (row.length < maxLength) {
|
||||
row.push('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
calculateColumnWidths() {
|
||||
this.columnWidths = this.rows[0].map((_column, columnIndex) =>
|
||||
PasteMarkdownTable.maxColumnWidth(this.rows, columnIndex),
|
||||
);
|
||||
}
|
||||
|
||||
formatColumn(column, index) {
|
||||
const spaces = Array(this.columnWidths[index] - column.length + 1).join(' ');
|
||||
return column + spaces;
|
||||
}
|
||||
|
||||
generateHeaderBreak() {
|
||||
// Add 3 dashes to line things up: there is additional spacing for the pipe characters
|
||||
const dashes = this.columnWidths.map((width, index) =>
|
||||
Array(this.columnWidths[index] + 3).join('-'),
|
||||
);
|
||||
return `|${dashes.join('|')}|`;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import $ from 'jquery';
|
|||
import Dropzone from 'dropzone';
|
||||
import _ from 'underscore';
|
||||
import './behaviors/preview_markdown';
|
||||
import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
|
||||
import csrf from './lib/utils/csrf';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import { n__, __ } from '~/locale';
|
||||
|
@ -173,14 +174,25 @@ export default function dropzoneInput(form) {
|
|||
// eslint-disable-next-line consistent-return
|
||||
handlePaste = event => {
|
||||
const pasteEvent = event.originalEvent;
|
||||
if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
|
||||
const image = isImage(pasteEvent);
|
||||
if (image) {
|
||||
const { clipboardData } = pasteEvent;
|
||||
if (clipboardData && clipboardData.items) {
|
||||
// Apple Numbers copies a table as an image, HTML, and text, so
|
||||
// we need to check for the presence of a table first.
|
||||
if (PasteMarkdownTable.isTable(clipboardData)) {
|
||||
event.preventDefault();
|
||||
const filename = getFilename(pasteEvent) || 'image.png';
|
||||
const text = `{{${filename}}}`;
|
||||
const converter = new PasteMarkdownTable(clipboardData);
|
||||
const text = converter.convertToTableMarkdown();
|
||||
pasteText(text);
|
||||
return uploadFile(image.getAsFile(), filename);
|
||||
} else {
|
||||
const image = isImage(pasteEvent);
|
||||
|
||||
if (image) {
|
||||
event.preventDefault();
|
||||
const filename = getFilename(pasteEvent) || 'image.png';
|
||||
const text = `{{${filename}}}`;
|
||||
pasteText(text);
|
||||
return uploadFile(image.getAsFile(), filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Cut and paste Markdown table from a spreadsheet
|
||||
merge_request: 22290
|
||||
author:
|
||||
type: added
|
|
@ -395,10 +395,26 @@ Omnibus GitLab 11.1.
|
|||
|
||||
## Set maximum pages size
|
||||
|
||||
The maximum size of the unpacked archive per project can be configured in the
|
||||
Admin area under the Application settings in the **Maximum size of pages (MB)**.
|
||||
You can configure the maximum size of the unpacked archive per project in the
|
||||
Admin area. Under Application settings, edit the **Maximum size of pages (MB)**.
|
||||
The default is 100MB.
|
||||
|
||||
### Override maximum pages size per project or group **(PREMIUM ONLY)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/16610) in GitLab 12.7.
|
||||
|
||||
To override the global maximum pages size for a specific project:
|
||||
|
||||
1. Navigate to your project's **Settings > Pages** page.
|
||||
1. Edit the **Maximum size of pages**.
|
||||
1. Click **Save changes**.
|
||||
|
||||
To override the global maximum pages size for a specific group:
|
||||
|
||||
1. Navigate to your group's **Settings > General** page and expand **Pages**.
|
||||
1. Edit the **Maximum size of pages**.
|
||||
1. Click **Save changes**.
|
||||
|
||||
## Running GitLab Pages on a separate server
|
||||
|
||||
You can run the GitLab Pages daemon on a separate server in order to decrease the load on your main application server.
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
|
||||
|
||||
describe('PasteMarkdownTable', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
const event = new window.Event('paste');
|
||||
|
||||
Object.defineProperty(event, 'dataTransfer', {
|
||||
value: {
|
||||
getData: jest.fn().mockImplementation(type => {
|
||||
if (type === 'text/html') {
|
||||
return '<table><tr><td></td></tr></table>';
|
||||
}
|
||||
return 'hello world';
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
data = event.dataTransfer;
|
||||
});
|
||||
|
||||
describe('isTable', () => {
|
||||
it('return false when no HTML data is provided', () => {
|
||||
data.types = ['text/plain'];
|
||||
|
||||
expect(PasteMarkdownTable.isTable(data)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when no text data is provided', () => {
|
||||
data.types = ['text/html'];
|
||||
|
||||
expect(PasteMarkdownTable.isTable(data)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when a table is provided in both text and HTML', () => {
|
||||
data.types = ['text/html', 'text/plain'];
|
||||
|
||||
expect(PasteMarkdownTable.isTable(data)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when no HTML table is included', () => {
|
||||
data.types = ['text/html', 'text/plain'];
|
||||
data.getData = jest.fn().mockImplementation(() => 'nothing');
|
||||
|
||||
expect(PasteMarkdownTable.isTable(data)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToTableMarkdown', () => {
|
||||
let converter;
|
||||
|
||||
beforeEach(() => {
|
||||
converter = new PasteMarkdownTable(data);
|
||||
});
|
||||
|
||||
it('returns a Markdown table', () => {
|
||||
data.getData = jest.fn().mockImplementation(type => {
|
||||
if (type === 'text/plain') {
|
||||
return 'First\tLast\nJohn\tDoe\nJane\tDoe';
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const expected = [
|
||||
'| First | Last |',
|
||||
'|-------|------|',
|
||||
'| John | Doe |',
|
||||
'| Jane | Doe |',
|
||||
].join('\n');
|
||||
|
||||
expect(converter.convertToTableMarkdown()).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns a Markdown table with rows normalized', () => {
|
||||
data.getData = jest.fn().mockImplementation(type => {
|
||||
if (type === 'text/plain') {
|
||||
return 'First\tLast\nJohn\tDoe\nJane';
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const expected = [
|
||||
'| First | Last |',
|
||||
'|-------|------|',
|
||||
'| John | Doe |',
|
||||
'| Jane | |',
|
||||
].join('\n');
|
||||
|
||||
expect(converter.convertToTableMarkdown()).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -23,6 +23,15 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
|
|||
remove_repository(project)
|
||||
end
|
||||
|
||||
it 'issues/new-issue.html' do
|
||||
get :new, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it 'issues/open-issue.html' do
|
||||
render_issue(create(:issue, project: project))
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import dropzoneInput from '~/dropzone_input';
|
||||
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
|
||||
|
||||
const TEST_FILE = new File([], 'somefile.jpg');
|
||||
TEST_FILE.upload = {};
|
||||
|
@ -25,6 +26,34 @@ describe('dropzone_input', () => {
|
|||
expect(dropzone.version).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('handlePaste', () => {
|
||||
beforeEach(() => {
|
||||
loadFixtures('issues/new-issue.html');
|
||||
|
||||
const form = $('#new_issue');
|
||||
form.data('uploads-path', TEST_UPLOAD_PATH);
|
||||
dropzoneInput(form);
|
||||
});
|
||||
|
||||
it('pastes Markdown tables', () => {
|
||||
const event = $.Event('paste');
|
||||
const origEvent = new Event('paste');
|
||||
const pasteData = new DataTransfer();
|
||||
pasteData.setData('text/plain', 'hello world');
|
||||
pasteData.setData('text/html', '<table></table>');
|
||||
origEvent.clipboardData = pasteData;
|
||||
event.originalEvent = origEvent;
|
||||
|
||||
spyOn(PasteMarkdownTable, 'isTable').and.callThrough();
|
||||
spyOn(PasteMarkdownTable.prototype, 'convertToTableMarkdown').and.callThrough();
|
||||
|
||||
$('.js-gfm-input').trigger(event);
|
||||
|
||||
expect(PasteMarkdownTable.isTable).toHaveBeenCalled();
|
||||
expect(PasteMarkdownTable.prototype.convertToTableMarkdown).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shows error message', () => {
|
||||
let form;
|
||||
let dropzone;
|
||||
|
|
Loading…
Reference in New Issue