Prettify blob behaviors and cycle_analytics modules
This commit is contained in:
parent
aeaf6686df
commit
7fd4e21c77
36 changed files with 509 additions and 490 deletions
|
@ -51,7 +51,7 @@ export default function initCopyToClipboard() {
|
|||
* the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
|
||||
* data types to the intended values.
|
||||
*/
|
||||
$(document).on('copy', 'body > textarea[readonly]', (e) => {
|
||||
$(document).on('copy', 'body > textarea[readonly]', e => {
|
||||
const { clipboardData } = e.originalEvent;
|
||||
if (!clipboardData) return;
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@ import $ from 'jquery';
|
|||
|
||||
$(() => {
|
||||
$('body').on('click', '.js-details-target', function target() {
|
||||
$(this).closest('.js-details-container').toggleClass('open');
|
||||
$(this)
|
||||
.closest('.js-details-container')
|
||||
.toggleClass('open');
|
||||
});
|
||||
|
||||
// Show details content. Hides link after click.
|
||||
|
@ -13,7 +15,9 @@ $(() => {
|
|||
//
|
||||
$('body').on('click', '.js-details-expand', function expand(e) {
|
||||
e.preventDefault();
|
||||
$(this).next('.js-details-content').removeClass('hide');
|
||||
$(this)
|
||||
.next('.js-details-content')
|
||||
.removeClass('hide');
|
||||
$(this).hide();
|
||||
|
||||
const truncatedItem = $(this).siblings('.js-details-short');
|
||||
|
|
|
@ -34,7 +34,7 @@ const gfmRules = {
|
|||
},
|
||||
},
|
||||
AutolinkFilter: {
|
||||
'a'(el, text) {
|
||||
a(el, text) {
|
||||
// Fallback on the regular MarkdownFilter's `a` handler.
|
||||
if (text !== el.getAttribute('href')) return false;
|
||||
|
||||
|
@ -60,7 +60,7 @@ const gfmRules = {
|
|||
},
|
||||
},
|
||||
ImageLazyLoadFilter: {
|
||||
'img'(el, text) {
|
||||
img(el, text) {
|
||||
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
|
@ -71,7 +71,7 @@ const gfmRules = {
|
|||
|
||||
return CopyAsGFM.nodeToGFM(videoEl);
|
||||
},
|
||||
'video'(el) {
|
||||
video(el) {
|
||||
return `![${el.dataset.title}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
|
@ -118,11 +118,14 @@ const gfmRules = {
|
|||
'a[name]:not([href]):empty'(el) {
|
||||
return el.outerHTML;
|
||||
},
|
||||
'dl'(el, text) {
|
||||
let lines = text.replace(/\n\n/g, '\n').trim().split('\n');
|
||||
dl(el, text) {
|
||||
let lines = text
|
||||
.replace(/\n\n/g, '\n')
|
||||
.trim()
|
||||
.split('\n');
|
||||
// Add two spaces to the front of subsequent list items lines,
|
||||
// or leave the line entirely blank.
|
||||
lines = lines.map((l) => {
|
||||
lines = lines.map(l => {
|
||||
const line = l.trim();
|
||||
if (line.length === 0) return '';
|
||||
|
||||
|
@ -151,27 +154,30 @@ const gfmRules = {
|
|||
|
||||
// Prefixes lines with 4 spaces if the code contains triple backticks
|
||||
if (lang === '' && text.match(/^```/gm)) {
|
||||
return text.split('\n').map((l) => {
|
||||
const line = l.trim();
|
||||
if (line.length === 0) return '';
|
||||
return text
|
||||
.split('\n')
|
||||
.map(l => {
|
||||
const line = l.trim();
|
||||
if (line.length === 0) return '';
|
||||
|
||||
return ` ${line}`;
|
||||
}).join('\n');
|
||||
return ` ${line}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return `\`\`\`${lang}\n${text}\n\`\`\``;
|
||||
},
|
||||
'pre > code'(el, text) {
|
||||
// Don't wrap code blocks in ``
|
||||
// Don't wrap code blocks in ``
|
||||
return text;
|
||||
},
|
||||
},
|
||||
MarkdownFilter: {
|
||||
'br'(el) {
|
||||
br(el) {
|
||||
// Two spaces at the end of a line are turned into a BR
|
||||
return ' ';
|
||||
},
|
||||
'code'(el, text) {
|
||||
code(el, text) {
|
||||
let backtickCount = 1;
|
||||
const backtickMatch = text.match(/`+/);
|
||||
if (backtickMatch) {
|
||||
|
@ -183,27 +189,31 @@ const gfmRules = {
|
|||
|
||||
return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
|
||||
},
|
||||
'blockquote'(el, text) {
|
||||
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
|
||||
blockquote(el, text) {
|
||||
return text
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(s => `> ${s}`.trim())
|
||||
.join('\n');
|
||||
},
|
||||
'img'(el) {
|
||||
img(el) {
|
||||
const imageSrc = el.src;
|
||||
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
|
||||
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
|
||||
return `![${el.getAttribute('alt')}](${imageUrl})`;
|
||||
},
|
||||
'a.anchor'(el, text) {
|
||||
// Don't render a Markdown link for the anchor link inside a heading
|
||||
return text;
|
||||
},
|
||||
'a'(el, text) {
|
||||
a(el, text) {
|
||||
return `[${text}](${el.getAttribute('href')})`;
|
||||
},
|
||||
'li'(el, text) {
|
||||
li(el, text) {
|
||||
const lines = text.trim().split('\n');
|
||||
const firstLine = `- ${lines.shift()}`;
|
||||
// Add four spaces to the front of subsequent list items lines,
|
||||
// or leave the line entirely blank.
|
||||
const nextLines = lines.map((s) => {
|
||||
const nextLines = lines.map(s => {
|
||||
if (s.trim().length === 0) return '';
|
||||
|
||||
return ` ${s}`;
|
||||
|
@ -211,49 +221,49 @@ const gfmRules = {
|
|||
|
||||
return `${firstLine}\n${nextLines.join('\n')}`;
|
||||
},
|
||||
'ul'(el, text) {
|
||||
ul(el, text) {
|
||||
return text;
|
||||
},
|
||||
'ol'(el, text) {
|
||||
ol(el, text) {
|
||||
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
|
||||
return text.replace(/^- /mg, '1. ');
|
||||
return text.replace(/^- /gm, '1. ');
|
||||
},
|
||||
'h1'(el, text) {
|
||||
h1(el, text) {
|
||||
return `# ${text.trim()}\n`;
|
||||
},
|
||||
'h2'(el, text) {
|
||||
h2(el, text) {
|
||||
return `## ${text.trim()}\n`;
|
||||
},
|
||||
'h3'(el, text) {
|
||||
h3(el, text) {
|
||||
return `### ${text.trim()}\n`;
|
||||
},
|
||||
'h4'(el, text) {
|
||||
h4(el, text) {
|
||||
return `#### ${text.trim()}\n`;
|
||||
},
|
||||
'h5'(el, text) {
|
||||
h5(el, text) {
|
||||
return `##### ${text.trim()}\n`;
|
||||
},
|
||||
'h6'(el, text) {
|
||||
h6(el, text) {
|
||||
return `###### ${text.trim()}\n`;
|
||||
},
|
||||
'strong'(el, text) {
|
||||
strong(el, text) {
|
||||
return `**${text}**`;
|
||||
},
|
||||
'em'(el, text) {
|
||||
em(el, text) {
|
||||
return `_${text}_`;
|
||||
},
|
||||
'del'(el, text) {
|
||||
del(el, text) {
|
||||
return `~~${text}~~`;
|
||||
},
|
||||
'hr'(el) {
|
||||
hr(el) {
|
||||
// extra leading \n is to ensure that there is a blank line between
|
||||
// a list followed by an hr, otherwise this breaks old redcarpet rendering
|
||||
return '\n-----\n';
|
||||
},
|
||||
'p'(el, text) {
|
||||
p(el, text) {
|
||||
return `${text.trim()}\n`;
|
||||
},
|
||||
'table'(el) {
|
||||
table(el) {
|
||||
const theadEl = el.querySelector('thead');
|
||||
const tbodyEl = el.querySelector('tbody');
|
||||
if (!theadEl || !tbodyEl) return false;
|
||||
|
@ -263,8 +273,8 @@ const gfmRules = {
|
|||
|
||||
return [theadText, tbodyText].join('\n');
|
||||
},
|
||||
'thead'(el, text) {
|
||||
const cells = _.map(el.querySelectorAll('th'), (cell) => {
|
||||
thead(el, text) {
|
||||
const cells = _.map(el.querySelectorAll('th'), cell => {
|
||||
let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
|
||||
|
||||
let before = '';
|
||||
|
@ -296,7 +306,7 @@ const gfmRules = {
|
|||
|
||||
return [text, separatorRow].join('\n');
|
||||
},
|
||||
'tr'(el) {
|
||||
tr(el) {
|
||||
const cellEls = el.querySelectorAll('td, th');
|
||||
if (cellEls.length === 0) return false;
|
||||
|
||||
|
@ -315,8 +325,12 @@ export class CopyAsGFM {
|
|||
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
|
||||
if (isIOS) return;
|
||||
|
||||
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
|
||||
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
|
||||
$(document).on('copy', '.md, .wiki', e => {
|
||||
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
|
||||
});
|
||||
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', e => {
|
||||
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection);
|
||||
});
|
||||
$(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
|
||||
}
|
||||
|
||||
|
@ -356,7 +370,7 @@ export class CopyAsGFM {
|
|||
// This will break down when the actual code block contains an uneven
|
||||
// number of backticks, but this is a rare edge case.
|
||||
const backtickMatch = textBefore.match(/`/g);
|
||||
const insideCodeBlock = backtickMatch && (backtickMatch.length % 2) === 1;
|
||||
const insideCodeBlock = backtickMatch && backtickMatch.length % 2 === 1;
|
||||
|
||||
if (insideCodeBlock) {
|
||||
return text;
|
||||
|
@ -393,7 +407,9 @@ export class CopyAsGFM {
|
|||
let lineSelector = '.line';
|
||||
|
||||
if (target) {
|
||||
const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
|
||||
const lineClass = ['left-side', 'right-side'].filter(name =>
|
||||
target.classList.contains(name),
|
||||
)[0];
|
||||
if (lineClass) {
|
||||
lineSelector = `.line_content.${lineClass} ${lineSelector}`;
|
||||
}
|
||||
|
@ -436,7 +452,8 @@ export class CopyAsGFM {
|
|||
return node.textContent;
|
||||
}
|
||||
|
||||
const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
|
||||
const respectWhitespace =
|
||||
respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
|
||||
|
||||
const text = this.innerGFM(node, respectWhitespace);
|
||||
|
||||
|
|
|
@ -32,7 +32,9 @@ export default function renderMath($els) {
|
|||
Promise.all([
|
||||
import(/* webpackChunkName: 'katex' */ 'katex'),
|
||||
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
|
||||
]).then(([katex]) => {
|
||||
renderWithKaTeX($els, katex);
|
||||
}).catch(() => flash(__('An error occurred while rendering KaTeX')));
|
||||
])
|
||||
.then(([katex]) => {
|
||||
renderWithKaTeX($els, katex);
|
||||
})
|
||||
.catch(() => flash(__('An error occurred while rendering KaTeX')));
|
||||
}
|
||||
|
|
|
@ -17,41 +17,43 @@ import flash from '~/flash';
|
|||
export default function renderMermaid($els) {
|
||||
if (!$els.length) return;
|
||||
|
||||
import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => {
|
||||
mermaid.initialize({
|
||||
// mermaid core options
|
||||
mermaid: {
|
||||
startOnLoad: false,
|
||||
},
|
||||
// mermaidAPI options
|
||||
theme: 'neutral',
|
||||
});
|
||||
|
||||
$els.each((i, el) => {
|
||||
const source = el.textContent;
|
||||
|
||||
// Remove any extra spans added by the backend syntax highlighting.
|
||||
Object.assign(el, { textContent: source });
|
||||
|
||||
mermaid.init(undefined, el, (id) => {
|
||||
const svg = document.getElementById(id);
|
||||
|
||||
svg.classList.add('mermaid');
|
||||
|
||||
// pre > code > svg
|
||||
svg.closest('pre').replaceWith(svg);
|
||||
|
||||
// We need to add the original source into the DOM to allow Copy-as-GFM
|
||||
// to access it.
|
||||
const sourceEl = document.createElement('text');
|
||||
sourceEl.classList.add('source');
|
||||
sourceEl.setAttribute('display', 'none');
|
||||
sourceEl.textContent = source;
|
||||
|
||||
svg.appendChild(sourceEl);
|
||||
import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid')
|
||||
.then(mermaid => {
|
||||
mermaid.initialize({
|
||||
// mermaid core options
|
||||
mermaid: {
|
||||
startOnLoad: false,
|
||||
},
|
||||
// mermaidAPI options
|
||||
theme: 'neutral',
|
||||
});
|
||||
|
||||
$els.each((i, el) => {
|
||||
const source = el.textContent;
|
||||
|
||||
// Remove any extra spans added by the backend syntax highlighting.
|
||||
Object.assign(el, { textContent: source });
|
||||
|
||||
mermaid.init(undefined, el, id => {
|
||||
const svg = document.getElementById(id);
|
||||
|
||||
svg.classList.add('mermaid');
|
||||
|
||||
// pre > code > svg
|
||||
svg.closest('pre').replaceWith(svg);
|
||||
|
||||
// We need to add the original source into the DOM to allow Copy-as-GFM
|
||||
// to access it.
|
||||
const sourceEl = document.createElement('text');
|
||||
sourceEl.classList.add('source');
|
||||
sourceEl.setAttribute('display', 'none');
|
||||
sourceEl.textContent = source;
|
||||
|
||||
svg.appendChild(sourceEl);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
flash(`Can't load mermaid module: ${err}`);
|
||||
});
|
||||
}).catch((err) => {
|
||||
flash(`Can't load mermaid module: ${err}`);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
|
|||
|
||||
MarkdownPreview.prototype.ajaxCache = {};
|
||||
|
||||
MarkdownPreview.prototype.showPreview = function ($form) {
|
||||
MarkdownPreview.prototype.showPreview = function($form) {
|
||||
var mdText;
|
||||
var markdownVersion;
|
||||
var url;
|
||||
|
@ -44,34 +44,40 @@ MarkdownPreview.prototype.showPreview = function ($form) {
|
|||
this.hideReferencedUsers($form);
|
||||
} else {
|
||||
preview.addClass('md-preview-loading').text('Loading...');
|
||||
this.fetchMarkdownPreview(mdText, url, (function (response) {
|
||||
var body;
|
||||
if (response.body.length > 0) {
|
||||
({ body } = response);
|
||||
} else {
|
||||
body = this.emptyMessage;
|
||||
}
|
||||
this.fetchMarkdownPreview(
|
||||
mdText,
|
||||
url,
|
||||
function(response) {
|
||||
var body;
|
||||
if (response.body.length > 0) {
|
||||
({ body } = response);
|
||||
} else {
|
||||
body = this.emptyMessage;
|
||||
}
|
||||
|
||||
preview.removeClass('md-preview-loading').html(body);
|
||||
preview.renderGFM();
|
||||
this.renderReferencedUsers(response.references.users, $form);
|
||||
preview.removeClass('md-preview-loading').html(body);
|
||||
preview.renderGFM();
|
||||
this.renderReferencedUsers(response.references.users, $form);
|
||||
|
||||
if (response.references.commands) {
|
||||
this.renderReferencedCommands(response.references.commands, $form);
|
||||
}
|
||||
}).bind(this));
|
||||
if (response.references.commands) {
|
||||
this.renderReferencedCommands(response.references.commands, $form);
|
||||
}
|
||||
}.bind(this),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.versionedPreviewPath = function (markdownPreviewPath, markdownVersion) {
|
||||
MarkdownPreview.prototype.versionedPreviewPath = function(markdownPreviewPath, markdownVersion) {
|
||||
if (typeof markdownVersion === 'undefined') {
|
||||
return markdownPreviewPath;
|
||||
}
|
||||
|
||||
return `${markdownPreviewPath}${markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'}markdown_version=${markdownVersion}`;
|
||||
return `${markdownPreviewPath}${
|
||||
markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
|
||||
}markdown_version=${markdownVersion}`;
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
|
||||
MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
@ -79,24 +85,25 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
|
|||
success(this.ajaxCache.response);
|
||||
return;
|
||||
}
|
||||
axios.post(url, {
|
||||
text,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.ajaxCache = {
|
||||
text: text,
|
||||
response: data,
|
||||
};
|
||||
success(data);
|
||||
})
|
||||
.catch(() => flash(__('An error occurred while fetching markdown preview')));
|
||||
axios
|
||||
.post(url, {
|
||||
text,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.ajaxCache = {
|
||||
text: text,
|
||||
response: data,
|
||||
};
|
||||
success(data);
|
||||
})
|
||||
.catch(() => flash(__('An error occurred while fetching markdown preview')));
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
|
||||
MarkdownPreview.prototype.hideReferencedUsers = function($form) {
|
||||
$form.find('.referenced-users').hide();
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
|
||||
MarkdownPreview.prototype.renderReferencedUsers = function(users, $form) {
|
||||
var referencedUsers;
|
||||
referencedUsers = $form.find('.referenced-users');
|
||||
if (referencedUsers.length) {
|
||||
|
@ -109,11 +116,11 @@ MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
|
|||
}
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.hideReferencedCommands = function ($form) {
|
||||
MarkdownPreview.prototype.hideReferencedCommands = function($form) {
|
||||
$form.find('.referenced-commands').hide();
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) {
|
||||
MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) {
|
||||
var referencedCommands;
|
||||
referencedCommands = $form.find('.referenced-commands');
|
||||
if (commands.length > 0) {
|
||||
|
@ -132,14 +139,14 @@ writeButtonSelector = '.js-md-write-button';
|
|||
lastTextareaPreviewed = null;
|
||||
const markdownToolbar = $('.md-header-toolbar');
|
||||
|
||||
$.fn.setupMarkdownPreview = function () {
|
||||
$.fn.setupMarkdownPreview = function() {
|
||||
var $form = $(this);
|
||||
$form.find('textarea.markdown-area').on('input', function () {
|
||||
$form.find('textarea.markdown-area').on('input', function() {
|
||||
markdownPreview.hideReferencedUsers($form);
|
||||
});
|
||||
};
|
||||
|
||||
$(document).on('markdown-preview:show', function (e, $form) {
|
||||
$(document).on('markdown-preview:show', function(e, $form) {
|
||||
if (!$form) {
|
||||
return;
|
||||
}
|
||||
|
@ -148,8 +155,14 @@ $(document).on('markdown-preview:show', function (e, $form) {
|
|||
lastTextareaHeight = lastTextareaPreviewed.height();
|
||||
|
||||
// toggle tabs
|
||||
$form.find(writeButtonSelector).parent().removeClass('active');
|
||||
$form.find(previewButtonSelector).parent().addClass('active');
|
||||
$form
|
||||
.find(writeButtonSelector)
|
||||
.parent()
|
||||
.removeClass('active');
|
||||
$form
|
||||
.find(previewButtonSelector)
|
||||
.parent()
|
||||
.addClass('active');
|
||||
|
||||
// toggle content
|
||||
$form.find('.md-write-holder').hide();
|
||||
|
@ -158,7 +171,7 @@ $(document).on('markdown-preview:show', function (e, $form) {
|
|||
markdownPreview.showPreview($form);
|
||||
});
|
||||
|
||||
$(document).on('markdown-preview:hide', function (e, $form) {
|
||||
$(document).on('markdown-preview:hide', function(e, $form) {
|
||||
if (!$form) {
|
||||
return;
|
||||
}
|
||||
|
@ -169,8 +182,14 @@ $(document).on('markdown-preview:hide', function (e, $form) {
|
|||
}
|
||||
|
||||
// toggle tabs
|
||||
$form.find(writeButtonSelector).parent().addClass('active');
|
||||
$form.find(previewButtonSelector).parent().removeClass('active');
|
||||
$form
|
||||
.find(writeButtonSelector)
|
||||
.parent()
|
||||
.addClass('active');
|
||||
$form
|
||||
.find(previewButtonSelector)
|
||||
.parent()
|
||||
.removeClass('active');
|
||||
|
||||
// toggle content
|
||||
$form.find('.md-write-holder').show();
|
||||
|
@ -181,7 +200,7 @@ $(document).on('markdown-preview:hide', function (e, $form) {
|
|||
markdownPreview.hideReferencedCommands($form);
|
||||
});
|
||||
|
||||
$(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
|
||||
$(document).on('markdown-preview:toggle', function(e, keyboardEvent) {
|
||||
var $target;
|
||||
$target = $(keyboardEvent.target);
|
||||
if ($target.is('textarea.markdown-area')) {
|
||||
|
@ -194,14 +213,14 @@ $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
|
|||
}
|
||||
});
|
||||
|
||||
$(document).on('click', previewButtonSelector, function (e) {
|
||||
$(document).on('click', previewButtonSelector, function(e) {
|
||||
var $form;
|
||||
e.preventDefault();
|
||||
$form = $(this).closest('form');
|
||||
$(document).triggerHandler('markdown-preview:show', [$form]);
|
||||
});
|
||||
|
||||
$(document).on('click', writeButtonSelector, function (e) {
|
||||
$(document).on('click', writeButtonSelector, function(e) {
|
||||
var $form;
|
||||
e.preventDefault();
|
||||
$form = $(this).closest('form');
|
||||
|
|
|
@ -28,7 +28,7 @@ function keyCodeIs(e, keyCode) {
|
|||
return e.keyCode === keyCode;
|
||||
}
|
||||
|
||||
$(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
|
||||
$(document).on('keydown.quick_submit', '.js-quick-submit', e => {
|
||||
// Enter
|
||||
if (!keyCodeIs(e, 13)) {
|
||||
return;
|
||||
|
@ -55,23 +55,25 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
|
|||
|
||||
// If the user tabs to a submit button on a `js-quick-submit` form, display a
|
||||
// tooltip to let them know they could've used the hotkey
|
||||
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function displayTooltip(e) {
|
||||
// Tab
|
||||
if (!keyCodeIs(e, 9)) {
|
||||
return;
|
||||
}
|
||||
$(document).on(
|
||||
'keyup.quick_submit',
|
||||
'.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]',
|
||||
function displayTooltip(e) {
|
||||
// Tab
|
||||
if (!keyCodeIs(e, 9)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $this = $(this);
|
||||
const title = isMac() ?
|
||||
'You can also press ⌘-Enter' :
|
||||
'You can also press Ctrl-Enter';
|
||||
const $this = $(this);
|
||||
const title = isMac() ? 'You can also press ⌘-Enter' : 'You can also press Ctrl-Enter';
|
||||
|
||||
$this.tooltip({
|
||||
container: 'body',
|
||||
html: 'true',
|
||||
placement: 'top',
|
||||
title,
|
||||
trigger: 'manual',
|
||||
});
|
||||
$this.tooltip('show').one('blur click', () => $this.tooltip('hide'));
|
||||
});
|
||||
$this.tooltip({
|
||||
container: 'body',
|
||||
html: 'true',
|
||||
placement: 'top',
|
||||
title,
|
||||
trigger: 'manual',
|
||||
});
|
||||
$this.tooltip('show').one('blur click', () => $this.tooltip('hide'));
|
||||
},
|
||||
);
|
||||
|
|
|
@ -18,7 +18,8 @@ import '../commons/bootstrap';
|
|||
$.fn.requiresInput = function requiresInput() {
|
||||
const $form = $(this);
|
||||
const $button = $('button[type=submit], input[type=submit]', $form);
|
||||
const fieldSelector = 'input[required=required], select[required=required], textarea[required=required]';
|
||||
const fieldSelector =
|
||||
'input[required=required], select[required=required], textarea[required=required]';
|
||||
|
||||
function requireInput() {
|
||||
// Collect the input values of *all* required fields
|
||||
|
|
|
@ -32,16 +32,18 @@ export default class SecretValues {
|
|||
|
||||
updateDom(isRevealed) {
|
||||
const values = this.container.querySelectorAll(this.valueSelector);
|
||||
values.forEach((value) => {
|
||||
values.forEach(value => {
|
||||
value.classList.toggle('hide', !isRevealed);
|
||||
});
|
||||
|
||||
const placeholders = this.container.querySelectorAll(this.placeholderSelector);
|
||||
placeholders.forEach((placeholder) => {
|
||||
placeholders.forEach(placeholder => {
|
||||
placeholder.classList.toggle('hide', isRevealed);
|
||||
});
|
||||
|
||||
this.revealButton.textContent = isRevealed ? n__('Hide value', 'Hide values', values.length) : n__('Reveal value', 'Reveal values', values.length);
|
||||
this.revealButton.textContent = isRevealed
|
||||
? n__('Hide value', 'Hide values', values.length)
|
||||
: n__('Reveal value', 'Reveal values', values.length);
|
||||
this.revealButton.dataset.secretRevealStatus = isRevealed;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,22 +88,24 @@ export default class Shortcuts {
|
|||
return null;
|
||||
}
|
||||
|
||||
return axios.get(gon.shortcuts_path, {
|
||||
responseType: 'text',
|
||||
}).then(({ data }) => {
|
||||
$.globalEval(data);
|
||||
return axios
|
||||
.get(gon.shortcuts_path, {
|
||||
responseType: 'text',
|
||||
})
|
||||
.then(({ data }) => {
|
||||
$.globalEval(data);
|
||||
|
||||
if (location && location.length > 0) {
|
||||
const results = [];
|
||||
for (let i = 0, len = location.length; i < len; i += 1) {
|
||||
results.push($(location[i]).show());
|
||||
if (location && location.length > 0) {
|
||||
const results = [];
|
||||
for (let i = 0, len = location.length; i < len; i += 1) {
|
||||
results.push($(location[i]).show());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
$('.hidden-shortcut').show();
|
||||
return $('.js-more-help-button').remove();
|
||||
});
|
||||
$('.hidden-shortcut').show();
|
||||
return $('.js-more-help-button').remove();
|
||||
});
|
||||
}
|
||||
|
||||
focusFilter(e) {
|
||||
|
|
|
@ -18,9 +18,7 @@ $(() => {
|
|||
.toggleClass('fa-chevron-up', toggleState)
|
||||
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
|
||||
|
||||
$container
|
||||
.find('.js-toggle-content')
|
||||
.toggle(toggleState);
|
||||
$container.find('.js-toggle-content').toggle(toggleState);
|
||||
}
|
||||
|
||||
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
|
||||
|
|
|
@ -18,12 +18,7 @@ export default class Renderer {
|
|||
this.loader = new STLLoader();
|
||||
|
||||
this.fov = 45;
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
this.fov,
|
||||
this.width / this.height,
|
||||
1,
|
||||
1000,
|
||||
);
|
||||
this.camera = new THREE.PerspectiveCamera(this.fov, this.width / this.height, 1, 1000);
|
||||
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
|
@ -35,10 +30,7 @@ export default class Renderer {
|
|||
this.setupLight();
|
||||
|
||||
// Set up OrbitControls
|
||||
this.controls = new OrbitControls(
|
||||
this.camera,
|
||||
this.renderer.domElement,
|
||||
);
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.minDistance = 5;
|
||||
this.controls.maxDistance = 30;
|
||||
this.controls.enableKeys = false;
|
||||
|
@ -51,47 +43,32 @@ export default class Renderer {
|
|||
antialias: true,
|
||||
});
|
||||
|
||||
this.renderer.setClearColor(0xFFFFFF);
|
||||
this.renderer.setClearColor(0xffffff);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.renderer.setSize(
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
this.renderer.setSize(this.width, this.height);
|
||||
}
|
||||
|
||||
setupLight() {
|
||||
// Point light illuminates the object
|
||||
const pointLight = new THREE.PointLight(
|
||||
0xFFFFFF,
|
||||
2,
|
||||
0,
|
||||
);
|
||||
const pointLight = new THREE.PointLight(0xffffff, 2, 0);
|
||||
|
||||
pointLight.castShadow = true;
|
||||
|
||||
this.camera.add(pointLight);
|
||||
|
||||
// Ambient light illuminates the scene
|
||||
const ambientLight = new THREE.AmbientLight(
|
||||
0xFFFFFF,
|
||||
1,
|
||||
);
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||
this.scene.add(ambientLight);
|
||||
}
|
||||
|
||||
setupGrid() {
|
||||
this.grid = new THREE.GridHelper(
|
||||
20,
|
||||
20,
|
||||
0x000000,
|
||||
0x000000,
|
||||
);
|
||||
this.grid = new THREE.GridHelper(20, 20, 0x000000, 0x000000);
|
||||
|
||||
this.scene.add(this.grid);
|
||||
}
|
||||
|
||||
loadFile() {
|
||||
this.loader.load(this.container.dataset.endpoint, (geo) => {
|
||||
this.loader.load(this.container.dataset.endpoint, geo => {
|
||||
const obj = new MeshObject(geo);
|
||||
|
||||
this.objects.push(obj);
|
||||
|
@ -116,30 +93,23 @@ export default class Renderer {
|
|||
}
|
||||
|
||||
render() {
|
||||
this.renderer.render(
|
||||
this.scene,
|
||||
this.camera,
|
||||
);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
|
||||
requestAnimationFrame(this.renderWrapper);
|
||||
}
|
||||
|
||||
changeObjectMaterials(type) {
|
||||
this.objects.forEach((obj) => {
|
||||
this.objects.forEach(obj => {
|
||||
obj.changeMaterial(type);
|
||||
});
|
||||
}
|
||||
|
||||
setDefaultCameraPosition() {
|
||||
const obj = this.objects[0];
|
||||
const radius = (obj.geometry.boundingSphere.radius / 1.5);
|
||||
const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2));
|
||||
const radius = obj.geometry.boundingSphere.radius / 1.5;
|
||||
const dist = radius / Math.sin((this.fov * (Math.PI / 180)) / 2);
|
||||
|
||||
this.camera.position.set(
|
||||
0,
|
||||
dist + 1,
|
||||
dist,
|
||||
);
|
||||
this.camera.position.set(0, dist + 1, dist);
|
||||
|
||||
this.camera.lookAt(this.grid);
|
||||
this.controls.update();
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import {
|
||||
Matrix4,
|
||||
MeshLambertMaterial,
|
||||
Mesh,
|
||||
} from 'three/build/three.module';
|
||||
import { Matrix4, MeshLambertMaterial, Mesh } from 'three/build/three.module';
|
||||
|
||||
const defaultColor = 0xE24329;
|
||||
const defaultColor = 0xe24329;
|
||||
const materials = {
|
||||
default: new MeshLambertMaterial({
|
||||
color: defaultColor,
|
||||
|
@ -17,10 +13,7 @@ const materials = {
|
|||
|
||||
export default class MeshObject extends Mesh {
|
||||
constructor(geo) {
|
||||
super(
|
||||
geo,
|
||||
materials.default,
|
||||
);
|
||||
super(geo, materials.default);
|
||||
|
||||
this.geometry.computeBoundingSphere();
|
||||
|
||||
|
@ -29,13 +22,7 @@ export default class MeshObject extends Mesh {
|
|||
if (this.geometry.boundingSphere.radius > 4) {
|
||||
const scale = 4 / this.geometry.boundingSphere.radius;
|
||||
|
||||
this.geometry.applyMatrix(
|
||||
new Matrix4().makeScale(
|
||||
scale,
|
||||
scale,
|
||||
scale,
|
||||
),
|
||||
);
|
||||
this.geometry.applyMatrix(new Matrix4().makeScale(scale, scale, scale));
|
||||
this.geometry.computeBoundingSphere();
|
||||
|
||||
this.position.x = -this.geometry.boundingSphere.center.x;
|
||||
|
|
|
@ -42,7 +42,7 @@ class BalsamiqViewer {
|
|||
this.initDatabase(loadEvent.target.response);
|
||||
|
||||
const previews = this.getPreviews();
|
||||
previews.forEach((preview) => {
|
||||
previews.forEach(preview => {
|
||||
const renderedPreview = this.renderPreview(preview);
|
||||
|
||||
container.appendChild(renderedPreview);
|
||||
|
|
|
@ -41,39 +41,45 @@ export default class BlobFileDropzone {
|
|||
addRemoveLinks: true,
|
||||
previewsContainer: '.dropzone-previews',
|
||||
headers: csrf.headers,
|
||||
init: function () {
|
||||
this.on('addedfile', function () {
|
||||
init: function() {
|
||||
this.on('addedfile', function() {
|
||||
toggleLoading(submitButton, submitButtonLoadingIcon, false);
|
||||
dropzoneMessage.addClass(HIDDEN_CLASS);
|
||||
$('.dropzone-alerts').html('').hide();
|
||||
$('.dropzone-alerts')
|
||||
.html('')
|
||||
.hide();
|
||||
});
|
||||
this.on('removedfile', function () {
|
||||
this.on('removedfile', function() {
|
||||
toggleLoading(submitButton, submitButtonLoadingIcon, false);
|
||||
dropzoneMessage.removeClass(HIDDEN_CLASS);
|
||||
});
|
||||
this.on('success', function (header, response) {
|
||||
this.on('success', function(header, response) {
|
||||
$('#modal-upload-blob').modal('hide');
|
||||
visitUrl(response.filePath);
|
||||
});
|
||||
this.on('maxfilesexceeded', function (file) {
|
||||
this.on('maxfilesexceeded', function(file) {
|
||||
dropzoneMessage.addClass(HIDDEN_CLASS);
|
||||
this.removeFile(file);
|
||||
});
|
||||
this.on('sending', function (file, xhr, formData) {
|
||||
this.on('sending', function(file, xhr, formData) {
|
||||
formData.append('branch_name', form.find('.js-branch-name').val());
|
||||
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
|
||||
formData.append('commit_message', form.find('.js-commit-message').val());
|
||||
});
|
||||
},
|
||||
// Override behavior of adding error underneath preview
|
||||
error: function (file, errorMessage) {
|
||||
const stripped = $('<div/>').html(errorMessage).text();
|
||||
$('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show();
|
||||
error: function(file, errorMessage) {
|
||||
const stripped = $('<div/>')
|
||||
.html(errorMessage)
|
||||
.text();
|
||||
$('.dropzone-alerts')
|
||||
.html(`Error uploading file: "${stripped}"`)
|
||||
.show();
|
||||
this.removeFile(file);
|
||||
},
|
||||
});
|
||||
|
||||
submitButton.on('click', (e) => {
|
||||
submitButton.on('click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
|
||||
|
|
|
@ -2,17 +2,19 @@ import { getLocationHash } from '../lib/utils/url_utility';
|
|||
|
||||
const lineNumberRe = /^L[0-9]+/;
|
||||
|
||||
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
|
||||
const updateLineNumbersOnBlobPermalinks = linksToUpdate => {
|
||||
const hash = getLocationHash();
|
||||
if (hash && lineNumberRe.test(hash)) {
|
||||
const hashUrlString = `#${hash}`;
|
||||
|
||||
[].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
|
||||
const baseHref = permalinkButton.getAttribute('data-original-href') || (() => {
|
||||
const href = permalinkButton.getAttribute('href');
|
||||
permalinkButton.setAttribute('data-original-href', href);
|
||||
return href;
|
||||
})();
|
||||
[].concat(Array.prototype.slice.call(linksToUpdate)).forEach(permalinkButton => {
|
||||
const baseHref =
|
||||
permalinkButton.getAttribute('data-original-href') ||
|
||||
(() => {
|
||||
const href = permalinkButton.getAttribute('href');
|
||||
permalinkButton.setAttribute('data-original-href', href);
|
||||
return href;
|
||||
})();
|
||||
permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
|
||||
});
|
||||
}
|
||||
|
@ -26,7 +28,7 @@ function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, element
|
|||
}, 0);
|
||||
};
|
||||
|
||||
blobContentHolder.addEventListener('click', (e) => {
|
||||
blobContentHolder.addEventListener('click', e => {
|
||||
if (e.target.matches(lineNumberSelector)) {
|
||||
updateBlameAndBlobPermalinkCb();
|
||||
}
|
||||
|
|
|
@ -45,15 +45,11 @@ export default class FileTemplateSelector {
|
|||
}
|
||||
|
||||
renderLoading() {
|
||||
this.$loadingIcon
|
||||
.addClass('fa-spinner fa-spin')
|
||||
.removeClass('fa-chevron-down');
|
||||
this.$loadingIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
|
||||
}
|
||||
|
||||
renderLoaded() {
|
||||
this.$loadingIcon
|
||||
.addClass('fa-chevron-down')
|
||||
.removeClass('fa-spinner fa-spin');
|
||||
this.$loadingIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin');
|
||||
}
|
||||
|
||||
reportSelection(options) {
|
||||
|
|
|
@ -40,13 +40,14 @@ export default () => {
|
|||
},
|
||||
methods: {
|
||||
loadFile() {
|
||||
axios.get(el.dataset.endpoint)
|
||||
axios
|
||||
.get(el.dataset.endpoint)
|
||||
.then(res => res.data)
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
this.json = data;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch(e => {
|
||||
if (e.status !== 200) {
|
||||
this.loadError = true;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export default class SketchLoader {
|
|||
return this.getZipFile()
|
||||
.then(data => JSZip.loadAsync(data))
|
||||
.then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array'))
|
||||
.then((content) => {
|
||||
.then(content => {
|
||||
const url = window.URL || window.webkitURL;
|
||||
const blob = new Blob([new Uint8Array(content)], {
|
||||
type: 'image/png',
|
||||
|
|
|
@ -3,8 +3,8 @@ import Renderer from './3d_viewer';
|
|||
export default () => {
|
||||
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
|
||||
|
||||
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
|
||||
el.addEventListener('click', (e) => {
|
||||
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach(el => {
|
||||
el.addEventListener('click', e => {
|
||||
const { target } = e;
|
||||
|
||||
e.preventDefault();
|
||||
|
|
|
@ -81,14 +81,10 @@ export default class TemplateSelector {
|
|||
}
|
||||
|
||||
startLoadingSpinner() {
|
||||
this.$dropdownIcon
|
||||
.addClass('fa-spinner fa-spin')
|
||||
.removeClass('fa-chevron-down');
|
||||
this.$dropdownIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
|
||||
}
|
||||
|
||||
stopLoadingSpinner() {
|
||||
this.$dropdownIcon
|
||||
.addClass('fa-chevron-down')
|
||||
.removeClass('fa-spinner fa-spin');
|
||||
this.$dropdownIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
|
|||
search: {
|
||||
fields: ['name'],
|
||||
},
|
||||
clicked: (options) => {
|
||||
clicked: options => {
|
||||
const { e } = options;
|
||||
const el = options.$el;
|
||||
const query = options.selectedObj;
|
||||
|
|
|
@ -21,5 +21,4 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector {
|
|||
text: item => item.name,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,9 +22,8 @@ export default class BlobViewer {
|
|||
const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
|
||||
if (!viewer || !viewer.dataset.richType) return;
|
||||
|
||||
const initViewer = promise => promise
|
||||
.then(module => module.default(viewer))
|
||||
.catch((error) => {
|
||||
const initViewer = promise =>
|
||||
promise.then(module => module.default(viewer)).catch(error => {
|
||||
Flash('Error loading file viewer.');
|
||||
throw error;
|
||||
});
|
||||
|
@ -79,10 +78,9 @@ export default class BlobViewer {
|
|||
|
||||
initBindings() {
|
||||
if (this.switcherBtns.length) {
|
||||
Array.from(this.switcherBtns)
|
||||
.forEach((el) => {
|
||||
el.addEventListener('click', this.switchViewHandler.bind(this));
|
||||
});
|
||||
Array.from(this.switcherBtns).forEach(el => {
|
||||
el.addEventListener('click', this.switchViewHandler.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
if (this.copySourceBtn) {
|
||||
|
@ -109,7 +107,10 @@ export default class BlobViewer {
|
|||
this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
|
||||
this.copySourceBtn.classList.remove('disabled');
|
||||
} else if (this.activeViewer === this.simpleViewer) {
|
||||
this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard');
|
||||
this.copySourceBtn.setAttribute(
|
||||
'title',
|
||||
'Wait for the source to load to copy it to the clipboard',
|
||||
);
|
||||
this.copySourceBtn.classList.add('disabled');
|
||||
} else {
|
||||
this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
|
||||
|
@ -147,15 +148,15 @@ export default class BlobViewer {
|
|||
this.toggleCopyButtonState();
|
||||
|
||||
BlobViewer.loadViewer(newViewer)
|
||||
.then((viewer) => {
|
||||
$(viewer).renderGFM();
|
||||
.then(viewer => {
|
||||
$(viewer).renderGFM();
|
||||
|
||||
this.$fileHolder.trigger('highlight:line');
|
||||
handleLocationHash();
|
||||
this.$fileHolder.trigger('highlight:line');
|
||||
handleLocationHash();
|
||||
|
||||
this.toggleCopyButtonState();
|
||||
})
|
||||
.catch(() => new Flash('Error loading viewer'));
|
||||
this.toggleCopyButtonState();
|
||||
})
|
||||
.catch(() => new Flash('Error loading viewer'));
|
||||
}
|
||||
|
||||
static loadViewer(viewerParam) {
|
||||
|
@ -168,12 +169,11 @@ export default class BlobViewer {
|
|||
|
||||
viewer.setAttribute('data-loading', 'true');
|
||||
|
||||
return axios.get(url)
|
||||
.then(({ data }) => {
|
||||
viewer.innerHTML = data.html;
|
||||
viewer.setAttribute('data-loaded', 'true');
|
||||
return axios.get(url).then(({ data }) => {
|
||||
viewer.innerHTML = data.html;
|
||||
viewer.setAttribute('data-loaded', 'true');
|
||||
|
||||
return viewer;
|
||||
});
|
||||
return viewer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
documentationLink: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
documentationLink: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconCycleAnalyticsSplash() {
|
||||
return iconCycleAnalyticsSplash;
|
||||
},
|
||||
computed: {
|
||||
iconCycleAnalyticsSplash() {
|
||||
return iconCycleAnalyticsSplash;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dismissOverviewDialog() {
|
||||
this.$emit('dismiss-overview-dialog');
|
||||
},
|
||||
methods: {
|
||||
dismissOverviewDialog() {
|
||||
this.$emit('dismiss-overview-dialog');
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="landing content-block">
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<script>
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<script>
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
limitWarning,
|
||||
totalTime,
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
limitWarning,
|
||||
totalTime,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<script>
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
limitWarning,
|
||||
totalTime,
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
limitWarning,
|
||||
totalTime,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
@ -73,4 +73,3 @@
|
|||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
<script>
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import iconCommit from '../svg/icon_commit.svg';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import iconCommit from '../svg/icon_commit.svg';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
totalTime,
|
||||
limitWarning,
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
totalTime,
|
||||
limitWarning,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
computed: {
|
||||
iconCommit() {
|
||||
return iconCommit;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconCommit() {
|
||||
return iconCommit;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
@ -74,4 +74,3 @@
|
|||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
<script>
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
totalTime,
|
||||
limitWarning,
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
totalTime,
|
||||
limitWarning,
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
<script>
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import iconBranch from '../svg/icon_branch.svg';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import iconBranch from '../svg/icon_branch.svg';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
totalTime,
|
||||
limitWarning,
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
userAvatarImage,
|
||||
totalTime,
|
||||
limitWarning,
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
computed: {
|
||||
iconBranch() {
|
||||
return iconBranch;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconBranch() {
|
||||
return iconBranch;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
<script>
|
||||
import iconBuildStatus from '../svg/icon_build_status.svg';
|
||||
import iconBranch from '../svg/icon_branch.svg';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import iconBuildStatus from '../svg/icon_build_status.svg';
|
||||
import iconBranch from '../svg/icon_branch.svg';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
import totalTime from './total_time_component.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
totalTime,
|
||||
limitWarning,
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
totalTime,
|
||||
limitWarning,
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
computed: {
|
||||
iconBuildStatus() {
|
||||
return iconBuildStatus;
|
||||
},
|
||||
iconBranch() {
|
||||
return iconBranch;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconBuildStatus() {
|
||||
return iconBuildStatus;
|
||||
},
|
||||
};
|
||||
iconBranch() {
|
||||
return iconBranch;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
time: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
export default {
|
||||
props: {
|
||||
time: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
computed: {
|
||||
hasData() {
|
||||
return Object.keys(this.time).length;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasData() {
|
||||
return Object.keys(this.time).length;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span class="total-time">
|
||||
|
|
|
@ -18,7 +18,8 @@ Vue.use(Translate);
|
|||
export default () => {
|
||||
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
|
||||
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: '#cycle-analytics',
|
||||
name: 'CycleAnalytics',
|
||||
components: {
|
||||
|
@ -66,14 +67,17 @@ export default () => {
|
|||
const $dropdown = $('.js-ca-dropdown');
|
||||
const $label = $dropdown.find('.dropdown-label');
|
||||
|
||||
$dropdown.find('li a').off('click').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
this.startDate = $target.data('value');
|
||||
$dropdown
|
||||
.find('li a')
|
||||
.off('click')
|
||||
.on('click', e => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
this.startDate = $target.data('value');
|
||||
|
||||
$label.text($target.text().trim());
|
||||
this.fetchCycleAnalyticsData({ startDate: this.startDate });
|
||||
});
|
||||
$label.text($target.text().trim());
|
||||
this.fetchCycleAnalyticsData({ startDate: this.startDate });
|
||||
});
|
||||
},
|
||||
fetchCycleAnalyticsData(options) {
|
||||
const fetchOptions = options || { startDate: this.startDate };
|
||||
|
@ -82,7 +86,7 @@ export default () => {
|
|||
|
||||
this.service
|
||||
.fetchCycleAnalyticsData(fetchOptions)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
this.store.setCycleAnalyticsData(response);
|
||||
this.selectDefaultStage();
|
||||
this.initDropdown();
|
||||
|
@ -115,7 +119,7 @@ export default () => {
|
|||
stage,
|
||||
startDate: this.startDate,
|
||||
})
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
this.isEmptyStage = !response.events.length;
|
||||
this.store.setStageEvents(response.events, stage);
|
||||
this.isLoadingStage = false;
|
||||
|
|
|
@ -18,10 +18,7 @@ export default class CycleAnalyticsService {
|
|||
}
|
||||
|
||||
fetchStageData(options) {
|
||||
const {
|
||||
stage,
|
||||
startDate,
|
||||
} = options;
|
||||
const { stage, startDate } = options;
|
||||
|
||||
return this.axios
|
||||
.get(`events/${stage.name}.json`, {
|
||||
|
|
|
@ -5,13 +5,27 @@ import { dasherize } from '../lib/utils/text_utility';
|
|||
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
|
||||
|
||||
const EMPTY_STAGE_TEXTS = {
|
||||
issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
|
||||
plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
|
||||
code: __('The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.'),
|
||||
test: __('The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.'),
|
||||
review: __('The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.'),
|
||||
staging: __('The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.'),
|
||||
production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
|
||||
issue: __(
|
||||
'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
|
||||
),
|
||||
plan: __(
|
||||
'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
|
||||
),
|
||||
code: __(
|
||||
'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
|
||||
),
|
||||
test: __(
|
||||
'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
|
||||
),
|
||||
review: __(
|
||||
'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
|
||||
),
|
||||
staging: __(
|
||||
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
|
||||
),
|
||||
production: __(
|
||||
'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
|
||||
),
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -31,11 +45,11 @@ export default {
|
|||
newData.stages = data.stats || [];
|
||||
newData.summary = data.summary || [];
|
||||
|
||||
newData.summary.forEach((item) => {
|
||||
newData.summary.forEach(item => {
|
||||
item.value = item.value || '-';
|
||||
});
|
||||
|
||||
newData.stages.forEach((item) => {
|
||||
newData.stages.forEach(item => {
|
||||
const stageSlug = dasherize(item.name.toLowerCase());
|
||||
item.active = false;
|
||||
item.isUserAllowed = data.permissions[stageSlug];
|
||||
|
@ -53,7 +67,7 @@ export default {
|
|||
this.state.hasError = state;
|
||||
},
|
||||
deactivateAllStages() {
|
||||
this.state.stages.forEach((stage) => {
|
||||
this.state.stages.forEach(stage => {
|
||||
stage.active = false;
|
||||
});
|
||||
},
|
||||
|
@ -67,7 +81,7 @@ export default {
|
|||
decorateEvents(events, stage) {
|
||||
const newEvents = [];
|
||||
|
||||
events.forEach((item) => {
|
||||
events.forEach(item => {
|
||||
if (!item) return;
|
||||
|
||||
const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
|
||||
|
|
Loading…
Reference in a new issue