Commit 8a0cd8ee authored by Nick Thomas's avatar Nick Thomas

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

parents b26cfbec 78de1c05
......@@ -55,7 +55,7 @@ stages:
.use-pg: &use-pg
services:
- postgres:latest
- postgres:9.2
- redis:alpine
- elasticsearch:5.3
......@@ -68,6 +68,7 @@ stages:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only:
- /mysql/
- /-stable$/
- master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce
......@@ -89,7 +90,7 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]}
- 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 CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
......@@ -120,7 +121,7 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]}
- 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 CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
......@@ -154,6 +155,7 @@ stages:
# Trigger a package build on omnibus-gitlab repository
build-package:
image: ruby:2.3-alpine
before_script: []
services: []
variables:
......@@ -183,8 +185,8 @@ update-knapsack:
<<: *only-canonical-masters
stage: post-test
script:
- 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_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
- '[[ -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
......@@ -318,6 +320,29 @@ downtime_check:
- /^[\d-]+-stable(-ee)?$/
- /(^docs[\/-].*|.*-docs$)/
<<<<<<< HEAD
=======
ee_compat_check:
<<: *rake-exec
only:
- branches@gitlab-org/gitlab-ce
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
allow_failure: yes
cache:
key: "ee_compat_check_repo"
paths:
- ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
expire_in: 10d
paths:
- ee_compat_check/patches/*.patch
>>>>>>> ce/master
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
stage: test
......
......@@ -971,14 +971,21 @@ RSpec/DescribeSymbol:
RSpec/DescribedClass:
Enabled: true
<<<<<<< HEAD
# Configuration parameters: CustomIncludeMethods.
=======
# Checks if an example group does not include any tests.
>>>>>>> ce/master
RSpec/EmptyExampleGroup:
Enabled: true
CustomIncludeMethods:
- run_permission_checks
<<<<<<< HEAD
- run_group_permission_checks
- it_should_email!
- it_should_not_email!
=======
>>>>>>> ce/master
# Checks for long example.
RSpec/ExampleLength:
......@@ -998,6 +1005,10 @@ RSpec/ExampleWording:
RSpec/ExpectActual:
Enabled: true
# Checks for opportunities to use `expect { … }.to output`.
RSpec/ExpectOutput:
Enabled: true
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
Enabled: true
......
......@@ -18,10 +18,6 @@ RSpec/EmptyLineAfterFinalLet:
RSpec/EmptyLineAfterSubject:
Enabled: false
# Offense count: 3
RSpec/ExpectOutput:
Enabled: false
# Offense count: 72
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
......
......@@ -22,7 +22,10 @@ entry.
- Display slash commands outcome when previewing Markdown. !10054 (Rares Sfirlogea)
- Resolve "Add more tests for spec/controllers/projects/builds_controller_spec.rb". !10244 (dosuken123)
- Add keyboard edit shotcut for wiki. !10245 (George Andrinopoulos)
<<<<<<< HEAD
- Update issue board cards design. !10353
=======
>>>>>>> ce/master
- Redirect old links after renaming a user/group/project. !10370
- Add system note on description change of issue/merge request. !10392 (blackst0ne)
- Improve validation of namespace & project paths. !10413
......@@ -137,7 +140,10 @@ entry.
- Remove `#` being added on commit sha in MR widget.
- Remove spinner from loading comment.
- Fixes an issue preventing screen readers from reading some icons.
<<<<<<< HEAD
- Add metrics events for incoming emails.
=======
>>>>>>> ce/master
- Load milestone tabs asynchronously to increase initial load performance.
- [BB Importer] Save the error trace and the whole raw document to debug problems easier.
- Fixed branches dropdown rendering branch names as HTML.
......
9.2.0-pre
9.3.0-pre
<<<<<<< HEAD
/* 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 */
var Api = {
=======
import $ from 'jquery';
const Api = {
>>>>>>> ce/master
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id.json',
namespacesPath: '/api/:version/namespaces.json',
......@@ -10,6 +16,7 @@ var Api = {
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
<<<<<<< HEAD
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
......@@ -37,51 +44,91 @@ var Api = {
// Return groups list. Filtered by query
groups: function(query, options, callback = $.noop) {
var url = Api.buildUrl(Api.groupsPath);
=======
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
.replace(':id', groupId);
return $.ajax({
url: url,
data: $.extend({
url,
dataType: 'json',
})
.done(group => callback(group));
},
// Return groups list. Filtered by query
groups(query, options, callback) {
const url = Api.buildUrl(Api.groupsPath);
>>>>>>> ce/master
return $.ajax({
url,
data: Object.assign({
search: query,
per_page: 20
per_page: 20,
}, options),
<<<<<<< HEAD
dataType: 'json'
}).done(function(groups) {
return callback(groups);
});
=======
dataType: 'json',
})
.done(groups => callback(groups));
>>>>>>> ce/master
},
// Return namespaces list. Filtered by query
namespaces: function(query, callback) {
var url = Api.buildUrl(Api.namespacesPath);
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
return $.ajax({
url: url,
url,
data: {
search: query,
per_page: 20
per_page: 20,
},
<<<<<<< HEAD
dataType: 'json'
}).done(function(namespaces) {
return callback(namespaces);
});
=======
dataType: 'json',
}).done(namespaces => callback(namespaces));
>>>>>>> ce/master
},
// Return projects list. Filtered by query
projects: function(query, options, callback) {
var url = Api.buildUrl(Api.projectsPath);
projects(query, options, callback) {
const url = Api.buildUrl(Api.projectsPath);
return $.ajax({
url: url,
data: $.extend({
url,
data: Object.assign({
search: query,
per_page: 20,
membership: true
membership: true,
}, options),
<<<<<<< HEAD
dataType: 'json'
}).done(function(projects) {
return callback(projects);
});
=======
dataType: 'json',
})
.done(projects => callback(projects));
>>>>>>> ce/master
},
newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
.replace(':namespace_path', namespace_path)
.replace(':project_path', project_path);
newLabel(namespacePath, projectPath, data, callback) {
const url = Api.buildUrl(Api.labelsPath)
.replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath);
return $.ajax({
<<<<<<< HEAD
url: url,
type: 'POST',
data: { 'label': data },
......@@ -91,68 +138,99 @@ var Api = {
}).error(function(message) {
return callback(message.responseJSON);
});
=======
url,
type: 'POST',
data: { label: data },
dataType: 'json',
})
.done(label => callback(label))
.error(message => callback(message.responseJSON));
>>>>>>> ce/master
},
// Return group projects list. Filtered by query
groupProjects: function(group_id, query, callback) {
var url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', group_id);
groupProjects(groupId, query, callback) {
const url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', groupId);
return $.ajax({
url: url,
url,
data: {
search: query,
per_page: 20
per_page: 20,
},
<<<<<<< HEAD
dataType: 'json'
}).done(function(projects) {
return callback(projects);
});
=======
dataType: 'json',
})
.done(projects => callback(projects));
>>>>>>> ce/master
},
// Return text for a specific license
licenseText: function(key, data, callback) {
var url = Api.buildUrl(Api.licensePath)
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
.replace(':key', key);
return $.ajax({
url: url,
data: data
}).done(function(license) {
return callback(license);
});
url,
data,
})
.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);
return $.get(url, function(gitignore) {
return callback(gitignore);
});
return $.get(url, gitignore => callback(gitignore));
},
gitlabCiYml: function(key, callback) {
var url = Api.buildUrl(Api.gitlabCiYmlPath)
gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath)
.replace(':key', key);
return $.get(url, function(file) {
return callback(file);
});
return $.get(url, file => 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);
},
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(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
$.ajax({
url: url,
dataType: 'json'
}).done(function(file) {
callback(null, file);
}).error(callback);
url,
dataType: 'json',
})
.done(file => callback(null, file))
.error(callback);
},
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return Api.wrapAjaxCall({
url,
data: Object.assign({
search: query,
per_page: 20,
}, options),
dataType: 'json',
});
},
buildUrl: function(url) {
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
url = gon.relative_url_root + url;
urlRoot = gon.relative_url_root;
}
<<<<<<< HEAD
return url.replace(':version', gon.api_version);
},
ldap_groups: function(query, provider, callback) {
......@@ -172,6 +250,24 @@ var Api = {
return callback(groups);
});
}
=======
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);
},
);
});
},
>>>>>>> ce/master
};
window.Api = Api;
export default Api;
/* global Api */
export default class FileTemplateSelector {
constructor(mediator) {
this.mediator = mediator;
......@@ -65,4 +63,3 @@ export default class FileTemplateSelector {
this.reportSelection(opts);
}
}
/* global Api */
import Api from '../../api';
import FileTemplateSelector from '../file_template_selector';
......
/* global Api */
import Api from '../../api';
import FileTemplateSelector from '../file_template_selector';
......
/* global Api */
import Api from '../../api';
import FileTemplateSelector from '../file_template_selector';
......
/* global Api */
import Api from '../../api';
import FileTemplateSelector from '../file_template_selector';
......
......@@ -50,9 +50,9 @@ export default class BlobViewer {
if (this.copySourceBtn) {
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');
});
}
}
......
......@@ -22,10 +22,13 @@ import './components/board_sidebar';
import './components/new_list_dropdown';
import './components/modal/index';
import '../vue_shared/vue_resource_interceptor';
<<<<<<< HEAD
import './components/boards_selector';
import collapseIcon from './icons/fullscreen_collapse.svg';
import expandIcon from './icons/fullscreen_expand.svg';
=======
>>>>>>> ce/master
Vue.use(VueResource);
......
import Vue from 'vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
......@@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({
maxCounter: 99,
};
},
components: {
userAvatarLink,
},
computed: {
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
......@@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({
</span>
</h4>
<div class="card-assignee">
<a
class="has-tooltip js-no-trigger"
:href="assigneeUrl(assignee)"
:title="assigneeUrlTitle(assignee)"
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
data-container="body"
data-placement="bottom"
>
<img
class="avatar avatar-inline s20"
:src="assignee.avatar"
width="20"
height="20"
:alt="avatarUrlTitle(assignee)"
/>
</a>
class="js-no-trigger"
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
:tooltip-text="assigneeUrlTitle(assignee)"
tooltip-placement="bottom"
/>
<span
class="avatar-counter has-tooltip"
:title="assigneeCounterTooltip"
......
......@@ -3,7 +3,10 @@
import Vue from 'vue';
import queryData from '../../utils/query_data';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
<<<<<<< HEAD
=======
>>>>>>> ce/master
import './header';
import './list';
import './footer';
......
......@@ -18,12 +18,12 @@ const gfmRules = {
},
},
TaskListFilter: {
'input[type=checkbox].task-list-item-checkbox'(el, text) {
'input[type=checkbox].task-list-item-checkbox'(el) {
return `[${el.checked ? 'x' : ' '}]`;
},
},
ReferenceFilter: {
'.tooltip'(el, text) {
'.tooltip'(el) {
return '';
},
'a.gfm:not([data-link=true])'(el, text) {
......@@ -39,15 +39,15 @@ const gfmRules = {
},
},
TableOfContentsFilter: {
'ul.section-nav'(el, text) {
'ul.section-nav'(el) {
return '[[_TOC_]]';
},
},
EmojiFilter: {
'img.emoji'(el, text) {
'img.emoji'(el) {
return el.getAttribute('alt');
},
'gl-emoji'(el, text) {
'gl-emoji'(el) {
return `:${el.getAttribute('data-name')}:`;
},
},
......@@ -57,13 +57,13 @@ const gfmRules = {
},
},
VideoLinkFilter: {
'.video-container'(el, text) {
'.video-container'(el) {
const videoEl = el.querySelector('video');
if (!videoEl) return false;
return CopyAsGFM.nodeToGFM(videoEl);
},
'video'(el, text) {
'video'(el) {
return `![${el.dataset.title}](${el.getAttribute('src')})`;
},
},
......@@ -74,19 +74,19 @@ const gfmRules = {
'code.code.math[data-math-style=inline]'(el, 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"]');
if (!mathAnnotation) return false;
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"]');
if (!mathAnnotation) return false;
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.
return '';
},
......@@ -95,7 +95,7 @@ const gfmRules = {
},
},
SanitizationFilter: {
'a[name]:not([href]):empty'(el, text) {
'a[name]:not([href]):empty'(el) {
return el.outerHTML;
},
'dl'(el, text) {
......@@ -143,7 +143,7 @@ const gfmRules = {
},
},
MarkdownFilter: {
'br'(el, text) {
'br'(el) {
// Two spaces at the end of a line are turned into a BR
return ' ';
},
......@@ -162,7 +162,7 @@ const gfmRules = {
'blockquote'(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
},
'img'(el, text) {
'img'(el) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
},
'a.anchor'(el, text) {
......@@ -222,10 +222,10 @@ const gfmRules = {
'sup'(el, text) {
return `^${text}`;
},
'hr'(el, text) {
'hr'(el) {
return '-----';
},
'table'(el, text) {
'table'(el) {
const theadEl = el.querySelector('thead');
const tbodyEl = el.querySelector('tbody');
if (!theadEl || !tbodyEl) return false;
......@@ -233,11 +233,11 @@ const gfmRules = {
const theadText = CopyAsGFM.nodeToGFM(theadEl);
const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
return theadText + tbodyText;
return [theadText, tbodyText].join('\n');
},
'thead'(el, text) {
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 after = '';
......@@ -262,10 +262,15 @@ const gfmRules = {
return before + middle + after;
});
return `${text}|${cells.join('|')}|`;
const separatorRow = `|${cells.join('|')}|`;
return [text, separatorRow].join('\n');
},
'tr'(el, text) {
const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
'tr'(el) {
const cellEls = el.querySelectorAll('td, th');
if (cellEls.length === 0) return false;
const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
return `| ${cells.join(' | ')} |`;
},
},
......@@ -273,12 +278,12 @@ const gfmRules = {
class CopyAsGFM {
constructor() {
$(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
$(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this));
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
$(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
}
copyAsGFM(e, transformer) {
static copyAsGFM(e, transformer) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
......@@ -292,26 +297,59 @@ class CopyAsGFM {
e.stopPropagation();
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;
if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
const gfm = clipboardData.getData('text/x-gfm');
if (!gfm) return;
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;
}
return gfm;
});
}
static transformGFMSelection(documentFragment) {
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return null;
const gfmEls = documentFragment.querySelectorAll('.md, .wiki');
switch (gfmEls.length) {
case 0: {
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 documentFragment;
return allGfmEl;
}
}
}
static transformCodeSelection(documentFragment) {
......@@ -343,7 +381,7 @@ class CopyAsGFM {
return codeEl;
}
static nodeToGFM(node) {
static nodeToGFM(node, respectWhitespaceParam = false) {
if (node.nodeType === Node.COMMENT_NODE) {
return '';
}
......@@ -352,7 +390,9 @@ class CopyAsGFM {
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) {
return text;
......@@ -366,7 +406,17 @@ class CopyAsGFM {
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;
return result;
......@@ -376,7 +426,7 @@ class CopyAsGFM {
return text;
}
static innerGFM(parentNode) {
static innerGFM(parentNode, respectWhitespace = false) {
const nodes = parentNode.childNodes;
const clonedParentNode = parentNode.cloneNode(true);
......@@ -386,13 +436,19 @@ class CopyAsGFM {
const node = nodes[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.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 */
/* global Api */
import Api from './api';
class CreateLabelDropdown {
constructor ($el, namespacePath, projectPath) {
......
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
......@@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
......@@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<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">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
......@@ -28,11 +33,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ __('OpenedNDaysAgo|Opened') }}
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ __('ByAuthor|by') }}
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
......
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
......@@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
......@@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<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">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
......@@ -28,11 +32,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ __('OpenedNDaysAgo|Opened') }}
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ __('ByAuthor|by') }}
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
......
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
const global = window.gl || (window.gl = {});
......@@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
data() {
return { iconCommit };
},
template: `
<div>
<div class="events-description">
......@@ -24,17 +26,18 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<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">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
{{ __('FirstPushedBy|First') }}
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span>
<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">
{{ commit.author.name }}
</a>
......
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
......@@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
......@@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<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">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
......@@ -28,11 +32,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ __('OpenedNDaysAgo|Opened') }}
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ __('ByAuthor|by') }}
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
......
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
......@@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
......@@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<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">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
......@@ -28,11 +32,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ __('OpenedNDaysAgo|Opened') }}
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ __('ByAuthor|by') }}
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
......
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
......@@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
data() {
return { iconBranch };
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
......@@ -22,7 +26,8 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<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">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
......@@ -32,7 +37,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
{{ __('ByAuthor|by') }}
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
......
......@@ -3,6 +3,7 @@
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({
props: ['discussionId'],
......@@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({
collapseIcon,
};
},
components: {
userAvatarImage,
},
template: `
<div class="diff-comment-avatar-holders"
v-show="notesCount !== 0">
<div v-if="!isVisible">
<img v-for="note in notesSubset"
class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
width="19"
height="19"
role="button"
data-container="body"
data-placement="top"
data-html="true"
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image
v-for="note in notesSubset"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="clickedAvatar($event)"
:img-src="note.authorAvatar"
:tooltip-text="getTooltipText(note)"
:data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar"
@click="clickedAvatar($event)" />
:size="19"
data-html="true"
/>
<span v-if="notesCount > shownAvatars"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
data-container="body"
......@@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({
setDiscussionVisible() {
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';
import GroupName from './group_name';
import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import setupProjectEdit from './project_edit';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import Landing from './landing';
......@@ -58,9 +59,12 @@ import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob';
<<<<<<< HEAD
// EE-only
import ApproversSelect from './approvers_select';
=======
>>>>>>> ce/master
(function() {
var Dispatcher;
......@@ -275,9 +279,14 @@ import ApproversSelect from './approvers_select';
new NotificationsForm();
if ($('#tree-slider').length) {
new TreeView();
}
if ($('.blob-viewer').length) {
new BlobViewer();
}
break;
case 'projects:edit':
setupProjectEdit();
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
......
/* eslint-disable */
import AjaxCache from '~/lib/utils/ajax_cache';
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) {
if (config.loadingTemplate) {
var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
......@@ -31,7 +14,6 @@ const Ajax = {
init: function init(hook) {
var self = this;
self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.Ajax;
this.hook = hook;
if (!config || !config.endpoint || !config.method) {
......@@ -48,14 +30,10 @@ const Ajax = {
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
if (self.cache[config.endpoint]) {
self._loadData(self.cache[config.endpoint], config, self);
} else {
this._loadUrlData(config.endpoint)
.then(function(d) {
self._loadData(d, config, self);
}, config.onError).catch(config.onError);
}
AjaxCache.retrieve(config.endpoint)
.then((data) => self._loadData(data, config, self))
.catch(config.onError);
},
destroy: function() {
this.destroyed = true;
......
/* eslint-disable */
import AjaxCache from '../../lib/utils/ajax_cache';
const AjaxFilter = {
init: function(hook) {
......@@ -58,50 +59,24 @@ const AjaxFilter = {
this.loading = true;
var params = config.params || {};
params[config.searchKey] = searchValue;
var self = this;
self.cache = self.cache || {};
var url = config.endpoint + this.buildParams(params);
var urlCachedData = self.cache[url];
if (urlCachedData) {
self._loadData(urlCachedData, config, self);
} else {
this._loadUrlData(url)
.then(function(data) {
self._loadData(data, config, self);
}, config.onError).catch(config.onError);
}
return AjaxCache.retrieve(url)
.then((data) => {
this._loadData(data, config);
})
.catch(config.onError);
},
_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) {
const list = self.hook.list;
_loadData(data, config) {
const list = this.hook.list;
if (config.loadingTemplate && list.data === undefined ||
list.data.length === 0) {
const dataLoadingTemplate = list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
dataLoadingTemplate.outerHTML = this.listTemplate;
}
}
if (!self.destroyed) {
if (!this.destroyed) {
var hookListChildren = list.list.children;
var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
if (onlyDynamicList && data.length === 0) {
......@@ -109,7 +84,7 @@ const AjaxFilter = {
}
list.setData.call(list, data);
}
self.notLoading();
this.notLoading();
list.currentIndex = 0;
},
......
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
......@@ -15,6 +16,7 @@ const timeagoInstance = new Timeago();
export default {
components: {
userAvatarLink,
'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
......@@ -485,15 +487,13 @@ export default {
<span v-if="!model.isFolder && deploymentHasUser">
by
<a
:href="deploymentUser.web_url"
class="js-deploy-user-container">
<img
class="avatar has-tooltip s20"
:src="deploymentUser.avatar_url"
:alt="userImageAltDescription"
:title="deploymentUser.username" />
</a>
<user-avatar-link
class="js-deploy-user-container"
:link-href="deploymentUser.web_url"
:img-src="deploymentUser.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="deploymentUser.username"
/>
</span>
</td>
......
......@@ -56,7 +56,11 @@ class DropdownHint extends gl.FilteredSearchDropdown {
}
renderContent() {
<<<<<<< HEAD
const dropdownData = this.tokenKeys.get()
=======
const dropdownData = gl.FilteredSearchTokenKeys.get()
>>>>>>> ce/master
.map(tokenKey => ({
icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key,
......
......@@ -8,6 +8,9 @@ import './filtered_search_manager';
import './filtered_search_token_keys';
import './filtered_search_tokenizer';
import './filtered_search_visual_tokens';
<<<<<<< HEAD
// EE-only
import './filtered_search_token_keys_with_weights';
=======
>>>>>>> ce/master
......@@ -454,7 +454,10 @@ class FilteredSearchManager {
const { tokens, searchToken }
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
<<<<<<< HEAD
=======
>>>>>>> ce/master
const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
......
......@@ -7,9 +7,10 @@ import GfmAutoComplete from './gfm_auto_complete';
window.gl = window.gl || {};
function GLForm(form) {
function GLForm(form, enableGFM = false) {
this.form = form;
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
this.destroy();
// Setup the form
......@@ -32,8 +33,14 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form');
// 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'));
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);
autosize(this.textarea);
}
......
......@@ -44,18 +44,18 @@ export default class GroupName {
showToggle() {
this.title.classList.add('wrap');
this.toggle.classList.remove('hidden');
if (this.isHidden) this.groupTitle.classList.add('is-hidden');
if (this.isHidden) this.groupTitle.classList.add('hidden');
}
hideToggle() {
this.title.classList.remove('wrap');
this.toggle.classList.add('hidden');
if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
if (this.isHidden) this.groupTitle.classList.remove('hidden');
}
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
this.groupTitle.classList.toggle('hidden');
}
render() {
......
......@@ -3,7 +3,7 @@
prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
/* global Api */
import Api from './api';
var slice = [].slice;
......
class AjaxCache {
import Cache from './cache';
class AjaxCache extends Cache {
constructor() {
this.internalStorage = { };
super();
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) {
if (this.hasData(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 @@
const textBefore = value.substring(0, selectionStart);
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.selectionStart = target.selectionEnd = selectionStart + text.length;
target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave
$(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.
......@@ -170,6 +170,7 @@ import './visibility_select';
import './wikis';
import './zen_mode';
<<<<<<< HEAD
// EE-only scripts
import './admin_email_select';
import './application_settings';
......@@ -178,6 +179,8 @@ import './ldap_groups_select';
import './path_locks';
import './weight_select';
=======
>>>>>>> ce/master
// eslint-disable-next-line global-require, import/no-commonjs
if (process.env.NODE_ENV !== 'production') require('./test_utils/');
......
......@@ -288,7 +288,11 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
if (anchor) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.addDiffNote(anchor, lineType, false);
notes.toggleDiffNote({
target: anchor,
lineType,
forceShow: true,
});
anchor[0].scrollIntoView();
// We have multiple elements on the page with `#note_xxx`
// (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 */
/* global Api */
import Api from './api';
(function() {
window.NamespaceSelect = (function() {
......
......@@ -29,7 +29,7 @@ const normalizeNewlines = function(str) {
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.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this);
......@@ -52,6 +52,7 @@ const normalizeNewlines = function(str) {
this.notes_url = notes_url;
this.note_ids = note_ids;
this.enableGFM = enableGFM;
// Used to keep track of updated notes while people are editing things
this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at;
......@@ -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.
......@@ -308,20 +316,17 @@ const normalizeNewlines = function(str) {
}
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (this.isNewNote(noteEntity)) {
if (Notes.isNewNote(noteEntity, this.note_ids)) {
this.note_ids.push(noteEntity.id);
const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
// Update datetime format on the recent note
gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
this.collapseLongCommitList();
this.taskList.init();
this.setupNewNote($newNote);
this.refresh();
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.
else if (this.isUpdatedNote(noteEntity, $note)) {
else if (Notes.isUpdatedNote(noteEntity, $note)) {
const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines(
$note.find('.original-note-content').text().trim()
......@@ -342,30 +347,11 @@ const normalizeNewlines = function(str) {
}
else {
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
// Update datetime format on the recent note
gl.utils.localTimeAgo($updatedNote.find('.js-timeago'), false);
this.setupNewNote($updatedNote);
}
}
};
/*
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() {
return Cookies.get('diff_view') === 'parallel';
};
......@@ -378,7 +364,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.renderDiscussionNote = function(noteEntity, $form) {
var discussionContainer, form, row, lineType, diffAvatarContainer;
if (!this.isNewNote(noteEntity)) {
if (!Notes.isNewNote(noteEntity, this.note_ids)) {
return;
}
this.note_ids.push(noteEntity.id);
......@@ -525,7 +511,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupNoteForm = function(form) {
var textarea, key;
new gl.GLForm(form);
new gl.GLForm(form, this.enableGFM);
textarea = form.find(".js-note-text");
key = [
"Note",
......@@ -596,12 +582,12 @@ const normalizeNewlines = function(str) {
Updates the current note field.
*/
Notes.prototype.updateNote = function(_xhr, noteEntity, _status) {
Notes.prototype.updateNote = function(noteEntity, $targetNote) {
var $noteEntityEl, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
$noteEntityEl = $(noteEntity.html);
$noteEntityEl.addClass('fade-in-full');
this.revertNoteEditForm();
this.revertNoteEditForm($targetNote);
gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl));
$noteEntityEl.renderGFM();
$noteEntityEl.find('.js-task-list-container').taskList('enable');
......@@ -683,10 +669,8 @@ const normalizeNewlines = function(str) {
if (this.updatedNotesTrackingMap[noteId]) {
const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
$note.replaceWith($newNote);
this.setupNewNote($newNote);
this.updatedNotesTrackingMap[noteId] = null;
// Update datetime format on the recent note
gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
}
else {
$note.find('.js-finish-edit-warning').hide();
......@@ -876,12 +860,22 @@ const normalizeNewlines = function(str) {
Notes.prototype.onAddDiffNote = function(e) {
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');
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;
$link = $(target);
row = $link.closest("tr");
......@@ -926,12 +920,12 @@ const normalizeNewlines = function(str) {
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
targetRow.show();
notesContent.toggle(!notesContent.is(':visible'));
const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
if (!targetRow.find('.content:not(:empty)').is(':visible')) {
targetRow.hide();
}
targetRow.toggle(showNow);
notesContent.toggle(showNow);
}
if (addForm) {
......@@ -1139,6 +1133,25 @@ const normalizeNewlines = function(str) {
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() {
if (gl.utils.getPagePath(1) === 'merge_requests') {
gl.mrWidget.checkStatus();
......@@ -1394,7 +1407,7 @@ const normalizeNewlines = function(str) {
gl.utils.ajaxPost(formAction, formData)
.then((note) => {
// Submission successful! render final note element
this.updateNote(null, note, null);
this.updateNote(note, $editingNote);
})
.fail(() => {
// Submission failed, revert back to original note
......
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
props: [
'pipeline',
......@@ -7,6 +9,9 @@ export default {
return !!this.pipeline.user;
},
},
components: {
userAvatarLink,
},
template: `
<td>
<a
......@@ -15,18 +20,13 @@ export default {
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<a
class="js-pipeline-url-user"
<user-avatar-link
v-if="user"
:href="pipeline.user.web_url">
<img
v-if="user"
class="avatar has-tooltip s20 "
:title="pipeline.user.name"
data-container="body"
:src="pipeline.user.avatar_url"
>
</a>
class="js-pipeline-url-user"
:link-href="pipeline.user.web_url"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
/>
<span
v-if="!user"
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 */
/* global Api */
import Api from './api';
(function() {
this.ProjectSelect = (function() {
......
import Raven from 'raven-js';
import $ from 'jquery';
const IGNORE_ERRORS = [
// Random plugins/extensions
......@@ -74,7 +75,7 @@ const RavenConfig = {
},
bindRavenErrors() {
window.$(document).on('ajaxError.raven', this.handleRavenErrors);
$(document).on('ajaxError.raven', this.handleRavenErrors);
},
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 */
/* global Flash */
/* global Api */
import Api from './api';
(function() {
this.Search = (function() {
......
......@@ -38,7 +38,7 @@ import './shortcuts_navigation';
}
ShortcutsIssuable.prototype.replyWithSelectedText = function() {
var quote, documentFragment, selected, separator;
var quote, documentFragment, el, selected, separator;
var replyField = $('.js-main-target-form #note_note');
documentFragment = window.gl.utils.getSelectedFragment();
......@@ -47,10 +47,8 @@ import './shortcuts_navigation';
return;
}
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return;
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
selected = window.gl.CopyAsGFM.nodeToGFM(el);
if (selected.trim() === "") {
return;
......
......@@ -67,10 +67,11 @@ export default {
<div>
<assignee-title
:number-of-assignees="store.assignees.length"
:loading="loading"
:loading="loading || store.isFetching.assignees"
:editable="store.editable"
/>
<assignees
v-if="!store.isFetching.assignees"
class="value"
:root-path="store.rootPath"
:users="store.assignees"
......
......@@ -10,6 +10,9 @@ export default class SidebarStore {
this.humanTimeEstimate = '';
this.humanTimeSpent = '';
this.assignees = [];
this.isFetching = {
assignees: true,
};
SidebarStore.singleton = this;
}
......@@ -18,6 +21,7 @@ export default class SidebarStore {
}
setAssigneeData(data) {
this.isFetching.assignees = false;
if (data.assignees) {
this.assignees = data.assignees;
}
......
/* 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';
......
......@@ -421,6 +421,15 @@ function UsersSelect(currentUser, els) {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
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) {
return user.id;
......
......@@ -56,7 +56,7 @@ export default {
<div class="ci-widget">
<div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link">
<span
<span class="ci-status-icon"
v-html="svg"
aria-hidden="true"></span>
</span>
......
......@@ -92,10 +92,7 @@ export default {
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a
:href="mr.targetBranchPath">
{{mr.targetBranch}}
</a>
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
......
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetAutoMergeFailed',
props: {
mr: { type: Object, required: true },
},
data() {
return {
isRefreshing: false,
};
},
methods: {
refreshWidget() {
this.isRefreshing = true;
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isRefreshing = false;
});
},
},
template: `
<div class="mr-widget-body">
<button
......@@ -13,8 +28,19 @@ export default {
</button>
<span class="bold danger">
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>
<div class="merge-error-text">
<div class="merge-error-text danger bold">
{{mr.mergeError}}
</div>
</div>
......
......@@ -33,7 +33,7 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
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 inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
......@@ -214,7 +214,7 @@ export default {
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-info dropdown-toggle"
class="btn btn-small btn-info dropdown-toggle"
data-toggle="dropdown">
<i
class="fa fa-caret-down"
......
......@@ -11,10 +11,6 @@ export default function deviseState(data) {
return 'conflicts';
} else if (data.work_in_progress) {
return 'workInProgress';
} else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
} else if (!this.canMerge) {
return 'notAllowedToMerge';
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return 'pipelineFailed';
} else if (this.hasMergeableDiscussionsState) {
......@@ -23,6 +19,10 @@ export default function deviseState(data) {
return 'pipelineBlocked';
} else if (this.hasSHAChanged) {
return 'shaMismatch';
} else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
} else if (!this.canMerge) {
return 'notAllowedToMerge';
} else if (this.canBeMerged) {
return 'readyToMerge';
}
......
......@@ -4,7 +4,7 @@ import { getStateKey } from '../dependencies';
export default class MergeRequestStore {
constructor(data) {
this.startingSha = data.diff_head_sha;
this.sha = data.diff_head_sha;
this.setData(data);
}
......@@ -19,7 +19,6 @@ export default class MergeRequestStore {
this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.sha = data.diff_head_sha;
this.commitMessage = data.merge_commit_message;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
......@@ -72,7 +71,7 @@ export default class MergeRequestStore {
this.canMerge = !!data.merge_path;
this.canCreateIssue = currentUser.can_create_issue || false;
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;
// Cherry-pick and Revert actions related
......
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
props: {
......@@ -110,6 +111,9 @@ export default {
return { commitIconSvg };
},
components: {
userAvatarLink,
},
template: `
<div class="branch-commit">
......@@ -133,16 +137,14 @@ export default {
<p class="commit-title">
<span v-if="title">
<a v-if="hasAuthor"
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
:href="author.web_url">
<img
class="avatar has-tooltip s20"
:src="author.avatar_url"
:alt="userImageAltDescription"
:title="author.username" />
</a>
:link-href="author.web_url"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
<a class="commit-row-message"
:href="commitUrl">
{{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 @@
border-radius: $avatar_radius;
border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); }
&.s18 { @include avatar-size(18px, 6px); }
&.s19 { @include avatar-size(19px, 6px); }
&.s20 { @include avatar-size(20px, 7px); }
&.s24 { @include avatar-size(24px, 8px); }
&.s26 { @include avatar-size(26px, 8px); }
......
......@@ -108,8 +108,9 @@
}
.award-control {
margin-right: 5px;
margin: 0 5px 6px 0;
outline: 0;
position: relative;
&.disabled {
cursor: default;
......@@ -227,8 +228,8 @@
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
left: 11px;
bottom: 7px;
left: 10px;
bottom: 6px;
opacity: 0;
@include transition(opacity, transform);
}
......@@ -237,7 +238,3 @@
vertical-align: middle;
}
}
.note-awards .award-control-icon-positive {
left: 6px;
}
......@@ -263,7 +263,9 @@
}
.filtered-search-input-dropdown-menu {
max-height: 215px;
max-width: 280px;
overflow: auto;
@media (max-width: $screen-xs-min) {
width: auto;
......@@ -372,11 +374,6 @@
padding: 0;
}
.filter-dropdown {
max-height: 215px;
overflow: auto;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.issue-bulk-update-dropdown-toggle {
width: 100px;
......
......@@ -31,7 +31,6 @@ header {
border: none;
border-bottom: 1px solid $border-color;
position: fixed;
z-index: 300;
top: 0;
left: 0;
right: 0;
......@@ -41,7 +40,17 @@ header {
}
&.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 {
......@@ -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 {
position: absolute;
left: -10px;
......
......@@ -65,3 +65,7 @@
text-decoration: none;
}
}
.user-avatar-link {
text-decoration: none;
}
......@@ -470,8 +470,8 @@
}
}
.activities {
.nav-block {
.nav-block {
&.activities {
border-bottom: 1px solid $border-color;
.nav-links {
......
......@@ -83,4 +83,8 @@
position: fixed;
top: $header-height;
}
&:not(.affix-top) {
min-height: 100%;
}
}
......@@ -5,7 +5,7 @@
.note-text {
p:last-child {
margin-bottom: 0;
margin-bottom: 0 !important;
}
}
......@@ -23,7 +23,6 @@
}
.timeline-entry {
padding: $gl-padding $gl-btn-padding 0;
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
......
......@@ -139,6 +139,15 @@
line-height: 1.6em;
overflow-x: auto;
border-radius: 2px;
&.plain-readme {
background: none;
border: none;
padding: 0;
margin: 0;
font-size: 14px;
}
}
p > code {
......@@ -169,14 +178,14 @@
}
ul.task-list {
li.task-list-item {
> li.task-list-item {
list-style-type: none;
position: relative;
min-height: 22px;
padding-left: 28px;
margin-left: 0 !important;
input.task-list-item-checkbox {
> input.task-list-item-checkbox {
position: absolute;
left: 8px;
top: 5px;
......@@ -279,14 +288,6 @@ h6 {
/** CODE **/
pre {
font-family: $monospace_font;
&.plain-readme {
background: none;
border: none;
padding: 0;
margin: 0;
font-size: 14px;
}
}
code {
......
......@@ -4,11 +4,7 @@
color: $gl-text-color;
line-height: 34px;
.author {
color: $gl-text-color;
}
.identifier {
a {
color: $gl-text-color;
}
......
......@@ -68,10 +68,6 @@
margin: 0;
}
.avatar-image-container {
text-decoration: none;
}
.icon-play {
height: 13px;
width: 12px;
......
......@@ -10,7 +10,6 @@
.page-content-header,
.commit-box,
.info-well,
.notes,
.commit-ci-menu,
.files-changed {
@extend .fixed-width-container;
......@@ -57,6 +56,10 @@
padding: 5px;
max-height: calc(100vh - 100px);
}
.emoji-block {
padding: 10px 0 4px;
}
}
.issuable-filter-count {
......
......@@ -209,17 +209,30 @@
}
}
.mr-widget-heading,
.mr-widget-body {
.mr-widget-heading {
.btn-default.btn-xs {
margin-left: 5px;
}
}
.mr-widget-body {
.btn {
font-size: 15px;
}
.btn-group .btn {
padding: 5px 10px;
&.dropdown-toggle {
padding: 5px 7px;
}
}
}
.mr-widget-body {
h4 {
font-weight: 600;
font-size: 16px;
font-weight: bold;
font-size: 15px;
margin: 5px 0;
color: $gl-text-color;
......@@ -246,8 +259,8 @@
}
.bold {
margin-left: 5px;
font-weight: bold;
font-size: 15px;
color: $gl-gray-light;
}
......@@ -271,6 +284,11 @@
margin-bottom: 24px;
}
.spacing,
.bold {
vertical-align: middle;
}
.dropdown-menu {
li a {
padding: 5px;
......@@ -389,6 +407,12 @@
}
}
.mr-state-widget .mr-widget-body {
.approve-btn {
margin-right: 5px;
}
}
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
......@@ -531,7 +555,7 @@
p {
float: left;
padding-left: 20px;
padding-left: 21px;
&::before {
top: 13px;
......
......@@ -164,10 +164,6 @@
.discussion-body,
.diff-file {
.notes .note {
padding: 10px 15px;
}
.discussion-reply-holder {
background-color: $white-light;
padding: 10px 16px;
......
......@@ -43,7 +43,11 @@ ul.notes {
}
.discussion-body {
padding-top: 15px;
padding-top: 8px;
.panel {
margin-bottom: 0;
}
}
.discussion {
......@@ -53,6 +57,7 @@ ul.notes {
}
.note {
padding: $gl-padding $gl-btn-padding 0;
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
......@@ -78,11 +83,7 @@ ul.notes {
&.note-discussion {
&.timeline-entry {
padding: 14px 10px;
}
.system-note {
padding: 0;
padding: $gl-padding 10px;
}
}
......@@ -167,7 +168,7 @@ ul.notes {
margin-left: 65px;
}
.note-header {
.note-header-info {
padding-bottom: 0;
}
......@@ -377,7 +378,11 @@ ul.notes {
.note-header-info {
min-width: 0;
padding-bottom: 5px;
padding-bottom: 8px;
}
.system-note .note-header-info {
padding-bottom: 0;
}
.note-headline-light {
......@@ -582,6 +587,17 @@ ul.notes {
}
}
.discussion-body,
.diff-file {
.notes .note {
padding: 10px 15px;
&.system-note {
padding: 0;
}
}
}
.diff-file {
.is-over {
.add-diff-note {
......@@ -669,7 +685,7 @@ ul.notes {
.line-resolve-btn {
position: relative;
top: 2px;
top: 0;
padding: 0;
background-color: transparent;
border: none;
......@@ -690,8 +706,8 @@ ul.notes {
svg {
fill: $gray-darkest;
height: 15px;
width: 15px;
height: 16px;
width: 16px;
}
.loading {
......
......@@ -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 {
width: 380px;
......
......@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
before_action :authenticate_user_from_private_token!
before_action :authenticate_user_from_rss_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :check_password_expiration
......@@ -72,13 +73,20 @@ class ApplicationController < ActionController::Base
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
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
sessionless_sign_in(user)
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
def log_exception(exception)
......@@ -286,4 +294,14 @@ class ApplicationController < ActionController::Base
ensure
Gitlab::I18n.reset_locale
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
......@@ -34,7 +34,11 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group)
params[:group_id] = group.to_param
<<<<<<< HEAD
=======
>>>>>>> ce/master
url_for(params)
end
end
......@@ -179,7 +179,11 @@ class GroupsController < Groups::ApplicationController
def build_canonical_path(group)
return group_path(group) if action_name == 'show' # root group path
<<<<<<< HEAD
=======
>>>>>>> ce/master
params[:id] = group.to_param
url_for(params)
......
......@@ -40,6 +40,14 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_account_path
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
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC").
......
......@@ -42,6 +42,8 @@ class Projects::BlobController < Projects::ApplicationController
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
render 'show'
end
......
......@@ -32,6 +32,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
.order(:name)
respond_to do |format|
format.html
......
......@@ -285,7 +285,10 @@ class Projects::IssuesController < Projects::ApplicationController
notice = "Please sign in to create the new issue."
store_location_for :user, request.fullpath
if request.get? && !request.xhr?
store_location_for :user, request.fullpath
end
redirect_to new_user_session_path, notice: notice
end
end
......@@ -10,10 +10,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge,
<<<<<<< HEAD
:pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds,
:remove_wip, :resolve_conflicts, :assign_related_issues, :commit_change_content,
# EE
:approve, :approvals, :unapprove, :rebase
=======
:pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :commit_change_content
>>>>>>> ce/master
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
......
......@@ -7,7 +7,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def update
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)
else
render 'show'
......
......@@ -24,6 +24,8 @@ class Projects::TreeController < Projects::ApplicationController
end
end
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
respond_to do |format|
format.html
# Disable cache so browser history works
......
......@@ -258,7 +258,7 @@ class ProjectsController < Projects::ApplicationController
#
# pages list order: repository readme, wiki home, issues list, customize workflow
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?
render 'projects/empty' if @project.empty_repo?
else
......
......@@ -21,6 +21,8 @@ class UploadsController < ApplicationController
can?(current_user, :read_project, model.project)
when User
true
when Appearance
true
else
permission = "read_#{model.class.to_s.underscore}".to_sym
......
......@@ -91,7 +91,7 @@ module CommitsHelper
end
def link_to_browse_code(project, commit)
return unless current_controller?(:projects, :commits)
return unless current_controller?(:commits)
if @path.blank?
return link_to(
......
require 'nokogiri'
module MarkupHelper
include ActionView::Helpers::TagHelper
include ActionView::Context
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
......
......@@ -49,7 +49,7 @@ module PreferencesHelper
user_view = current_user.project_view
if @project.feature_available?(:repository, current_user)
if can?(current_user, :download_code, @project)
user_view
elsif user_view == "activity"
"activity"
......
module RssHelper
def rss_url_options
{ format: :atom, private_token: current_user.try(:private_token) }
{ format: :atom, rss_token: current_user.try(:rss_token) }
end
end
......@@ -18,8 +18,12 @@ module SystemNoteHelper
'milestone' => 'icon_clock_o',
'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right',
<<<<<<< HEAD
'approved' => 'icon_check',
'unapproved' => 'icon_fa_close'
=======
'outdated' => 'icon_edit'
>>>>>>> ce/master
}.freeze
def icon_for_system_note(note)
......
......@@ -40,8 +40,22 @@ class Blob < SimpleDelegator
BlobViewer::GitlabCiYml,
BlobViewer::RouteMap,
BlobViewer::Readme,
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
attr_reader :project
......
......@@ -2,11 +2,17 @@ module BlobViewer
module Auxiliary
extend ActiveSupport::Concern
include Gitlab::Allowable
included do
self.loading_partial_name = 'loading_auxiliary'
self.type = :auxiliary
self.overridable_max_size = 100.kilobytes
self.max_size = 100.kilobytes
end
def visible_to?(current_user)
true
end
end
end
......@@ -11,6 +11,8 @@ module BlobViewer
attr_reader :blob
attr_accessor :override_max_size
delegate :project, to: :blob
def initialize(blob)
@blob = blob
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.
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