Commit dd23edce authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'nt/ce-to-ee-thursday' into 'master'

CE upstream: Thursday

Closes gitlab-ce#32738, gitlab-ce#32647, #2440, gitlab-ce#32551, #2241, gitlab-ce#32449, gitlab-ce#32424, gitlab-ce#30814, and gitlab-ce#17489

See merge request !1981
parents ccf31896 a27363e2
...@@ -55,7 +55,7 @@ stages: ...@@ -55,7 +55,7 @@ stages:
.use-pg: &use-pg .use-pg: &use-pg
services: services:
- postgres:latest - postgres:9.2
- redis:alpine - redis:alpine
- elasticsearch:5.3 - elasticsearch:5.3
...@@ -68,6 +68,7 @@ stages: ...@@ -68,6 +68,7 @@ stages:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only: only:
- /mysql/ - /mysql/
- /-stable$/
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
...@@ -89,7 +90,7 @@ stages: ...@@ -89,7 +90,7 @@ stages:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]} - export CI_NODE_INDEX=${JOB_NAME[-2]}
- export CI_NODE_TOTAL=${JOB_NAME[-1]} - export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
...@@ -120,7 +121,7 @@ stages: ...@@ -120,7 +121,7 @@ stages:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]} - export CI_NODE_INDEX=${JOB_NAME[-2]}
- export CI_NODE_TOTAL=${JOB_NAME[-1]} - export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
...@@ -154,6 +155,7 @@ stages: ...@@ -154,6 +155,7 @@ stages:
# Trigger a package build on omnibus-gitlab repository # Trigger a package build on omnibus-gitlab repository
build-package: build-package:
image: ruby:2.3-alpine
before_script: [] before_script: []
services: [] services: []
variables: variables:
...@@ -183,8 +185,8 @@ update-knapsack: ...@@ -183,8 +185,8 @@ update-knapsack:
<<: *only-canonical-masters <<: *only-canonical-masters
stage: post-test stage: post-test
script: script:
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_pg_node_*.json - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_pg_node_*.json - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
......
...@@ -971,7 +971,7 @@ RSpec/DescribeSymbol: ...@@ -971,7 +971,7 @@ RSpec/DescribeSymbol:
RSpec/DescribedClass: RSpec/DescribedClass:
Enabled: true Enabled: true
# Configuration parameters: CustomIncludeMethods. # Checks if an example group does not include any tests.
RSpec/EmptyExampleGroup: RSpec/EmptyExampleGroup:
Enabled: true Enabled: true
CustomIncludeMethods: CustomIncludeMethods:
...@@ -998,6 +998,10 @@ RSpec/ExampleWording: ...@@ -998,6 +998,10 @@ RSpec/ExampleWording:
RSpec/ExpectActual: RSpec/ExpectActual:
Enabled: true Enabled: true
# Checks for opportunities to use `expect { … }.to output`.
RSpec/ExpectOutput:
Enabled: true
# Checks the file and folder naming of the spec file. # Checks the file and folder naming of the spec file.
RSpec/FilePath: RSpec/FilePath:
Enabled: true Enabled: true
......
...@@ -18,10 +18,6 @@ RSpec/EmptyLineAfterFinalLet: ...@@ -18,10 +18,6 @@ RSpec/EmptyLineAfterFinalLet:
RSpec/EmptyLineAfterSubject: RSpec/EmptyLineAfterSubject:
Enabled: false Enabled: false
# Offense count: 3
RSpec/ExpectOutput:
Enabled: false
# Offense count: 72 # Offense count: 72
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example # SupportedStyles: implicit, each, example
......
9.2.0-pre 9.3.0-pre
/* eslint-disable no-var, wrap-iife, func-names, space-before-function-paren, camelcase, no-unused-vars, quotes, object-shorthand, one-var, one-var-declaration-per-line, prefer-arrow-callback, comma-dangle, prefer-template, no-else-return, yoda, prefer-rest-params, prefer-spread, max-len */ /* eslint-disable no-var, wrap-iife, func-names, space-before-function-paren, camelcase, no-unused-vars, quotes, object-shorthand, one-var, one-var-declaration-per-line, prefer-arrow-callback, comma-dangle, prefer-template, no-else-return, yoda, prefer-rest-params, prefer-spread, max-len */
/* global Api */ import Api from './api';
var slice = [].slice; var slice = [].slice;
......
/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */ import $ from 'jquery';
var Api = { const Api = {
groupsPath: '/api/:version/groups.json', groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id.json', groupPath: '/api/:version/groups/:id.json',
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
...@@ -13,165 +13,190 @@ var Api = { ...@@ -13,165 +13,190 @@ var Api = {
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json', ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
dockerfilePath: '/api/:version/templates/dockerfiles/:key', dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
group: function(group_id, callback) { usersPath: '/api/:version/users.json',
var url = Api.buildUrl(Api.groupPath) group(groupId, callback) {
.replace(':id', group_id); const url = Api.buildUrl(Api.groupPath)
return $.ajax({ .replace(':id', groupId);
url: url,
dataType: 'json'
}).done(function(group) {
return callback(group);
});
},
users: function(search, options, callback = $.noop) {
var url = Api.buildUrl('/autocomplete/users.json');
return $.ajax({ return $.ajax({
url, url,
data: $.extend({ dataType: 'json',
search, })
per_page: 20 .done(group => callback(group));
}, options),
dataType: 'json'
}).done(callback);
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
groups: function(query, options, callback = $.noop) { groups(query, options, callback = $.noop) {
var url = Api.buildUrl(Api.groupsPath); const url = Api.buildUrl(Api.groupsPath);
return $.ajax({ return $.ajax({
url: url, url,
data: $.extend({ data: Object.assign({
search: query, search: query,
per_page: 20 per_page: 20,
}, options), }, options),
dataType: 'json' dataType: 'json',
}).done(function(groups) { })
return callback(groups); .done(groups => callback(groups));
});
}, },
// Return namespaces list. Filtered by query // Return namespaces list. Filtered by query
namespaces: function(query, callback) { namespaces(query, callback) {
var url = Api.buildUrl(Api.namespacesPath); const url = Api.buildUrl(Api.namespacesPath);
return $.ajax({ return $.ajax({
url: url, url,
data: { data: {
search: query, search: query,
per_page: 20 per_page: 20,
}, },
dataType: 'json' dataType: 'json',
}).done(function(namespaces) { }).done(namespaces => callback(namespaces));
return callback(namespaces);
});
}, },
// Return projects list. Filtered by query // Return projects list. Filtered by query
projects: function(query, options, callback) { projects(query, options, callback) {
var url = Api.buildUrl(Api.projectsPath); const url = Api.buildUrl(Api.projectsPath);
return $.ajax({ return $.ajax({
url: url, url,
data: $.extend({ data: Object.assign({
search: query, search: query,
per_page: 20, per_page: 20,
membership: true membership: true,
}, options), }, options),
dataType: 'json' dataType: 'json',
}).done(function(projects) { })
return callback(projects); .done(projects => callback(projects));
});
}, },
newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath) newLabel(namespacePath, projectPath, data, callback) {
.replace(':namespace_path', namespace_path) const url = Api.buildUrl(Api.labelsPath)
.replace(':project_path', project_path); .replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath);
return $.ajax({ return $.ajax({
url: url, url,
type: 'POST', type: 'POST',
data: { 'label': data }, data: { label: data },
dataType: 'json' dataType: 'json',
}).done(function(label) { })
return callback(label); .done(label => callback(label))
}).error(function(message) { .error(message => callback(message.responseJSON));
return callback(message.responseJSON);
});
}, },
// Return group projects list. Filtered by query // Return group projects list. Filtered by query
groupProjects: function(group_id, query, callback) { groupProjects(groupId, query, callback) {
var url = Api.buildUrl(Api.groupProjectsPath) const url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', group_id); .replace(':id', groupId);
return $.ajax({ return $.ajax({
url: url, url,
data: { data: {
search: query, search: query,
per_page: 20 per_page: 20,
}, },
dataType: 'json' dataType: 'json',
}).done(function(projects) { })
return callback(projects); .done(projects => callback(projects));
});
}, },
// Return text for a specific license // Return text for a specific license
licenseText: function(key, data, callback) { licenseText(key, data, callback) {
var url = Api.buildUrl(Api.licensePath) const url = Api.buildUrl(Api.licensePath)
.replace(':key', key); .replace(':key', key);
return $.ajax({ return $.ajax({
url: url, url,
data: data data,
}).done(function(license) { })
return callback(license); .done(license => callback(license));
});
}, },
gitignoreText: function(key, callback) {
var url = Api.buildUrl(Api.gitignorePath) gitignoreText(key, callback) {
const url = Api.buildUrl(Api.gitignorePath)
.replace(':key', key); .replace(':key', key);
return $.get(url, function(gitignore) { return $.get(url, gitignore => callback(gitignore));
return callback(gitignore);
});
}, },
gitlabCiYml: function(key, callback) {
var url = Api.buildUrl(Api.gitlabCiYmlPath) gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath)
.replace(':key', key); .replace(':key', key);
return $.get(url, function(file) { return $.get(url, file => callback(file));
return callback(file);
});
}, },
dockerfileYml: function(key, callback) {
var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
$.get(url, callback); $.get(url, callback);
}, },
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
var url = Api.buildUrl(Api.issuableTemplatePath) issueTemplate(namespacePath, projectPath, key, type, callback) {
const url = Api.buildUrl(Api.issuableTemplatePath)
.replace(':key', key) .replace(':key', key)
.replace(':type', type) .replace(':type', type)
.replace(':project_path', projectPath) .replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath); .replace(':namespace_path', namespacePath);
$.ajax({ $.ajax({
url: url, url,
dataType: 'json' dataType: 'json',
}).done(function(file) { })
callback(null, file); .done(file => callback(null, file))
}).error(callback); .error(callback);
}, },
buildUrl: function(url) {
if (gon.relative_url_root != null) { users(query, options) {
url = gon.relative_url_root + url; const url = Api.buildUrl(this.usersPath);
} return Api.wrapAjaxCall({
return url.replace(':version', gon.api_version); url,
data: Object.assign({
search: query,
per_page: 20,
}, options),
dataType: 'json',
});
}, },
ldap_groups: function(query, provider, callback) {
var url; approverUsers(search, options, callback = $.noop) {
url = Api.buildUrl(Api.ldapGroupsPath); const url = Api.buildUrl('/autocomplete/users.json');
url = url.replace(':provider', provider);
return $.ajax({ return $.ajax({
url: url, url,
data: { data: $.extend({
search,
per_page: 20,
}, options),
dataType: 'json',
}).done(callback);
},
ldap_groups(query, provider, callback) {
const url = Api.buildUrl(this.ldapGroupsPath).replace(':provider', provider);
return Api.wrapAjaxCall({
url,
data: Object.assign({
private_token: gon.api_token, private_token: gon.api_token,
search: query, search: query,
per_page: 20, per_page: 20,
active: true active: true,
}),
dataType: 'json',
})
.done(groups => callback(groups));
}, },
dataType: 'json'
}).done(function(groups) { buildUrl(url) {
return callback(groups); let urlRoot = '';
}); if (gon.relative_url_root != null) {
urlRoot = gon.relative_url_root;
} }
return urlRoot + url.replace(':version', gon.api_version);
},
wrapAjaxCall(options) {
return new Promise((resolve, reject) => {
// jQuery 2 is not Promises/A+ compatible (missing catch)
$.ajax(options) // eslint-disable-line promise/catch-or-return
.then(data => resolve(data),
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus;
reject(error);
},
);
});
},
}; };
window.Api = Api; export default Api;
/* global Api */ import Api from './api';
export default class ApproversSelect { export default class ApproversSelect {
constructor() { constructor() {
...@@ -46,7 +46,7 @@ export default class ApproversSelect { ...@@ -46,7 +46,7 @@ export default class ApproversSelect {
skip_users: ApproversSelect.getApprovers(this.fieldNames[0], '.js-approver'), skip_users: ApproversSelect.getApprovers(this.fieldNames[0], '.js-approver'),
project_id: $('#project_id').val(), project_id: $('#project_id').val(),
}; };
return Api.users(term, options); return Api.approverUsers(term, options);
} }
handleSelectChange(e) { handleSelectChange(e) {
......
/* global Api */
export default class FileTemplateSelector { export default class FileTemplateSelector {
constructor(mediator) { constructor(mediator) {
this.mediator = mediator; this.mediator = mediator;
...@@ -65,4 +63,3 @@ export default class FileTemplateSelector { ...@@ -65,4 +63,3 @@ export default class FileTemplateSelector {
this.reportSelection(opts); this.reportSelection(opts);
} }
} }
/* global Api */ import Api from '../../api';
import FileTemplateSelector from '../file_template_selector'; import FileTemplateSelector from '../file_template_selector';
......
/* global Api */ import Api from '../../api';
import FileTemplateSelector from '../file_template_selector'; import FileTemplateSelector from '../file_template_selector';
......
/* global Api */ import Api from '../../api';
import FileTemplateSelector from '../file_template_selector'; import FileTemplateSelector from '../file_template_selector';
......
/* global Api */ import Api from '../../api';
import FileTemplateSelector from '../file_template_selector'; import FileTemplateSelector from '../file_template_selector';
......
...@@ -50,9 +50,9 @@ export default class BlobViewer { ...@@ -50,9 +50,9 @@ export default class BlobViewer {
if (this.copySourceBtn) { if (this.copySourceBtn) {
this.copySourceBtn.addEventListener('click', () => { this.copySourceBtn.addEventListener('click', () => {
if (this.copySourceBtn.classList.contains('disabled')) return; if (this.copySourceBtn.classList.contains('disabled')) return this.copySourceBtn.blur();
this.switchToViewer('simple'); return this.switchToViewer('simple');
}); });
} }
} }
......
import Vue from 'vue'; import Vue from 'vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({
maxCounter: 99, maxCounter: 99,
}; };
}, },
components: {
userAvatarLink,
},
computed: { computed: {
numberOverLimit() { numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter; return this.issue.assignees.length - this.limitBeforeCounter;
...@@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({
</span> </span>
</h4> </h4>
<div class="card-assignee"> <div class="card-assignee">
<a <user-avatar-link
class="has-tooltip js-no-trigger"
:href="assigneeUrl(assignee)"
:title="assigneeUrlTitle(assignee)"
v-for="(assignee, index) in issue.assignees" v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)" v-if="shouldRenderAssignee(index)"
data-container="body" class="js-no-trigger"
data-placement="bottom" :link-href="assigneeUrl(assignee)"
> :img-alt="avatarUrlTitle(assignee)"
<img :img-src="assignee.avatar"
class="avatar avatar-inline s20" :tooltip-text="assigneeUrlTitle(assignee)"
:src="assignee.avatar" tooltip-placement="bottom"
width="20"
height="20"
:alt="avatarUrlTitle(assignee)"
/> />
</a>
<span <span
class="avatar-counter has-tooltip" class="avatar-counter has-tooltip"
:title="assigneeCounterTooltip" :title="assigneeCounterTooltip"
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
import Vue from 'vue'; import Vue from 'vue';
import queryData from '../../utils/query_data'; import queryData from '../../utils/query_data';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import './header'; import './header';
import './list'; import './list';
import './footer'; import './footer';
......
...@@ -18,12 +18,12 @@ const gfmRules = { ...@@ -18,12 +18,12 @@ const gfmRules = {
}, },
}, },
TaskListFilter: { TaskListFilter: {
'input[type=checkbox].task-list-item-checkbox'(el, text) { 'input[type=checkbox].task-list-item-checkbox'(el) {
return `[${el.checked ? 'x' : ' '}]`; return `[${el.checked ? 'x' : ' '}]`;
}, },
}, },
ReferenceFilter: { ReferenceFilter: {
'.tooltip'(el, text) { '.tooltip'(el) {
return ''; return '';
}, },
'a.gfm:not([data-link=true])'(el, text) { 'a.gfm:not([data-link=true])'(el, text) {
...@@ -39,15 +39,15 @@ const gfmRules = { ...@@ -39,15 +39,15 @@ const gfmRules = {
}, },
}, },
TableOfContentsFilter: { TableOfContentsFilter: {
'ul.section-nav'(el, text) { 'ul.section-nav'(el) {
return '[[_TOC_]]'; return '[[_TOC_]]';
}, },
}, },
EmojiFilter: { EmojiFilter: {
'img.emoji'(el, text) { 'img.emoji'(el) {
return el.getAttribute('alt'); return el.getAttribute('alt');
}, },
'gl-emoji'(el, text) { 'gl-emoji'(el) {
return `:${el.getAttribute('data-name')}:`; return `:${el.getAttribute('data-name')}:`;
}, },
}, },
...@@ -57,13 +57,13 @@ const gfmRules = { ...@@ -57,13 +57,13 @@ const gfmRules = {
}, },
}, },
VideoLinkFilter: { VideoLinkFilter: {
'.video-container'(el, text) { '.video-container'(el) {
const videoEl = el.querySelector('video'); const videoEl = el.querySelector('video');
if (!videoEl) return false; if (!videoEl) return false;
return CopyAsGFM.nodeToGFM(videoEl); return CopyAsGFM.nodeToGFM(videoEl);
}, },
'video'(el, text) { 'video'(el) {
return `![${el.dataset.title}](${el.getAttribute('src')})`; return `![${el.dataset.title}](${el.getAttribute('src')})`;
}, },
}, },
...@@ -74,19 +74,19 @@ const gfmRules = { ...@@ -74,19 +74,19 @@ const gfmRules = {
'code.code.math[data-math-style=inline]'(el, text) { 'code.code.math[data-math-style=inline]'(el, text) {
return `$\`${text}\`$`; return `$\`${text}\`$`;
}, },
'span.katex-display span.katex-mathml'(el, text) { 'span.katex-display span.katex-mathml'(el) {
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
if (!mathAnnotation) return false; if (!mathAnnotation) return false;
return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``; return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
}, },
'span.katex-mathml'(el, text) { 'span.katex-mathml'(el) {
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
if (!mathAnnotation) return false; if (!mathAnnotation) return false;
return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`; return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
}, },
'span.katex-html'(el, text) { 'span.katex-html'(el) {
// We don't want to include the content of this element in the copied text. // We don't want to include the content of this element in the copied text.
return ''; return '';
}, },
...@@ -95,7 +95,7 @@ const gfmRules = { ...@@ -95,7 +95,7 @@ const gfmRules = {
}, },
}, },
SanitizationFilter: { SanitizationFilter: {
'a[name]:not([href]):empty'(el, text) { 'a[name]:not([href]):empty'(el) {
return el.outerHTML; return el.outerHTML;
}, },
'dl'(el, text) { 'dl'(el, text) {
...@@ -143,7 +143,7 @@ const gfmRules = { ...@@ -143,7 +143,7 @@ const gfmRules = {
}, },
}, },
MarkdownFilter: { MarkdownFilter: {
'br'(el, text) { 'br'(el) {
// Two spaces at the end of a line are turned into a BR // Two spaces at the end of a line are turned into a BR
return ' '; return ' ';
}, },
...@@ -162,7 +162,7 @@ const gfmRules = { ...@@ -162,7 +162,7 @@ const gfmRules = {
'blockquote'(el, text) { 'blockquote'(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
}, },
'img'(el, text) { 'img'(el) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
}, },
'a.anchor'(el, text) { 'a.anchor'(el, text) {
...@@ -222,10 +222,10 @@ const gfmRules = { ...@@ -222,10 +222,10 @@ const gfmRules = {
'sup'(el, text) { 'sup'(el, text) {
return `^${text}`; return `^${text}`;
}, },
'hr'(el, text) { 'hr'(el) {
return '-----'; return '-----';
}, },
'table'(el, text) { 'table'(el) {
const theadEl = el.querySelector('thead'); const theadEl = el.querySelector('thead');
const tbodyEl = el.querySelector('tbody'); const tbodyEl = el.querySelector('tbody');
if (!theadEl || !tbodyEl) return false; if (!theadEl || !tbodyEl) return false;
...@@ -233,11 +233,11 @@ const gfmRules = { ...@@ -233,11 +233,11 @@ const gfmRules = {
const theadText = CopyAsGFM.nodeToGFM(theadEl); const theadText = CopyAsGFM.nodeToGFM(theadEl);
const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
return theadText + tbodyText; return [theadText, tbodyText].join('\n');
}, },
'thead'(el, text) { 'thead'(el, text) {
const cells = _.map(el.querySelectorAll('th'), (cell) => { const cells = _.map(el.querySelectorAll('th'), (cell) => {
let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2; let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
let before = ''; let before = '';
let after = ''; let after = '';
...@@ -262,10 +262,15 @@ const gfmRules = { ...@@ -262,10 +262,15 @@ const gfmRules = {
return before + middle + after; return before + middle + after;
}); });
return `${text}|${cells.join('|')}|`; const separatorRow = `|${cells.join('|')}|`;
return [text, separatorRow].join('\n');
}, },
'tr'(el, text) { 'tr'(el) {
const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim()); const cellEls = el.querySelectorAll('td, th');
if (cellEls.length === 0) return false;
const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
return `| ${cells.join(' | ')} |`; return `| ${cells.join(' | ')} |`;
}, },
}, },
...@@ -273,12 +278,12 @@ const gfmRules = { ...@@ -273,12 +278,12 @@ const gfmRules = {
class CopyAsGFM { class CopyAsGFM {
constructor() { constructor() {
$(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
$(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this)); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
} }
copyAsGFM(e, transformer) { static copyAsGFM(e, transformer) {
const clipboardData = e.originalEvent.clipboardData; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
...@@ -292,27 +297,60 @@ class CopyAsGFM { ...@@ -292,27 +297,60 @@ class CopyAsGFM {
e.stopPropagation(); e.stopPropagation();
clipboardData.setData('text/plain', el.textContent); clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el)); clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
} }
pasteGFM(e) { static pasteGFM(e) {
const clipboardData = e.originalEvent.clipboardData; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
const gfm = clipboardData.getData('text/x-gfm'); const gfm = clipboardData.getData('text/x-gfm');
if (!gfm) return; if (!gfm) return;
e.preventDefault(); e.preventDefault();
window.gl.utils.insertText(e.target, gfm); window.gl.utils.insertText(e.target, (textBefore, textAfter) => {
// If the text before the cursor contains an odd number of backticks,
// we are either inside an inline code span that starts with 1 backtick
// or a code block that starts with 3 backticks.
// This logic still holds when there are one or more _closed_ code spans
// or blocks that will have 2 or 6 backticks.
// 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;
if (insideCodeBlock) {
return text;
} }
static transformGFMSelection(documentFragment) { return gfm;
// If the documentFragment contains more than just Markdown, don't copy as GFM. });
if (documentFragment.querySelector('.md, .wiki')) return null; }
static transformGFMSelection(documentFragment) {
const gfmEls = documentFragment.querySelectorAll('.md, .wiki');
switch (gfmEls.length) {
case 0: {
return documentFragment; return documentFragment;
} }
case 1: {
return gfmEls[0];
}
default: {
const allGfmEl = document.createElement('div');
for (let i = 0; i < gfmEls.length; i += 1) {
const lineEl = gfmEls[i];
allGfmEl.appendChild(lineEl);
allGfmEl.appendChild(document.createTextNode('\n\n'));
}
return allGfmEl;
}
}
}
static transformCodeSelection(documentFragment) { static transformCodeSelection(documentFragment) {
const lineEls = documentFragment.querySelectorAll('.line'); const lineEls = documentFragment.querySelectorAll('.line');
...@@ -343,7 +381,7 @@ class CopyAsGFM { ...@@ -343,7 +381,7 @@ class CopyAsGFM {
return codeEl; return codeEl;
} }
static nodeToGFM(node) { static nodeToGFM(node, respectWhitespaceParam = false) {
if (node.nodeType === Node.COMMENT_NODE) { if (node.nodeType === Node.COMMENT_NODE) {
return ''; return '';
} }
...@@ -352,7 +390,9 @@ class CopyAsGFM { ...@@ -352,7 +390,9 @@ class CopyAsGFM {
return node.textContent; return node.textContent;
} }
const text = this.innerGFM(node); const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
const text = this.innerGFM(node, respectWhitespace);
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
return text; return text;
...@@ -366,7 +406,17 @@ class CopyAsGFM { ...@@ -366,7 +406,17 @@ class CopyAsGFM {
if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
const result = func(node, text); let result;
if (func.length === 2) {
// if `func` takes 2 arguments, it depends on text.
// if there is no text, we don't need to generate GFM for this node.
if (text.length === 0) continue;
result = func(node, text);
} else {
result = func(node);
}
if (result === false) continue; if (result === false) continue;
return result; return result;
...@@ -376,7 +426,7 @@ class CopyAsGFM { ...@@ -376,7 +426,7 @@ class CopyAsGFM {
return text; return text;
} }
static innerGFM(parentNode) { static innerGFM(parentNode, respectWhitespace = false) {
const nodes = parentNode.childNodes; const nodes = parentNode.childNodes;
const clonedParentNode = parentNode.cloneNode(true); const clonedParentNode = parentNode.cloneNode(true);
...@@ -386,13 +436,19 @@ class CopyAsGFM { ...@@ -386,13 +436,19 @@ class CopyAsGFM {
const node = nodes[i]; const node = nodes[i];
const clonedNode = clonedNodes[i]; const clonedNode = clonedNodes[i];
const text = this.nodeToGFM(node); const text = this.nodeToGFM(node, respectWhitespace);
// `clonedNode.replaceWith(text)` is not yet widely supported // `clonedNode.replaceWith(text)` is not yet widely supported
clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
} }
return clonedParentNode.innerText || clonedParentNode.textContent; let nodeText = clonedParentNode.innerText || clonedParentNode.textContent;
if (!respectWhitespace) {
nodeText = nodeText.trim();
}
return nodeText;
} }
} }
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
/* global Api */ import Api from './api';
class CreateLabelDropdown { class CreateLabelDropdown {
constructor ($el, namespacePath, projectPath) { constructor ($el, namespacePath, projectPath) {
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ ...@@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ ...@@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item"> <li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
...@@ -28,11 +33,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ ...@@ -28,11 +33,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot; &middot;
<span> <span>
{{ __('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span> </span>
<span> <span>
{{ __('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span> </span>
</div> </div>
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ ...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ ...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item"> <li v-for="issue in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title"> <h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url"> <a class="issue-title" :href="issue.url">
{{ issue.title }} {{ issue.title }}
...@@ -28,11 +32,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ ...@@ -28,11 +32,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot; &middot;
<span> <span>
{{ __('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span> </span>
<span> <span>
{{ __('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link"> <a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }} {{ issue.author.name }}
</a> </a>
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg'; import iconCommit from '../svg/icon_commit.svg';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
...@@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ ...@@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
data() { data() {
return { iconCommit }; return { iconCommit };
}, },
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -24,17 +26,18 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ ...@@ -24,17 +26,18 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item"> <li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component"> <div class="item-details item-conmmit-component">
<img class="avatar" :src="commit.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title"> <h5 class="item-title commit-title">
<a :href="commit.commitUrl"> <a :href="commit.commitUrl">
{{ commit.title }} {{ commit.title }}
</a> </a>
</h5> </h5>
<span> <span>
{{ __('FirstPushedBy|First') }} {{ s__('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span> <span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ __('FirstPushedBy|pushed by') }} {{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link"> <a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }} {{ commit.author.name }}
</a> </a>
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ ...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ ...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item"> <li v-for="issue in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title"> <h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url"> <a class="issue-title" :href="issue.url">
{{ issue.title }} {{ issue.title }}
...@@ -28,11 +32,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ ...@@ -28,11 +32,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot; &middot;
<span> <span>
{{ __('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span> </span>
<span> <span>
{{ __('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link"> <a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }} {{ issue.author.name }}
</a> </a>
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ ...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ ...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item"> <li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
...@@ -28,11 +32,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ ...@@ -28,11 +32,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot; &middot;
<span> <span>
{{ __('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span> </span>
<span> <span>
{{ __('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span> </span>
<template v-if="mergeRequest.state === 'closed'"> <template v-if="mergeRequest.state === 'closed'">
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg'; import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
...@@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ ...@@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
data() { data() {
return { iconBranch }; return { iconBranch };
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -22,7 +26,8 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ ...@@ -22,7 +26,8 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component"> <li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="build.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title"> <h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i> <i class="fa fa-code-fork"></i>
...@@ -32,7 +37,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ ...@@ -32,7 +37,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
</h5> </h5>
<span> <span>
<a :href="build.url" class="build-date">{{ build.date }}</a> <a :href="build.url" class="build-date">{{ build.date }}</a>
{{ __('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link"> <a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }} {{ build.author.name }}
</a> </a>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Vue from 'vue'; import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg'; import collapseIcon from '../icons/collapse_icon.svg';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({ const DiffNoteAvatars = Vue.extend({
props: ['discussionId'], props: ['discussionId'],
...@@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({
collapseIcon, collapseIcon,
}; };
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div class="diff-comment-avatar-holders" <div class="diff-comment-avatar-holders"
v-show="notesCount !== 0"> v-show="notesCount !== 0">
<div v-if="!isVisible"> <div v-if="!isVisible">
<img v-for="note in notesSubset" <!-- FIXME: Pass an alt attribute here for accessibility -->
class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" <user-avatar-image
width="19" v-for="note in notesSubset"
height="19" class="diff-comment-avatar js-diff-comment-avatar"
role="button" @click.native="clickedAvatar($event)"
data-container="body" :img-src="note.authorAvatar"
data-placement="top" :tooltip-text="getTooltipText(note)"
data-html="true"
:data-line-type="lineType" :data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated" :size="19"
:src="note.authorAvatar" data-html="true"
@click="clickedAvatar($event)" /> />
<span v-if="notesCount > shownAvatars" <span v-if="notesCount > shownAvatars"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar" class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
data-container="body" data-container="body"
...@@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({
setDiscussionVisible() { setDiscussionVisible() {
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
}, },
getTooltipText(note) {
return `${note.authorName}: ${note.noteTruncated}`;
},
}, },
}); });
......
...@@ -42,6 +42,7 @@ import Group from './group'; ...@@ -42,6 +42,7 @@ import Group from './group';
import GroupName from './group_name'; import GroupName from './group_name';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
import setupProjectEdit from './project_edit';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import Landing from './landing'; import Landing from './landing';
...@@ -275,9 +276,14 @@ import ApproversSelect from './approvers_select'; ...@@ -275,9 +276,14 @@ import ApproversSelect from './approvers_select';
new NotificationsForm(); new NotificationsForm();
if ($('#tree-slider').length) { if ($('#tree-slider').length) {
new TreeView(); new TreeView();
}
if ($('.blob-viewer').length) {
new BlobViewer(); new BlobViewer();
} }
break; break;
case 'projects:edit':
setupProjectEdit();
break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:failures': case 'projects:pipelines:failures':
case 'projects:pipelines:show': case 'projects:pipelines:show':
......
/* eslint-disable */ /* eslint-disable */
import AjaxCache from '~/lib/utils/ajax_cache';
const Ajax = { const Ajax = {
_loadUrlData: function _loadUrlData(url) {
var self = this;
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
self.cache[url] = data;
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
_loadData: function _loadData(data, config, self) { _loadData: function _loadData(data, config, self) {
if (config.loadingTemplate) { if (config.loadingTemplate) {
var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
...@@ -31,7 +14,6 @@ const Ajax = { ...@@ -31,7 +14,6 @@ const Ajax = {
init: function init(hook) { init: function init(hook) {
var self = this; var self = this;
self.destroyed = false; self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.Ajax; var config = hook.config.Ajax;
this.hook = hook; this.hook = hook;
if (!config || !config.endpoint || !config.method) { if (!config || !config.endpoint || !config.method) {
...@@ -48,14 +30,10 @@ const Ajax = { ...@@ -48,14 +30,10 @@ const Ajax = {
this.listTemplate = dynamicList.outerHTML; this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML; dynamicList.outerHTML = loadingTemplate.outerHTML;
} }
if (self.cache[config.endpoint]) {
self._loadData(self.cache[config.endpoint], config, self); AjaxCache.retrieve(config.endpoint)
} else { .then((data) => self._loadData(data, config, self))
this._loadUrlData(config.endpoint) .catch(config.onError);
.then(function(d) {
self._loadData(d, config, self);
}, config.onError).catch(config.onError);
}
}, },
destroy: function() { destroy: function() {
this.destroyed = true; this.destroyed = true;
......
/* eslint-disable */ /* eslint-disable */
import AjaxCache from '../../lib/utils/ajax_cache';
const AjaxFilter = { const AjaxFilter = {
init: function(hook) { init: function(hook) {
...@@ -58,50 +59,24 @@ const AjaxFilter = { ...@@ -58,50 +59,24 @@ const AjaxFilter = {
this.loading = true; this.loading = true;
var params = config.params || {}; var params = config.params || {};
params[config.searchKey] = searchValue; params[config.searchKey] = searchValue;
var self = this;
self.cache = self.cache || {};
var url = config.endpoint + this.buildParams(params); var url = config.endpoint + this.buildParams(params);
var urlCachedData = self.cache[url]; return AjaxCache.retrieve(url)
if (urlCachedData) { .then((data) => {
self._loadData(urlCachedData, config, self); this._loadData(data, config);
} else { })
this._loadUrlData(url) .catch(config.onError);
.then(function(data) {
self._loadData(data, config, self);
}, config.onError).catch(config.onError);
}
}, },
_loadUrlData: function _loadUrlData(url) { _loadData(data, config) {
var self = this; const list = this.hook.list;
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
self.cache[url] = data;
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
_loadData: function _loadData(data, config, self) {
const list = self.hook.list;
if (config.loadingTemplate && list.data === undefined || if (config.loadingTemplate && list.data === undefined ||
list.data.length === 0) { list.data.length === 0) {
const dataLoadingTemplate = list.list.querySelector('[data-loading-template]'); const dataLoadingTemplate = list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) { if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate; dataLoadingTemplate.outerHTML = this.listTemplate;
} }
} }
if (!self.destroyed) { if (!this.destroyed) {
var hookListChildren = list.list.children; var hookListChildren = list.list.children;
var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
if (onlyDynamicList && data.length === 0) { if (onlyDynamicList && data.length === 0) {
...@@ -109,7 +84,7 @@ const AjaxFilter = { ...@@ -109,7 +84,7 @@ const AjaxFilter = {
} }
list.setData.call(list, data); list.setData.call(list, data);
} }
self.notLoading(); this.notLoading();
list.currentIndex = 0; list.currentIndex = 0;
}, },
......
<script> <script>
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility'; import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
...@@ -15,6 +16,7 @@ const timeagoInstance = new Timeago(); ...@@ -15,6 +16,7 @@ const timeagoInstance = new Timeago();
export default { export default {
components: { components: {
userAvatarLink,
'commit-component': CommitComponent, 'commit-component': CommitComponent,
'actions-component': ActionsComponent, 'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent, 'external-url-component': ExternalUrlComponent,
...@@ -485,15 +487,13 @@ export default { ...@@ -485,15 +487,13 @@ export default {
<span v-if="!model.isFolder && deploymentHasUser"> <span v-if="!model.isFolder && deploymentHasUser">
by by
<a <user-avatar-link
:href="deploymentUser.web_url" class="js-deploy-user-container"
class="js-deploy-user-container"> :link-href="deploymentUser.web_url"
<img :img-src="deploymentUser.avatar_url"
class="avatar has-tooltip s20" :img-alt="userImageAltDescription"
:src="deploymentUser.avatar_url" :tooltip-text="deploymentUser.username"
:alt="userImageAltDescription" />
:title="deploymentUser.username" />
</a>
</span> </span>
</td> </td>
......
...@@ -454,7 +454,6 @@ class FilteredSearchManager { ...@@ -454,7 +454,6 @@ class FilteredSearchManager {
const { tokens, searchToken } const { tokens, searchToken }
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
const currentState = gl.utils.getParameterByName('state') || 'opened'; const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`); paths.push(`state=${currentState}`);
......
...@@ -7,9 +7,10 @@ import GfmAutoComplete from './gfm_auto_complete'; ...@@ -7,9 +7,10 @@ import GfmAutoComplete from './gfm_auto_complete';
window.gl = window.gl || {}; window.gl = window.gl || {};
function GLForm(form) { function GLForm(form, enableGFM = false) {
this.form = form; this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input'); this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = enableGFM;
// Before we start, we should clean up any previous data for this form // Before we start, we should clean up any previous data for this form
this.destroy(); this.destroy();
// Setup the form // Setup the form
...@@ -32,8 +33,14 @@ GLForm.prototype.setupForm = function() { ...@@ -32,8 +33,14 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form'); this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes // remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), {
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input')); emojis: true,
members: this.enableGFM,
issues: this.enableGFM,
milestones: this.enableGFM,
mergeRequests: this.enableGFM,
labels: this.enableGFM,
});
new DropzoneInput(this.form); new DropzoneInput(this.form);
autosize(this.textarea); autosize(this.textarea);
} }
......
...@@ -44,18 +44,18 @@ export default class GroupName { ...@@ -44,18 +44,18 @@ export default class GroupName {
showToggle() { showToggle() {
this.title.classList.add('wrap'); this.title.classList.add('wrap');
this.toggle.classList.remove('hidden'); this.toggle.classList.remove('hidden');
if (this.isHidden) this.groupTitle.classList.add('is-hidden'); if (this.isHidden) this.groupTitle.classList.add('hidden');
} }
hideToggle() { hideToggle() {
this.title.classList.remove('wrap'); this.title.classList.remove('wrap');
this.toggle.classList.add('hidden'); this.toggle.classList.add('hidden');
if (this.isHidden) this.groupTitle.classList.remove('is-hidden'); if (this.isHidden) this.groupTitle.classList.remove('hidden');
} }
toggleGroups() { toggleGroups() {
this.isHidden = !this.isHidden; this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden'); this.groupTitle.classList.toggle('hidden');
} }
render() { render() {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */ promise/catch-or-return */
/* global Api */ import Api from './api';
var slice = [].slice; var slice = [].slice;
......
/* eslint-disable space-before-function-paren, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, object-shorthand, quotes, comma-dangle, consistent-return, no-unused-vars, padded-blocks, func-names, max-len */ /* eslint-disable space-before-function-paren, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, object-shorthand, quotes, comma-dangle, consistent-return, no-unused-vars, padded-blocks, func-names, max-len */
/* global Api */ import Api from './api';
(function() { (function() {
$(function() { $(function() {
......
class AjaxCache { import Cache from './cache';
class AjaxCache extends Cache {
constructor() { constructor() {
this.internalStorage = { }; super();
this.pendingRequests = { }; this.pendingRequests = { };
} }
get(endpoint) {
return this.internalStorage[endpoint];
}
hasData(endpoint) {
return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint);
}
remove(endpoint) {
delete this.internalStorage[endpoint];
}
retrieve(endpoint) { retrieve(endpoint) {
if (this.hasData(endpoint)) { if (this.hasData(endpoint)) {
return Promise.resolve(this.get(endpoint)); return Promise.resolve(this.get(endpoint));
......
class Cache {
constructor() {
this.internalStorage = { };
}
get(key) {
return this.internalStorage[key];
}
hasData(key) {
return Object.prototype.hasOwnProperty.call(this.internalStorage, key);
}
remove(key) {
delete this.internalStorage[key];
}
}
export default Cache;
...@@ -198,10 +198,12 @@ ...@@ -198,10 +198,12 @@
const textBefore = value.substring(0, selectionStart); const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length); const textAfter = value.substring(selectionEnd, value.length);
const newText = textBefore + text + textAfter;
const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
const newText = textBefore + insertedText + textAfter;
target.value = newText; target.value = newText;
target.selectionStart = target.selectionEnd = selectionStart + text.length; target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave // Trigger autosave
$(target).trigger('input'); $(target).trigger('input');
......
import Api from '../../api';
import Cache from './cache';
class UsersCache extends Cache {
retrieve(username) {
if (this.hasData(username)) {
return Promise.resolve(this.get(username));
}
return Api.users('', { username })
.then((users) => {
if (!users.length) {
throw new Error(`User "${username}" could not be found!`);
}
if (users.length > 1) {
throw new Error(`Expected username "${username}" to be unique!`);
}
const user = users[0];
this.internalStorage[username] = user;
return user;
});
// missing catch is intentional, error handling depends on use case
}
}
export default new UsersCache();
This diff is collapsed.
...@@ -288,7 +288,11 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -288,7 +288,11 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
if (anchor) { if (anchor) {
const notesContent = anchor.closest('.notes_content'); const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old'; const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.addDiffNote(anchor, lineType, false); notes.toggleDiffNote({
target: anchor,
lineType,
forceShow: true,
});
anchor[0].scrollIntoView(); anchor[0].scrollIntoView();
// We have multiple elements on the page with `#note_xxx` // We have multiple elements on the page with `#note_xxx`
// (discussion and diff tabs) and `:target` only applies to the first // (discussion and diff tabs) and `:target` only applies to the first
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */
/* global Api */ import Api from './api';
(function() { (function() {
window.NamespaceSelect = (function() { window.NamespaceSelect = (function() {
......
...@@ -29,7 +29,7 @@ const normalizeNewlines = function(str) { ...@@ -29,7 +29,7 @@ const normalizeNewlines = function(str) {
Notes.interval = null; Notes.interval = null;
function Notes(notes_url, note_ids, last_fetched_at, view) { function Notes(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
this.updateTargetButtons = this.updateTargetButtons.bind(this); this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this); this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this); this.visibilityChange = this.visibilityChange.bind(this);
...@@ -52,6 +52,7 @@ const normalizeNewlines = function(str) { ...@@ -52,6 +52,7 @@ const normalizeNewlines = function(str) {
this.notes_url = notes_url; this.notes_url = notes_url;
this.note_ids = note_ids; this.note_ids = note_ids;
this.enableGFM = enableGFM;
// Used to keep track of updated notes while people are editing things // Used to keep track of updated notes while people are editing things
this.updatedNotesTrackingMap = {}; this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at; this.last_fetched_at = last_fetched_at;
...@@ -288,6 +289,13 @@ const normalizeNewlines = function(str) { ...@@ -288,6 +289,13 @@ const normalizeNewlines = function(str) {
} }
}; };
Notes.prototype.setupNewNote = function($note) {
// Update datetime format on the recent note
gl.utils.localTimeAgo($note.find('.js-timeago'), false);
this.collapseLongCommitList();
this.taskList.init();
};
/* /*
Render note in main comments area. Render note in main comments area.
...@@ -308,20 +316,17 @@ const normalizeNewlines = function(str) { ...@@ -308,20 +316,17 @@ const normalizeNewlines = function(str) {
} }
const $note = $notesList.find(`#note_${noteEntity.id}`); const $note = $notesList.find(`#note_${noteEntity.id}`);
if (this.isNewNote(noteEntity)) { if (Notes.isNewNote(noteEntity, this.note_ids)) {
this.note_ids.push(noteEntity.id); this.note_ids.push(noteEntity.id);
const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
// Update datetime format on the recent note this.setupNewNote($newNote);
gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
this.collapseLongCommitList();
this.taskList.init();
this.refresh(); this.refresh();
return this.updateNotesCount(1); return this.updateNotesCount(1);
} }
// The server can send the same update multiple times so we need to make sure to only update once per actual update. // The server can send the same update multiple times so we need to make sure to only update once per actual update.
else if (this.isUpdatedNote(noteEntity, $note)) { else if (Notes.isUpdatedNote(noteEntity, $note)) {
const isEditing = $note.hasClass('is-editing'); const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines( const initialContent = normalizeNewlines(
$note.find('.original-note-content').text().trim() $note.find('.original-note-content').text().trim()
...@@ -342,30 +347,11 @@ const normalizeNewlines = function(str) { ...@@ -342,30 +347,11 @@ const normalizeNewlines = function(str) {
} }
else { else {
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
this.setupNewNote($updatedNote);
// Update datetime format on the recent note
gl.utils.localTimeAgo($updatedNote.find('.js-timeago'), false);
} }
} }
}; };
/*
Check if note does not exists on page
*/
Notes.prototype.isNewNote = function(noteEntity) {
return $.inArray(noteEntity.id, this.note_ids) === -1;
};
Notes.prototype.isUpdatedNote = function(noteEntity, $note) {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
const currentNoteText = normalizeNewlines(
$note.find('.original-note-content').text().trim()
);
return sanitizedNoteNote !== currentNoteText;
};
Notes.prototype.isParallelView = function() { Notes.prototype.isParallelView = function() {
return Cookies.get('diff_view') === 'parallel'; return Cookies.get('diff_view') === 'parallel';
}; };
...@@ -378,7 +364,7 @@ const normalizeNewlines = function(str) { ...@@ -378,7 +364,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.renderDiscussionNote = function(noteEntity, $form) { Notes.prototype.renderDiscussionNote = function(noteEntity, $form) {
var discussionContainer, form, row, lineType, diffAvatarContainer; var discussionContainer, form, row, lineType, diffAvatarContainer;
if (!this.isNewNote(noteEntity)) { if (!Notes.isNewNote(noteEntity, this.note_ids)) {
return; return;
} }
this.note_ids.push(noteEntity.id); this.note_ids.push(noteEntity.id);
...@@ -525,7 +511,7 @@ const normalizeNewlines = function(str) { ...@@ -525,7 +511,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupNoteForm = function(form) { Notes.prototype.setupNoteForm = function(form) {
var textarea, key; var textarea, key;
new gl.GLForm(form); new gl.GLForm(form, this.enableGFM);
textarea = form.find(".js-note-text"); textarea = form.find(".js-note-text");
key = [ key = [
"Note", "Note",
...@@ -596,12 +582,12 @@ const normalizeNewlines = function(str) { ...@@ -596,12 +582,12 @@ const normalizeNewlines = function(str) {
Updates the current note field. Updates the current note field.
*/ */
Notes.prototype.updateNote = function(_xhr, noteEntity, _status) { Notes.prototype.updateNote = function(noteEntity, $targetNote) {
var $noteEntityEl, $note_li; var $noteEntityEl, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further // Convert returned HTML to a jQuery object so we can modify it further
$noteEntityEl = $(noteEntity.html); $noteEntityEl = $(noteEntity.html);
$noteEntityEl.addClass('fade-in-full'); $noteEntityEl.addClass('fade-in-full');
this.revertNoteEditForm(); this.revertNoteEditForm($targetNote);
gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl)); gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl));
$noteEntityEl.renderGFM(); $noteEntityEl.renderGFM();
$noteEntityEl.find('.js-task-list-container').taskList('enable'); $noteEntityEl.find('.js-task-list-container').taskList('enable');
...@@ -683,10 +669,8 @@ const normalizeNewlines = function(str) { ...@@ -683,10 +669,8 @@ const normalizeNewlines = function(str) {
if (this.updatedNotesTrackingMap[noteId]) { if (this.updatedNotesTrackingMap[noteId]) {
const $newNote = $(this.updatedNotesTrackingMap[noteId].html); const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
$note.replaceWith($newNote); $note.replaceWith($newNote);
this.setupNewNote($newNote);
this.updatedNotesTrackingMap[noteId] = null; this.updatedNotesTrackingMap[noteId] = null;
// Update datetime format on the recent note
gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
} }
else { else {
$note.find('.js-finish-edit-warning').hide(); $note.find('.js-finish-edit-warning').hide();
...@@ -876,12 +860,22 @@ const normalizeNewlines = function(str) { ...@@ -876,12 +860,22 @@ const normalizeNewlines = function(str) {
Notes.prototype.onAddDiffNote = function(e) { Notes.prototype.onAddDiffNote = function(e) {
e.preventDefault(); e.preventDefault();
const $link = $(e.currentTarget || e.target); const link = e.currentTarget || e.target;
const $link = $(link);
const showReplyInput = !$link.hasClass('js-diff-comment-avatar'); const showReplyInput = !$link.hasClass('js-diff-comment-avatar');
this.addDiffNote($link, $link.data('lineType'), showReplyInput); this.toggleDiffNote({
target: $link,
lineType: link.dataset.lineType,
showReplyInput
});
}; };
Notes.prototype.addDiffNote = function(target, lineType, showReplyInput) { Notes.prototype.toggleDiffNote = function({
target,
lineType,
forceShow,
showReplyInput = false,
}) {
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
$link = $(target); $link = $(target);
row = $link.closest("tr"); row = $link.closest("tr");
...@@ -926,12 +920,12 @@ const normalizeNewlines = function(str) { ...@@ -926,12 +920,12 @@ const normalizeNewlines = function(str) {
notesContent = targetRow.find(notesContentSelector); notesContent = targetRow.find(notesContentSelector);
addForm = true; addForm = true;
} else { } else {
targetRow.show(); const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
notesContent.toggle(!notesContent.is(':visible')); const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
if (!targetRow.find('.content:not(:empty)').is(':visible')) { targetRow.toggle(showNow);
targetRow.hide(); notesContent.toggle(showNow);
}
} }
if (addForm) { if (addForm) {
...@@ -1139,6 +1133,25 @@ const normalizeNewlines = function(str) { ...@@ -1139,6 +1133,25 @@ const normalizeNewlines = function(str) {
return $form; return $form;
}; };
/**
* Check if note does not exists on page
*/
Notes.isNewNote = function(noteEntity, noteIds) {
return $.inArray(noteEntity.id, noteIds) === -1;
};
/**
* Check if $note already contains the `noteEntity` content
*/
Notes.isUpdatedNote = function(noteEntity, $note) {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim());
const currentNoteText = normalizeNewlines(
$note.find('.original-note-content').first().text().trim()
);
return sanitizedNoteEntityText !== currentNoteText;
};
Notes.checkMergeRequestStatus = function() { Notes.checkMergeRequestStatus = function() {
if (gl.utils.getPagePath(1) === 'merge_requests') { if (gl.utils.getPagePath(1) === 'merge_requests') {
gl.mrWidget.checkStatus(); gl.mrWidget.checkStatus();
...@@ -1394,7 +1407,7 @@ const normalizeNewlines = function(str) { ...@@ -1394,7 +1407,7 @@ const normalizeNewlines = function(str) {
gl.utils.ajaxPost(formAction, formData) gl.utils.ajaxPost(formAction, formData)
.then((note) => { .then((note) => {
// Submission successful! render final note element // Submission successful! render final note element
this.updateNote(null, note, null); this.updateNote(note, $editingNote);
}) })
.fail(() => { .fail(() => {
// Submission failed, revert back to original note // Submission failed, revert back to original note
......
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
export default { export default {
props: [ props: [
'pipeline', 'pipeline',
...@@ -7,6 +9,9 @@ export default { ...@@ -7,6 +9,9 @@ export default {
return !!this.pipeline.user; return !!this.pipeline.user;
}, },
}, },
components: {
userAvatarLink,
},
template: ` template: `
<td> <td>
<a <a
...@@ -15,18 +20,13 @@ export default { ...@@ -15,18 +20,13 @@ export default {
<span class="pipeline-id">#{{pipeline.id}}</span> <span class="pipeline-id">#{{pipeline.id}}</span>
</a> </a>
<span>by</span> <span>by</span>
<a <user-avatar-link
class="js-pipeline-url-user"
v-if="user" v-if="user"
:href="pipeline.user.web_url"> class="js-pipeline-url-user"
<img :link-href="pipeline.user.web_url"
v-if="user" :img-src="pipeline.user.avatar_url"
class="avatar has-tooltip s20 " :tooltip-text="pipeline.user.name"
:title="pipeline.user.name" />
data-container="body"
:src="pipeline.user.avatar_url"
>
</a>
<span <span
v-if="!user" v-if="!user"
class="js-pipeline-url-api api"> class="js-pipeline-url-api api">
......
export default function setupProjectEdit() {
const $transferForm = $('.js-project-transfer-form');
const $selectNamespace = $transferForm.find('.select2');
$selectNamespace.on('change', () => {
$transferForm.find(':submit').prop('disabled', !$selectNamespace.val());
});
$selectNamespace.trigger('change');
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
/* global Api */ import Api from './api';
(function() { (function() {
this.ProjectSelect = (function() { this.ProjectSelect = (function() {
......
import Raven from 'raven-js'; import Raven from 'raven-js';
import $ from 'jquery';
const IGNORE_ERRORS = [ const IGNORE_ERRORS = [
// Random plugins/extensions // Random plugins/extensions
...@@ -74,7 +75,7 @@ const RavenConfig = { ...@@ -74,7 +75,7 @@ const RavenConfig = {
}, },
bindRavenErrors() { bindRavenErrors() {
window.$(document).on('ajaxError.raven', this.handleRavenErrors); $(document).on('ajaxError.raven', this.handleRavenErrors);
}, },
handleRavenErrors(event, req, config, err) { handleRavenErrors(event, req, config, err) {
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
/* global Flash */ /* global Flash */
/* global Api */ import Api from './api';
(function() { (function() {
this.Search = (function() { this.Search = (function() {
......
...@@ -38,7 +38,7 @@ import './shortcuts_navigation'; ...@@ -38,7 +38,7 @@ import './shortcuts_navigation';
} }
ShortcutsIssuable.prototype.replyWithSelectedText = function() { ShortcutsIssuable.prototype.replyWithSelectedText = function() {
var quote, documentFragment, selected, separator; var quote, documentFragment, el, selected, separator;
var replyField = $('.js-main-target-form #note_note'); var replyField = $('.js-main-target-form #note_note');
documentFragment = window.gl.utils.getSelectedFragment(); documentFragment = window.gl.utils.getSelectedFragment();
...@@ -47,10 +47,8 @@ import './shortcuts_navigation'; ...@@ -47,10 +47,8 @@ import './shortcuts_navigation';
return; return;
} }
// If the documentFragment contains more than just Markdown, don't copy as GFM. el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
if (documentFragment.querySelector('.md, .wiki')) return; selected = window.gl.CopyAsGFM.nodeToGFM(el);
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
if (selected.trim() === "") { if (selected.trim() === "") {
return; return;
......
...@@ -67,10 +67,11 @@ export default { ...@@ -67,10 +67,11 @@ export default {
<div> <div>
<assignee-title <assignee-title
:number-of-assignees="store.assignees.length" :number-of-assignees="store.assignees.length"
:loading="loading" :loading="loading || store.isFetching.assignees"
:editable="store.editable" :editable="store.editable"
/> />
<assignees <assignees
v-if="!store.isFetching.assignees"
class="value" class="value"
:root-path="store.rootPath" :root-path="store.rootPath"
:users="store.assignees" :users="store.assignees"
......
...@@ -10,6 +10,9 @@ export default class SidebarStore { ...@@ -10,6 +10,9 @@ export default class SidebarStore {
this.humanTimeEstimate = ''; this.humanTimeEstimate = '';
this.humanTimeSpent = ''; this.humanTimeSpent = '';
this.assignees = []; this.assignees = [];
this.isFetching = {
assignees: true,
};
SidebarStore.singleton = this; SidebarStore.singleton = this;
} }
...@@ -18,6 +21,7 @@ export default class SidebarStore { ...@@ -18,6 +21,7 @@ export default class SidebarStore {
} }
setAssigneeData(data) { setAssigneeData(data) {
this.isFetching.assignees = false;
if (data.assignees) { if (data.assignees) {
this.assignees = data.assignees; this.assignees = data.assignees;
} }
......
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */ import Api from '../api';
import TemplateSelector from '../blob/template_selector'; import TemplateSelector from '../blob/template_selector';
......
...@@ -421,6 +421,15 @@ function UsersSelect(currentUser, els) { ...@@ -421,6 +421,15 @@ function UsersSelect(currentUser, els) {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
return assignTo(selected); return assignTo(selected);
} }
// Automatically close dropdown after assignee is selected
// since CE has no multiple assignees
// EE does not have a max-select
if ($dropdown.data('max-select') &&
getSelected().length === $dropdown.data('max-select')) {
// Close the dropdown
$dropdown.dropdown('toggle');
}
}, },
id: function (user) { id: function (user) {
return user.id; return user.id;
......
...@@ -56,7 +56,7 @@ export default { ...@@ -56,7 +56,7 @@ export default {
<div class="ci-widget"> <div class="ci-widget">
<div class="ci-status-icon ci-status-icon-success"> <div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link"> <span class="js-icon-link icon-link">
<span <span class="ci-status-icon"
v-html="svg" v-html="svg"
aria-hidden="true"></span> aria-hidden="true"></span>
</span> </span>
......
...@@ -92,10 +92,7 @@ export default { ...@@ -92,10 +92,7 @@ export default {
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}" :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''" :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom"> data-placement="bottom">
<a <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
:href="mr.targetBranchPath">
{{mr.targetBranch}}
</a>
</span> </span>
</strong> </strong>
<span <span
......
import eventHub from '../../event_hub';
export default { export default {
name: 'MRWidgetAutoMergeFailed', name: 'MRWidgetAutoMergeFailed',
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
}, },
data() {
return {
isRefreshing: false,
};
},
methods: {
refreshWidget() {
this.isRefreshing = true;
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isRefreshing = false;
});
},
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body">
<button <button
...@@ -13,8 +28,19 @@ export default { ...@@ -13,8 +28,19 @@ export default {
</button> </button>
<span class="bold danger"> <span class="bold danger">
This merge request failed to be merged automatically. This merge request failed to be merged automatically.
<button
@click="refreshWidget"
:class="{ disabled: isRefreshing }"
type="button"
class="btn btn-xs btn-default">
<i
v-if="isRefreshing"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Refresh
</button>
</span> </span>
<div class="merge-error-text"> <div class="merge-error-text danger bold">
{{mr.mergeError}} {{mr.mergeError}}
</div> </div>
</div> </div>
......
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc; return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
}, },
mergeButtonClass() { mergeButtonClass() {
const defaultClass = 'btn btn-success accept-merge-request'; const defaultClass = 'btn btn-small btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`; const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`; const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
...@@ -214,7 +214,7 @@ export default { ...@@ -214,7 +214,7 @@ export default {
v-if="shouldShowMergeOptionsDropdown" v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
type="button" type="button"
class="btn btn-info dropdown-toggle" class="btn btn-small btn-info dropdown-toggle"
data-toggle="dropdown"> data-toggle="dropdown">
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
......
...@@ -11,10 +11,6 @@ export default function deviseState(data) { ...@@ -11,10 +11,6 @@ export default function deviseState(data) {
return 'conflicts'; return 'conflicts';
} else if (data.work_in_progress) { } else if (data.work_in_progress) {
return 'workInProgress'; return 'workInProgress';
} else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
} else if (!this.canMerge) {
return 'notAllowedToMerge';
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return 'pipelineFailed'; return 'pipelineFailed';
} else if (this.hasMergeableDiscussionsState) { } else if (this.hasMergeableDiscussionsState) {
...@@ -23,6 +19,10 @@ export default function deviseState(data) { ...@@ -23,6 +19,10 @@ export default function deviseState(data) {
return 'pipelineBlocked'; return 'pipelineBlocked';
} else if (this.hasSHAChanged) { } else if (this.hasSHAChanged) {
return 'shaMismatch'; return 'shaMismatch';
} else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
} else if (!this.canMerge) {
return 'notAllowedToMerge';
} else if (this.canBeMerged) { } else if (this.canBeMerged) {
return 'readyToMerge'; return 'readyToMerge';
} }
......
...@@ -4,7 +4,7 @@ import { getStateKey } from '../dependencies'; ...@@ -4,7 +4,7 @@ import { getStateKey } from '../dependencies';
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
this.startingSha = data.diff_head_sha; this.sha = data.diff_head_sha;
this.setData(data); this.setData(data);
} }
...@@ -19,7 +19,6 @@ export default class MergeRequestStore { ...@@ -19,7 +19,6 @@ export default class MergeRequestStore {
this.targetBranch = data.target_branch; this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch; this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status; this.mergeStatus = data.merge_status;
this.sha = data.diff_head_sha;
this.commitMessage = data.merge_commit_message; this.commitMessage = data.merge_commit_message;
this.commitMessageWithDescription = data.merge_commit_message_with_description; this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count; this.commitsCount = data.commits_count;
...@@ -72,7 +71,7 @@ export default class MergeRequestStore { ...@@ -72,7 +71,7 @@ export default class MergeRequestStore {
this.canMerge = !!data.merge_path; this.canMerge = !!data.merge_path;
this.canCreateIssue = currentUser.can_create_issue || false; this.canCreateIssue = currentUser.can_create_issue || false;
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== this.startingSha; this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
......
import commitIconSvg from 'icons/_icon_commit.svg'; import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default { export default {
props: { props: {
...@@ -110,6 +111,9 @@ export default { ...@@ -110,6 +111,9 @@ export default {
return { commitIconSvg }; return { commitIconSvg };
}, },
components: {
userAvatarLink,
},
template: ` template: `
<div class="branch-commit"> <div class="branch-commit">
...@@ -133,16 +137,14 @@ export default { ...@@ -133,16 +137,14 @@ export default {
<p class="commit-title"> <p class="commit-title">
<span v-if="title"> <span v-if="title">
<a v-if="hasAuthor" <user-avatar-link
v-if="hasAuthor"
class="avatar-image-container" class="avatar-image-container"
:href="author.web_url"> :link-href="author.web_url"
<img :img-src="author.avatar_url"
class="avatar has-tooltip s20" :img-alt="userImageAltDescription"
:src="author.avatar_url" :tooltip-text="author.username"
:alt="userImageAltDescription" />
:title="author.username" />
</a>
<a class="commit-row-message" <a class="commit-row-message"
:href="commitUrl"> :href="commitUrl">
{{title}} {{title}}
......
<script>
/* This is a re-usable vue component for rendering a user avatar that
does not need to link to the user's profile. The image and an optional
tooltip can be configured by props passed to this component.
Sample configuration:
<user-avatar-image
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import TooltipMixin from '../../mixins/tooltip';
export default {
name: 'UserAvatarImage',
mixins: [TooltipMixin],
props: {
imgSrc: {
type: String,
required: false,
default: defaultAvatarUrl,
},
cssClasses: {
type: String,
required: false,
default: '',
},
imgAlt: {
type: String,
required: false,
default: 'user avatar',
},
size: {
type: Number,
required: false,
default: 20,
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
},
computed: {
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
avatarSizeClass() {
return `s${this.size}`;
},
},
};
</script>
<template>
<img
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imgSrc"
:width="size"
:height="size"
:alt="imgAlt"
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
ref="tooltip"
/>
</template>
<script>
/* This is a re-usable vue component for rendering a user avatar wrapped in
a clickable link (likely to the user's profile). The link, image, and
tooltip can be configured by props passed to this component.
Sample configuration:
<user-avatar-link
:link-href="userProfileUrl"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:img-size="20"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
import userAvatarImage from './user_avatar_image.vue';
export default {
name: 'UserAvatarLink',
components: {
userAvatarImage,
},
props: {
linkHref: {
type: String,
required: false,
default: '',
},
imgSrc: {
type: String,
required: false,
default: '',
},
imgAlt: {
type: String,
required: false,
default: '',
},
imgCssClasses: {
type: String,
required: false,
default: '',
},
imgSize: {
type: Number,
required: false,
default: 20,
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
},
};
</script>
<template>
<a
class="user-avatar-link"
:href="linkHref">
<user-avatar-image
:img-src="imgSrc"
:img-alt="imgAlt"
:css-classes="imgCssClasses"
:size="imgSize"
:tooltip-text="tooltipText"
:tooltip-placement="tooltipPlacement"
/>
</a>
</template>
<script>
/* This is a re-usable vue component for rendering a user avatar svg (typically
for a blank state). It will receive styles comparable to the user avatar,
but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported.
The svg and avatar size can be configured by props passed to this component.
Sample configuration:
<user-avatar-svg
:svg="potentialApproverSvg"
:size="20"
/>
*/
export default {
props: {
svg: {
type: String,
required: true,
},
size: {
type: Number,
required: false,
default: 20,
},
},
computed: {
avatarSizeClass() {
return `s${this.size}`;
},
},
};
</script>
<template>
<svg
:class="avatarSizeClass"
:height="size"
:width="size"
v-html="svg">
</svg>
</template>
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
border-radius: $avatar_radius; border-radius: $avatar_radius;
border: 1px solid $avatar-border; border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); } &.s16 { @include avatar-size(16px, 6px); }
&.s18 { @include avatar-size(18px, 6px); }
&.s19 { @include avatar-size(19px, 6px); }
&.s20 { @include avatar-size(20px, 7px); } &.s20 { @include avatar-size(20px, 7px); }
&.s24 { @include avatar-size(24px, 8px); } &.s24 { @include avatar-size(24px, 8px); }
&.s26 { @include avatar-size(26px, 8px); } &.s26 { @include avatar-size(26px, 8px); }
......
...@@ -108,8 +108,9 @@ ...@@ -108,8 +108,9 @@
} }
.award-control { .award-control {
margin-right: 5px; margin: 0 5px 6px 0;
outline: 0; outline: 0;
position: relative;
&.disabled { &.disabled {
cursor: default; cursor: default;
...@@ -227,8 +228,8 @@ ...@@ -227,8 +228,8 @@
.award-control-icon-positive, .award-control-icon-positive,
.award-control-icon-super-positive { .award-control-icon-super-positive {
position: absolute; position: absolute;
left: 11px; left: 10px;
bottom: 7px; bottom: 6px;
opacity: 0; opacity: 0;
@include transition(opacity, transform); @include transition(opacity, transform);
} }
...@@ -237,7 +238,3 @@ ...@@ -237,7 +238,3 @@
vertical-align: middle; vertical-align: middle;
} }
} }
.note-awards .award-control-icon-positive {
left: 6px;
}
...@@ -263,7 +263,9 @@ ...@@ -263,7 +263,9 @@
} }
.filtered-search-input-dropdown-menu { .filtered-search-input-dropdown-menu {
max-height: 215px;
max-width: 280px; max-width: 280px;
overflow: auto;
@media (max-width: $screen-xs-min) { @media (max-width: $screen-xs-min) {
width: auto; width: auto;
...@@ -372,11 +374,6 @@ ...@@ -372,11 +374,6 @@
padding: 0; padding: 0;
} }
.filter-dropdown {
max-height: 215px;
overflow: auto;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.issue-bulk-update-dropdown-toggle { .issue-bulk-update-dropdown-toggle {
width: 100px; width: 100px;
......
...@@ -31,7 +31,6 @@ header { ...@@ -31,7 +31,6 @@ header {
border: none; border: none;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
position: fixed; position: fixed;
z-index: 300;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
...@@ -41,7 +40,17 @@ header { ...@@ -41,7 +40,17 @@ header {
} }
&.with-horizontal-nav { &.with-horizontal-nav {
border-color: transparent; border-bottom: 0;
.navbar-border {
height: 1px;
position: absolute;
right: 0;
left: 0;
bottom: -1px;
background-color: $border-color;
opacity: 0;
}
} }
.container-fluid { .container-fluid {
...@@ -115,16 +124,6 @@ header { ...@@ -115,16 +124,6 @@ header {
} }
} }
.navbar-border {
height: 1px;
position: absolute;
right: 0;
left: 0;
bottom: 0;
background-color: $border-color;
opacity: 0;
}
.global-dropdown { .global-dropdown {
position: absolute; position: absolute;
left: -10px; left: -10px;
......
...@@ -65,3 +65,7 @@ ...@@ -65,3 +65,7 @@
text-decoration: none; text-decoration: none;
} }
} }
.user-avatar-link {
text-decoration: none;
}
...@@ -470,8 +470,8 @@ ...@@ -470,8 +470,8 @@
} }
} }
.activities { .nav-block {
.nav-block { &.activities {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
.nav-links { .nav-links {
......
...@@ -83,4 +83,8 @@ ...@@ -83,4 +83,8 @@
position: fixed; position: fixed;
top: $header-height; top: $header-height;
} }
&:not(.affix-top) {
min-height: 100%;
}
} }
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.note-text { .note-text {
p:last-child { p:last-child {
margin-bottom: 0; margin-bottom: 0 !important;
} }
} }
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
} }
.timeline-entry { .timeline-entry {
padding: $gl-padding $gl-btn-padding 0;
border-color: $white-normal; border-color: $white-normal;
color: $gl-text-color; color: $gl-text-color;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
......
...@@ -139,6 +139,15 @@ ...@@ -139,6 +139,15 @@
line-height: 1.6em; line-height: 1.6em;
overflow-x: auto; overflow-x: auto;
border-radius: 2px; border-radius: 2px;
&.plain-readme {
background: none;
border: none;
padding: 0;
margin: 0;
font-size: 14px;
}
} }
p > code { p > code {
...@@ -169,14 +178,14 @@ ...@@ -169,14 +178,14 @@
} }
ul.task-list { ul.task-list {
li.task-list-item { > li.task-list-item {
list-style-type: none; list-style-type: none;
position: relative; position: relative;
min-height: 22px; min-height: 22px;
padding-left: 28px; padding-left: 28px;
margin-left: 0 !important; margin-left: 0 !important;
input.task-list-item-checkbox { > input.task-list-item-checkbox {
position: absolute; position: absolute;
left: 8px; left: 8px;
top: 5px; top: 5px;
...@@ -279,14 +288,6 @@ h6 { ...@@ -279,14 +288,6 @@ h6 {
/** CODE **/ /** CODE **/
pre { pre {
font-family: $monospace_font; font-family: $monospace_font;
&.plain-readme {
background: none;
border: none;
padding: 0;
margin: 0;
font-size: 14px;
}
} }
code { code {
......
...@@ -4,11 +4,7 @@ ...@@ -4,11 +4,7 @@
color: $gl-text-color; color: $gl-text-color;
line-height: 34px; line-height: 34px;
.author { a {
color: $gl-text-color;
}
.identifier {
color: $gl-text-color; color: $gl-text-color;
} }
......
...@@ -68,10 +68,6 @@ ...@@ -68,10 +68,6 @@
margin: 0; margin: 0;
} }
.avatar-image-container {
text-decoration: none;
}
.icon-play { .icon-play {
height: 13px; height: 13px;
width: 12px; width: 12px;
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
.page-content-header, .page-content-header,
.commit-box, .commit-box,
.info-well, .info-well,
.notes,
.commit-ci-menu, .commit-ci-menu,
.files-changed { .files-changed {
@extend .fixed-width-container; @extend .fixed-width-container;
...@@ -57,6 +56,10 @@ ...@@ -57,6 +56,10 @@
padding: 5px; padding: 5px;
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
} }
.emoji-block {
padding: 10px 0 4px;
}
} }
.issuable-filter-count { .issuable-filter-count {
......
...@@ -209,17 +209,30 @@ ...@@ -209,17 +209,30 @@
} }
} }
.mr-widget-heading, .mr-widget-heading {
.mr-widget-body {
.btn-default.btn-xs { .btn-default.btn-xs {
margin-left: 5px; margin-left: 5px;
} }
} }
.mr-widget-body {
.btn {
font-size: 15px;
}
.btn-group .btn {
padding: 5px 10px;
&.dropdown-toggle {
padding: 5px 7px;
}
}
}
.mr-widget-body { .mr-widget-body {
h4 { h4 {
font-weight: 600; font-weight: bold;
font-size: 16px; font-size: 15px;
margin: 5px 0; margin: 5px 0;
color: $gl-text-color; color: $gl-text-color;
...@@ -246,8 +259,8 @@ ...@@ -246,8 +259,8 @@
} }
.bold { .bold {
margin-left: 5px;
font-weight: bold; font-weight: bold;
font-size: 15px;
color: $gl-gray-light; color: $gl-gray-light;
} }
...@@ -271,6 +284,11 @@ ...@@ -271,6 +284,11 @@
margin-bottom: 24px; margin-bottom: 24px;
} }
.spacing,
.bold {
vertical-align: middle;
}
.dropdown-menu { .dropdown-menu {
li a { li a {
padding: 5px; padding: 5px;
...@@ -389,6 +407,12 @@ ...@@ -389,6 +407,12 @@
} }
} }
.mr-state-widget .mr-widget-body {
.approve-btn {
margin-right: 5px;
}
}
.mr_source_commit, .mr_source_commit,
.mr_target_commit { .mr_target_commit {
margin-bottom: 0; margin-bottom: 0;
...@@ -531,7 +555,7 @@ ...@@ -531,7 +555,7 @@
p { p {
float: left; float: left;
padding-left: 20px; padding-left: 21px;
&::before { &::before {
top: 13px; top: 13px;
......
...@@ -164,10 +164,6 @@ ...@@ -164,10 +164,6 @@
.discussion-body, .discussion-body,
.diff-file { .diff-file {
.notes .note {
padding: 10px 15px;
}
.discussion-reply-holder { .discussion-reply-holder {
background-color: $white-light; background-color: $white-light;
padding: 10px 16px; padding: 10px 16px;
......
...@@ -43,7 +43,11 @@ ul.notes { ...@@ -43,7 +43,11 @@ ul.notes {
} }
.discussion-body { .discussion-body {
padding-top: 15px; padding-top: 8px;
.panel {
margin-bottom: 0;
}
} }
.discussion { .discussion {
...@@ -53,6 +57,7 @@ ul.notes { ...@@ -53,6 +57,7 @@ ul.notes {
} }
.note { .note {
padding: $gl-padding $gl-btn-padding 0;
display: block; display: block;
position: relative; position: relative;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
...@@ -78,11 +83,7 @@ ul.notes { ...@@ -78,11 +83,7 @@ ul.notes {
&.note-discussion { &.note-discussion {
&.timeline-entry { &.timeline-entry {
padding: 14px 10px; padding: $gl-padding 10px;
}
.system-note {
padding: 0;
} }
} }
...@@ -167,7 +168,7 @@ ul.notes { ...@@ -167,7 +168,7 @@ ul.notes {
margin-left: 65px; margin-left: 65px;
} }
.note-header { .note-header-info {
padding-bottom: 0; padding-bottom: 0;
} }
...@@ -377,7 +378,11 @@ ul.notes { ...@@ -377,7 +378,11 @@ ul.notes {
.note-header-info { .note-header-info {
min-width: 0; min-width: 0;
padding-bottom: 5px; padding-bottom: 8px;
}
.system-note .note-header-info {
padding-bottom: 0;
} }
.note-headline-light { .note-headline-light {
...@@ -582,6 +587,17 @@ ul.notes { ...@@ -582,6 +587,17 @@ ul.notes {
} }
} }
.discussion-body,
.diff-file {
.notes .note {
padding: 10px 15px;
&.system-note {
padding: 0;
}
}
}
.diff-file { .diff-file {
.is-over { .is-over {
.add-diff-note { .add-diff-note {
...@@ -669,7 +685,7 @@ ul.notes { ...@@ -669,7 +685,7 @@ ul.notes {
.line-resolve-btn { .line-resolve-btn {
position: relative; position: relative;
top: 2px; top: 0;
padding: 0; padding: 0;
background-color: transparent; background-color: transparent;
border: none; border: none;
...@@ -690,8 +706,8 @@ ul.notes { ...@@ -690,8 +706,8 @@ ul.notes {
svg { svg {
fill: $gray-darkest; fill: $gray-darkest;
height: 15px; height: 16px;
width: 15px; width: 16px;
} }
.loading { .loading {
......
...@@ -647,58 +647,6 @@ pre.light-well { ...@@ -647,58 +647,6 @@ pre.light-well {
} }
} }
.project-last-commit {
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-base;
padding: 12px;
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
}
.ci-status {
margin-right: $gl-padding;
}
.commit-row-message {
color: $gl-text-color;
}
.commit-sha {
margin-right: 5px;
font-weight: 600;
}
.commit-author-link {
.commit-author-name {
font-weight: 600;
}
}
}
.project-show-readme {
.row-content-block {
background-color: inherit;
border: none;
}
.readme-holder {
padding: $gl-padding 0;
border-top: 0;
.edit-project-readme {
z-index: 2;
position: relative;
}
.wiki h1 {
border-bottom: none;
padding: 0;
}
}
}
.git-clone-holder { .git-clone-holder {
width: 380px; width: 380px;
......
...@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base ...@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user_from_rss_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
...@@ -72,13 +73,20 @@ class ApplicationController < ActionController::Base ...@@ -72,13 +73,20 @@ class ApplicationController < ActionController::Base
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
if user && can?(user, :log_in) sessionless_sign_in(user)
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
# sign in token, you can simply remove store: false.
sign_in user, store: false
end end
# This filter handles authentication for atom request with an rss_token
def authenticate_user_from_rss_token!
return unless request.format.atom?
token = params[:rss_token].presence
return unless token.present?
user = User.find_by_rss_token(token)
sessionless_sign_in(user)
end end
def log_exception(exception) def log_exception(exception)
...@@ -286,4 +294,14 @@ class ApplicationController < ActionController::Base ...@@ -286,4 +294,14 @@ class ApplicationController < ActionController::Base
ensure ensure
Gitlab::I18n.reset_locale Gitlab::I18n.reset_locale
end end
def sessionless_sign_in(user)
if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
# sign in token, you can simply remove store: false.
sign_in user, store: false
end
end
end end
...@@ -40,6 +40,14 @@ class ProfilesController < Profiles::ApplicationController ...@@ -40,6 +40,14 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_account_path redirect_to profile_account_path
end end
def reset_rss_token
if current_user.reset_rss_token!
flash[:notice] = "RSS token was successfully reset"
end
redirect_to profile_account_path
end
def audit_log def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id). @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC"). order("created_at DESC").
......
...@@ -42,6 +42,8 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -42,6 +42,8 @@ class Projects::BlobController < Projects::ApplicationController
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
render 'show' render 'show'
end end
......
...@@ -32,6 +32,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -32,6 +32,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def folder def folder
folder_environments = project.environments.where(environment_type: params[:id]) folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available) @environments = folder_environments.with_state(params[:scope] || :available)
.order(:name)
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -285,7 +285,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -285,7 +285,10 @@ class Projects::IssuesController < Projects::ApplicationController
notice = "Please sign in to create the new issue." notice = "Please sign in to create the new issue."
if request.get? && !request.xhr?
store_location_for :user, request.fullpath store_location_for :user, request.fullpath
end
redirect_to new_user_session_path, notice: notice redirect_to new_user_session_path, notice: notice
end end
end end
...@@ -7,7 +7,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -7,7 +7,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def update def update
if @project.update_attributes(update_params) if @project.update_attributes(update_params)
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated." flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
else else
render 'show' render 'show'
......
...@@ -24,6 +24,8 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -24,6 +24,8 @@ class Projects::TreeController < Projects::ApplicationController
end end
end end
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
respond_to do |format| respond_to do |format|
format.html format.html
# Disable cache so browser history works # Disable cache so browser history works
......
...@@ -258,7 +258,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -258,7 +258,7 @@ class ProjectsController < Projects::ApplicationController
# #
# pages list order: repository readme, wiki home, issues list, customize workflow # pages list order: repository readme, wiki home, issues list, customize workflow
def render_landing_page def render_landing_page
if @project.feature_available?(:repository, current_user) if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists? return render 'projects/no_repo' unless @project.repository_exists?
render 'projects/empty' if @project.empty_repo? render 'projects/empty' if @project.empty_repo?
else else
......
...@@ -21,6 +21,8 @@ class UploadsController < ApplicationController ...@@ -21,6 +21,8 @@ class UploadsController < ApplicationController
can?(current_user, :read_project, model.project) can?(current_user, :read_project, model.project)
when User when User
true true
when Appearance
true
else else
permission = "read_#{model.class.to_s.underscore}".to_sym permission = "read_#{model.class.to_s.underscore}".to_sym
......
...@@ -91,7 +91,7 @@ module CommitsHelper ...@@ -91,7 +91,7 @@ module CommitsHelper
end end
def link_to_browse_code(project, commit) def link_to_browse_code(project, commit)
return unless current_controller?(:projects, :commits) return unless current_controller?(:commits)
if @path.blank? if @path.blank?
return link_to( return link_to(
......
require 'nokogiri' require 'nokogiri'
module MarkupHelper module MarkupHelper
include ActionView::Helpers::TagHelper
include ActionView::Context
def plain?(filename) def plain?(filename)
Gitlab::MarkupHelper.plain?(filename) Gitlab::MarkupHelper.plain?(filename)
end end
......
...@@ -49,7 +49,7 @@ module PreferencesHelper ...@@ -49,7 +49,7 @@ module PreferencesHelper
user_view = current_user.project_view user_view = current_user.project_view
if @project.feature_available?(:repository, current_user) if can?(current_user, :download_code, @project)
user_view user_view
elsif user_view == "activity" elsif user_view == "activity"
"activity" "activity"
......
module RssHelper module RssHelper
def rss_url_options def rss_url_options
{ format: :atom, private_token: current_user.try(:private_token) } { format: :atom, rss_token: current_user.try(:rss_token) }
end end
end end
...@@ -18,6 +18,7 @@ module SystemNoteHelper ...@@ -18,6 +18,7 @@ module SystemNoteHelper
'milestone' => 'icon_clock_o', 'milestone' => 'icon_clock_o',
'discussion' => 'icon_comment_o', 'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right', 'moved' => 'icon_arrow_circle_o_right',
'outdated' => 'icon_edit',
'approved' => 'icon_check', 'approved' => 'icon_check',
'unapproved' => 'icon_fa_close' 'unapproved' => 'icon_fa_close'
}.freeze }.freeze
......
...@@ -40,8 +40,22 @@ class Blob < SimpleDelegator ...@@ -40,8 +40,22 @@ class Blob < SimpleDelegator
BlobViewer::GitlabCiYml, BlobViewer::GitlabCiYml,
BlobViewer::RouteMap, BlobViewer::RouteMap,
BlobViewer::Readme,
BlobViewer::License, BlobViewer::License,
BlobViewer::Contributing BlobViewer::Contributing,
BlobViewer::Changelog,
BlobViewer::Cartfile,
BlobViewer::ComposerJson,
BlobViewer::Gemfile,
BlobViewer::Gemspec,
BlobViewer::GodepsJson,
BlobViewer::PackageJson,
BlobViewer::Podfile,
BlobViewer::Podspec,
BlobViewer::PodspecJson,
BlobViewer::RequirementsTxt,
BlobViewer::YarnLock
].freeze ].freeze
attr_reader :project attr_reader :project
......
...@@ -2,11 +2,17 @@ module BlobViewer ...@@ -2,11 +2,17 @@ module BlobViewer
module Auxiliary module Auxiliary
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::Allowable
included do included do
self.loading_partial_name = 'loading_auxiliary' self.loading_partial_name = 'loading_auxiliary'
self.type = :auxiliary self.type = :auxiliary
self.overridable_max_size = 100.kilobytes self.overridable_max_size = 100.kilobytes
self.max_size = 100.kilobytes self.max_size = 100.kilobytes
end end
def visible_to?(current_user)
true
end
end end
end end
...@@ -11,6 +11,8 @@ module BlobViewer ...@@ -11,6 +11,8 @@ module BlobViewer
attr_reader :blob attr_reader :blob
attr_accessor :override_max_size attr_accessor :override_max_size
delegate :project, to: :blob
def initialize(blob) def initialize(blob)
@blob = blob @blob = blob
end end
......
module BlobViewer
class Cartfile < DependencyManager
include Static
self.file_types = %i(cartfile)
def manager_name
'Carthage'
end
def manager_url
'https://github.com/Carthage/Carthage'
end
end
end
module BlobViewer
class Changelog < Base
include Auxiliary
include Static
self.partial_name = 'changelog'
self.file_types = %i(changelog)
self.binary = false
def render_error
return if project.repository.tag_count > 0
:no_tags
end
end
end
module BlobViewer
class ComposerJson < DependencyManager
include ServerSide
self.file_types = %i(composer_json)
def manager_name
'Composer'
end
def manager_url
'https://getcomposer.com/'
end
def package_name
@package_name ||= package_name_from_json('name')
end
def package_url
"https://packagist.org/packages/#{package_name}"
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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