Commit 62ddd3a0 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 1e6a9268
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'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import Dropzone from 'dropzone'; import Dropzone from 'dropzone';
import _ from 'underscore'; import _ from 'underscore';
import './behaviors/preview_markdown'; import './behaviors/preview_markdown';
import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import csrf from './lib/utils/csrf'; import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { n__, __ } from '~/locale'; import { n__, __ } from '~/locale';
...@@ -173,14 +174,25 @@ export default function dropzoneInput(form) { ...@@ -173,14 +174,25 @@ export default function dropzoneInput(form) {
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
handlePaste = event => { handlePaste = event => {
const pasteEvent = event.originalEvent; const pasteEvent = event.originalEvent;
if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { const { clipboardData } = pasteEvent;
const image = isImage(pasteEvent); if (clipboardData && clipboardData.items) {
if (image) { // 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(); event.preventDefault();
const filename = getFilename(pasteEvent) || 'image.png'; const converter = new PasteMarkdownTable(clipboardData);
const text = `{{${filename}}}`; const text = converter.convertToTableMarkdown();
pasteText(text); 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);
}
} }
} }
}; };
......
---
title: Cut and paste Markdown table from a spreadsheet
merge_request: 22290
author:
type: added
...@@ -395,10 +395,26 @@ Omnibus GitLab 11.1. ...@@ -395,10 +395,26 @@ Omnibus GitLab 11.1.
## Set maximum pages size ## Set maximum pages size
The maximum size of the unpacked archive per project can be configured in the You can configure the maximum size of the unpacked archive per project in the
Admin area under the Application settings in the **Maximum size of pages (MB)**. Admin area. Under Application settings, edit the **Maximum size of pages (MB)**.
The default is 100MB. 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 ## 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. You can run the GitLab Pages daemon on a separate server in order to decrease the load on your main application server.
......
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 ...@@ -23,6 +23,15 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
remove_repository(project) remove_repository(project)
end 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 it 'issues/open-issue.html' do
render_issue(create(:issue, project: project)) render_issue(create(:issue, project: project))
end end
......
import $ from 'jquery'; import $ from 'jquery';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import dropzoneInput from '~/dropzone_input'; import dropzoneInput from '~/dropzone_input';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
const TEST_FILE = new File([], 'somefile.jpg'); const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {}; TEST_FILE.upload = {};
...@@ -25,6 +26,34 @@ describe('dropzone_input', () => { ...@@ -25,6 +26,34 @@ describe('dropzone_input', () => {
expect(dropzone.version).toBeTruthy(); 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', () => { describe('shows error message', () => {
let form; let form;
let dropzone; let dropzone;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment