Commit d0ddfcd0 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce_upstream[ci skip]

parents 826d6526 6ce1df41
......@@ -7,3 +7,4 @@
/vendor/
karma.config.js
webpack.config.js
/app/assets/javascripts/locale/**/*.js
*.log
*.swp
*.mo
*.edit.po
.DS_Store
.bundle
.chef
......@@ -55,3 +57,4 @@ eslint-report.html
/shared/*
/.gitlab_workhorse_secret
/webpack-report/
/locale/**/LC_MESSAGES
......@@ -26,6 +26,7 @@ before_script:
- source scripts/prepare_build.sh
stages:
- build
- prepare
- test
- post-test
......@@ -142,6 +143,28 @@ stages:
<<: *only-master-and-ee-or-mysql
<<: *except-docs
# Trigger a package build on omnibus-gitlab repository
build-package:
services: []
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
stage: build
when: manual
script:
# If no branch in omnibus is specified, trigger pipeline against master
- if [ -z "$OMNIBUS_BRANCH" ] ; then export OMNIBUS_BRANCH=master ;fi
- echo "token=${BUILD_TRIGGER_TOKEN}" > version_details
- echo "ref=${OMNIBUS_BRANCH}" >> version_details
- echo "variables[ALTERNATIVE_SOURCES]=true" >> version_details
- echo "variables[GITLAB_VERSION]=${CI_COMMIT_SHA}" >> version_details
# Collect version details of all components
- for f in *_VERSION; do echo "variables[$f]=$(cat $f)" >> version_details; done
# Trigger the API and pass values collected above as parameters to it
- cat version_details | tr '\n' '&' | curl -X POST https://gitlab.com/api/v4/projects/20699/trigger/pipeline --data-binary @-
- rm version_details
# Prepare and merge knapsack tests
knapsack:
<<: *knapsack-state
......@@ -397,18 +420,6 @@ rake karma:
paths:
- coverage-javascript/
bundler:audit:
stage: test
<<: *ruby-static-analysis
<<: *dedicated-runner
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- "bundle exec bundle-audit check --update --ignore CVE-2016-4658"
.migration-paths: &migration-paths
stage: test
<<: *dedicated-runner
......
......@@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.1.3 (2017-05-05)
- Do not show private groups on subgroups page if user doesn't have access to.
- Enforce project features when searching blobs and wikis.
- Fixed branches dropdown rendering branch names as HTML.
- Make Asciidoc & other markup go through pipeline to prevent XSS.
- Validate URLs in markdown using URI to detect the host correctly.
- Fix for XSS in project import view caused by Hamlit filter usage.
- Sanitize submodule URLs before linking to them in the file tree view.
- Refactor snippets finder & dont return internal snippets for external users.
- Fix snippets visibility for show action - external users can not see internal snippets.
## 9.1.2 (2017-05-01)
- Add index on ci_runners.contacted_at. !10876 (blackst0ne)
......@@ -277,6 +289,18 @@ entry.
- Only send chat notifications for the default branch.
- Don't fill in the default kubernetes namespace.
## 9.0.7 (2017-05-05)
- Enforce project features when searching blobs and wikis.
- Fixed branches dropdown rendering branch names as HTML.
- Make Asciidoc & other markup go through pipeline to prevent XSS.
- Validate URLs in markdown using URI to detect the host correctly.
- Fix for XSS in project import view caused by Hamlit filter usage.
- Sanitize submodule URLs before linking to them in the file tree view.
- Refactor snippets finder & dont return internal snippets for external users.
- Fix snippets visibility for show action - external users can not see internal snippets.
- Do not show private groups on subgroups page if user doesn't have access to.
## 9.0.6 (2017-04-21)
- Bugfix: POST /projects/:id/hooks and PUT /projects/:id/hook/:hook_id no longer ignore the the job_events param in the V4 API. !10586
......@@ -621,6 +645,17 @@ entry.
- Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids.
## 8.17.6 (2017-05-05)
- Enforce project features when searching blobs and wikis.
- Fixed branches dropdown rendering branch names as HTML.
- Make Asciidoc & other markup go through pipeline to prevent XSS.
- Validate URLs in markdown using URI to detect the host correctly.
- Fix for XSS in project import view caused by Hamlit filter usage.
- Sanitize submodule URLs before linking to them in the file tree view.
- Refactor snippets finder & dont return internal snippets for external users.
- Fix snippets visibility for show action - external users can not see internal snippets.
## 8.17.5 (2017-04-05)
- Don’t show source project name when user does not have access.
......
This diff is collapsed.
......@@ -266,6 +266,12 @@ gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
# I18n
gem 'ruby_parser', '~> 3.8.4', require: false
gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
......
......@@ -222,6 +222,7 @@ GEM
faraday_middleware-multi_json (0.0.6)
faraday_middleware
multi_json
fast_gettext (1.4.0)
ffaker (2.4.0)
ffi (1.9.10)
flay (2.8.1)
......@@ -275,6 +276,16 @@ GEM
gemojione (3.0.1)
json
get_process_mem (0.2.0)
gettext (3.2.2)
locale (>= 2.0.5)
text (>= 1.3.0)
gettext_i18n_rails (1.8.0)
fast_gettext (>= 0.9.0)
gettext_i18n_rails_js (1.2.0)
gettext (>= 3.0.2)
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.5.0)
google-protobuf (~> 3.1)
......@@ -450,6 +461,7 @@ GEM
licensee (8.7.0)
rugged (~> 0.24)
little-plugger (1.1.4)
locale (2.1.2)
logging (2.1.0)
little-plugger (~> 1.1)
multi_json (~> 1.10)
......@@ -553,6 +565,8 @@ GEM
ast (~> 2.2)
path_expander (1.0.1)
pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
......@@ -805,6 +819,7 @@ GEM
temple (0.7.7)
test_after_commit (1.1.0)
activerecord (>= 3.2)
text (1.3.1)
thin (1.7.0)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
......@@ -937,6 +952,9 @@ DEPENDENCIES
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.5.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
......@@ -1030,6 +1048,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8.4)
rufus-scheduler (~> 3.1.10)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
......
# GitLab Contributing Process
## GitLab Core Team & GitLab Inc. Contribution Process
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Purpose of describing the contributing process](#purpose-of-describing-the-contributing-process)
- [Common actions](#common-actions)
- [Merge request coaching](#merge-request-coaching)
- [Assigning issues](#assigning-issues)
- [Be kind](#be-kind)
- [Feature freeze on the 7th for the release on the 22nd](#feature-freeze-on-the-7th-for-the-release-on-the-22nd)
- [Between the 1st and the 7th](#between-the-1st-and-the-7th)
- [On the 7th](#on-the-7th)
- [After the 7th](#after-the-7th)
- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
- [Retrospective](#retrospective)
- [Kickoff](#kickoff)
- [Copy & paste responses](#copy--paste-responses)
- [Improperly formatted issue](#improperly-formatted-issue)
- [Issue report for old version](#issue-report-for-old-version)
- [Support requests and configuration questions](#support-requests-and-configuration-questions)
- [Code format](#code-format)
- [Issue fixed in newer version](#issue-fixed-in-newer-version)
- [Improperly formatted merge request](#improperly-formatted-merge-request)
- [Inactivity close of an issue](#inactivity-close-of-an-issue)
- [Inactivity close of a merge request](#inactivity-close-of-a-merge-request)
- [Accepting merge requests](#accepting-merge-requests)
- [Only accepting merge requests with green tests](#only-accepting-merge-requests-with-green-tests)
- [Closing down the issue tracker on GitHub](#closing-down-the-issue-tracker-on-github)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
---
## Purpose of describing the contributing process
Below we describe the contributing process to GitLab for two reasons. So that
contributors know what to expect from maintainers (possible responses, friendly
treatment, etc.). And so that maintainers know what to expect from contributors
(use the latest version, ensure that the issue is addressed, friendly treatment,
etc.).
Below we describe the contributing process to GitLab for two reasons:
1. Contributors know what to expect from maintainers (possible responses, friendly
treatment, etc.)
1. Maintainers know what to expect from contributors (use the latest version,
ensure that the issue is addressed, friendly treatment, etc.).
- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
## Common actions
### Issue triaging
Our issue triage policies are [described in our handbook]. You are very welcome
to help the GitLab team triage issues. We also organize [issue bash events] once
every quarter.
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issues. Please select someone with relevant experience from
[GitLab team][team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
timely response. If the involvement of the lead developer is needed the other
core team members will mention this person.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get
......@@ -37,12 +55,6 @@ their contributions accepted by meeting our [Definition of done][done].
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
## Workflow labels
Labelling issues is described in the [GitLab Inc engineering workflow].
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
## Assigning issues
If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover.
......@@ -146,6 +158,29 @@ release should have the correct milestone assigned _and_ have the label
Merge requests without a milestone and this label will
not be merged into any stable branches.
## Release retrospective and kickoff
### Retrospective
After each release, we have a retrospective call where we discuss what went well,
what went wrong, and what we can improve for the next release. The
[retrospective notes] are public and you are invited to comment on them.
If you're interested, you can even join the
[retrospective call][retro-kickoff-call], on the first working day after the
22nd at 6pm CET / 9am PST.
### Kickoff
Before working on the next release, we have a
kickoff call to explain what we expect to ship in the next release. The
[kickoff notes] are public and you are invited to comment on them.
If you're interested, you can even join the [kickoff call][retro-kickoff-call],
on the first working day after the 7th at 6pm CET / 9am PST..
[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
## Copy & paste responses
### Improperly formatted issue
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() {
function Autosave(field, key) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (key.join != null) {
key = key.join("/");
}
......@@ -17,16 +20,12 @@ window.Autosave = (function() {
}
Autosave.prototype.restore = function() {
var e, text;
if (window.localStorage == null) {
return;
}
try {
text = window.localStorage.getItem(this.key);
} catch (error) {
e = error;
return;
}
var text;
if (!this.isLocalStorageAvailable) return;
text = window.localStorage.getItem(this.key);
if ((text != null ? text.length : void 0) > 0) {
this.field.val(text);
}
......@@ -35,27 +34,22 @@ window.Autosave = (function() {
Autosave.prototype.save = function() {
var text;
if (window.localStorage == null) {
return;
}
text = this.field.val();
if ((text != null ? text.length : void 0) > 0) {
try {
return window.localStorage.setItem(this.key, text);
} catch (error) {}
} else {
return this.reset();
if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
return window.localStorage.setItem(this.key, text);
}
return this.reset();
};
Autosave.prototype.reset = function() {
if (window.localStorage == null) {
return;
}
try {
return window.localStorage.removeItem(this.key);
} catch (error) {}
if (!this.isLocalStorageAvailable) return;
return window.localStorage.removeItem(this.key);
};
return Autosave;
})();
export default window.Autosave;
import AccessorUtilities from '../../lib/utils/accessor';
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
......@@ -140,16 +142,25 @@ function generateUnicodeSupportMap(testMap) {
function getUnicodeSupportMap() {
let unicodeSupportMap;
const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
let userAgentFromCache;
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
}
return unicodeSupportMap;
......
......@@ -43,8 +43,8 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
const $submitButton = $form.find('input[type=submit], button[type=submit]');
if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]);
$submitButton.disable();
$form.submit();
}
});
......
/* global Flash */
import sqljs from 'sql.js';
import { template as _template } from 'underscore';
const PREVIEW_TEMPLATE = _template(`
<div class="panel panel-default">
<div class="panel-heading"><%- name %></div>
<div class="panel-body">
<img class="img-thumbnail" src="data:image/png;base64,<%- image %>"/>
</div>
</div>
`);
class BalsamiqViewer {
constructor(viewer) {
this.viewer = viewer;
this.endpoint = this.viewer.dataset.endpoint;
}
loadFile() {
const xhr = new XMLHttpRequest();
xhr.open('GET', this.endpoint, true);
xhr.responseType = 'arraybuffer';
xhr.onload = this.renderFile.bind(this);
xhr.onerror = BalsamiqViewer.onError;
xhr.send();
}
renderFile(loadEvent) {
const container = document.createElement('ul');
this.initDatabase(loadEvent.target.response);
const previews = this.getPreviews();
previews.forEach((preview) => {
const renderedPreview = this.renderPreview(preview);
container.appendChild(renderedPreview);
});
container.classList.add('list-inline');
container.classList.add('previews');
this.viewer.appendChild(container);
}
initDatabase(data) {
const previewBinary = new Uint8Array(data);
this.database = new sqljs.Database(previewBinary);
}
getPreviews() {
const thumbnails = this.database.exec('SELECT * FROM thumbnails');
return thumbnails[0].values.map(BalsamiqViewer.parsePreview);
}
getResource(resourceID) {
const resources = this.database.exec(`SELECT * FROM resources WHERE id = '${resourceID}'`);
return resources[0];
}
renderPreview(preview) {
const previewElement = document.createElement('li');
previewElement.classList.add('preview');
previewElement.innerHTML = this.renderTemplate(preview);
return previewElement;
}
renderTemplate(preview) {
const resource = this.getResource(preview.resourceID);
const name = BalsamiqViewer.parseTitle(resource);
const image = preview.image;
const template = PREVIEW_TEMPLATE({
name,
image,
});
return template;
}
static parsePreview(preview) {
return JSON.parse(preview[1]);
}
/*
* resource = {
* columns: ['ID', 'BRANCHID', 'ATTRIBUTES', 'DATA'],
* values: [['id', 'branchId', 'attributes', 'data']],
* }
*
* 'attributes' being a JSON string containing the `name` property.
*/
static parseTitle(resource) {
return JSON.parse(resource.values[0][2]).name;
}
static onError() {
const flash = new Flash('Balsamiq file could not be loaded.');
return flash;
}
}
export default BalsamiqViewer;
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
document.addEventListener('DOMContentLoaded', () => {
const balsamiqViewer = new BalsamiqViewer(document.getElementById('js-balsamiq-viewer'));
balsamiqViewer.loadFile();
});
......@@ -54,7 +54,10 @@ export default class FileTemplateSelector {
reportSelection(options) {
const { query, e, data } = options;
<<<<<<< HEAD
=======
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
e.preventDefault();
return this.mediator.selectTemplateFile(this, query, data);
}
......
......@@ -64,7 +64,11 @@ $(() => {
rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail,
<<<<<<< HEAD
milestoneTitle: $boardApp.dataset.boardMilestoneTitle,
=======
defaultAvatar: $boardApp.dataset.defaultAvatar,
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
},
computed: {
detailIssueVisible () {
......@@ -98,7 +102,7 @@ $(() => {
gl.boardService.all()
.then((resp) => {
resp.json().forEach((board) => {
const list = Store.addList(board);
const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') {
list.position = Infinity;
......
......@@ -157,7 +157,11 @@ gl.issueBoards.IssueCardInner = Vue.extend({
>
<img
class="avatar avatar-inline s20"
<<<<<<< HEAD
:src="assignee.avatarUrl"
=======
:src="assignee.avatar"
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
width="20"
height="20"
:alt="avatarUrlTitle(assignee)"
......
......@@ -6,7 +6,7 @@
import Vue from 'vue';
class ListIssue {
constructor (obj) {
constructor (obj, defaultAvatar) {
this.globalId = obj.id;
this.id = obj.iid;
this.title = obj.title;
......@@ -17,7 +17,10 @@ class ListIssue {
this.assignees = [];
this.selected = false;
this.position = obj.relative_position || Infinity;
<<<<<<< HEAD
this.milestone_id = obj.milestone_id;
=======
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
......@@ -27,7 +30,11 @@ class ListIssue {
this.labels.push(new ListLabel(label));
});
<<<<<<< HEAD
this.assignees = obj.assignees.map(a => new ListAssignee(a));
=======
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
}
addLabel (label) {
......
......@@ -6,7 +6,7 @@ import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List {
constructor (obj) {
constructor (obj, defaultAvatar) {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
......@@ -18,6 +18,7 @@ class List {
this.loadingMore = false;
this.issues = [];
this.issuesSize = 0;
this.defaultAvatar = defaultAvatar;
if (obj.label) {
this.label = new ListLabel(obj.label);
......@@ -107,7 +108,7 @@ class List {
createIssues (data) {
data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj));
this.addIssue(new ListIssue(issueObj, this.defaultAvatar));
});
}
......
......@@ -23,6 +23,7 @@ gl.issueBoards.BoardsStore = {
this.state.lists = [];
this.filter.path = gl.utils.getUrlParamsArray().join('&');
},
<<<<<<< HEAD
createNewListDropdownData() {
this.state.currentBoard = {};
this.state.currentPage = '';
......@@ -30,6 +31,10 @@ gl.issueBoards.BoardsStore = {
},
addList (listObj) {
const list = new List(listObj);
=======
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
this.state.lists.push(list);
return list;
......
......@@ -9,9 +9,9 @@ export default {
<span v-if="count === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
aria-hidden="true"
title="Limited to showing 50 events at most"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
Showing 50 events
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
`,
};
......@@ -28,11 +28,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
{{ __('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
{{ __('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
......
......@@ -28,11 +28,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
{{ __('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
{{ __('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
......
......@@ -31,10 +31,10 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
</a>
</h5>
<span>
First
{{ __('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
pushed by
{{ __('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
......
......@@ -28,11 +28,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
{{ __('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
{{ __('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
......
......@@ -28,11 +28,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
{{ __('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
{{ __('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
......
......@@ -32,7 +32,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
by
{{ __('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
......
......@@ -12,10 +12,10 @@ global.cycleAnalytics.TotalTimeComponent = Vue.extend({
template: `
<span class="total-time">
<template v-if="Object.keys(time).length">
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
......
......@@ -2,6 +2,7 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate';
import LimitWarningComponent from './components/limit_warning_component';
require('./components/stage_code_component');
......@@ -16,6 +17,8 @@ require('./cycle_analytics_service');
require('./cycle_analytics_store');
require('./default_event_objects');
Vue.use(Translate);
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
......
......@@ -30,7 +30,7 @@ class CycleAnalyticsService {
startDate,
} = options;
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
return $.get(`${this.requestPath}/events/${stage.name}.json`, {
cycle_analytics: {
start_date: startDate,
},
......
/* eslint-disable no-param-reassign */
import { __ } from '../locale';
require('../lib/utils/text_utility');
const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
......@@ -7,13 +8,13 @@ const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = {
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
code: __('The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.'),
test: __('The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.'),
review: __('The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.'),
staging: __('The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.'),
production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
};
global.cycleAnalytics.CycleAnalyticsStore = {
......@@ -38,7 +39,7 @@ global.cycleAnalytics.CycleAnalyticsStore = {
});
newData.stages.forEach((item) => {
const stageSlug = gl.text.dasherize(item.title.toLowerCase());
const stageSlug = gl.text.dasherize(item.name.toLowerCase());
item.active = false;
item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
......
<script>
import eventHub from '../eventhub';
export default {
data() {
return {
isLoading: false,
};
},
props: {
deployKey: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
},
methods: {
doAction() {
this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey);
},
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
};
</script>
<template>
<button
class="btn btn-sm prepend-left-10"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
{{ text }}
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="Loading">
</i>
</button>
</template>
<script>
/* global Flash */
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import keysPanel from './keys_panel.vue';
export default {
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
},
props: {
endpoint: {
type: String,
required: true,
},
},
computed: {
hasKeys() {
return Object.keys(this.keys).length;
},
keys() {
return this.store.keys;
},
},
components: {
keysPanel,
},
methods: {
fetchKeys() {
this.isLoading = true;
this.service.getKeys()
.then((data) => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => new Flash('Error getting deploy keys'));
},
enableKey(deployKey) {
this.service.enableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key'));
},
disableKey(deployKey) {
// eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error removing deploy key'));
}
},
},
created() {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
};
</script>
<template>
<div class="col-lg-9 col-lg-offset-3 append-bottom-default deploy-keys">
<div
class="text-center"
v-if="isLoading && !hasKeys">
<i
class="fa fa-spinner fa-spin fa-2x"
aria-hidden="true"
aria-label="Loading deploy keys">
</i>
</div>
<div v-else-if="hasKeys">
<keys-panel
title="Enabled deploy keys for this project"
:keys="keys.enabled_keys"
:store="store" />
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store" />
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store" />
</div>
</div>
</template>
<script>
import actionBtn from './action_btn.vue';
export default {
props: {
deployKey: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
},
components: {
actionBtn,
},
computed: {
timeagoDate() {
return gl.utils.getTimeago().format(this.deployKey.created_at);
},
},
methods: {
isEnabled(id) {
return this.store.findEnabledKey(id) !== undefined;
},
},
};
</script>
<template>
<div>
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon">
</i>
</div>
<div class="deploy-key-content key-list-item-info">
<strong class="title">
{{ deployKey.title }}
</strong>
<div class="description">
{{ deployKey.fingerprint }}
</div>
<div
v-if="deployKey.can_push"
class="write-access-allowed">
Write access allowed
</div>
</div>
<div class="deploy-key-content prepend-left-default deploy-key-projects">
<a
v-for="project in deployKey.projects"
class="label deploy-project-label"
:href="project.full_path">
{{ project.full_name }}
</a>
</div>
<div class="deploy-key-content">
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<action-btn
v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey"
type="enable"/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove" />
<action-btn
v-else
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable" />
</div>
</div>
</template>
<script>
import key from './key.vue';
export default {
props: {
title: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: {
type: Object,
required: true,
},
},
components: {
key,
},
};
</script>
<template>
<div class="deploy-keys-panel">
<h5>
{{ title }}
({{ keys.length }})
</h5>
<ul class="well-list"
v-if="keys.length">
<li
v-for="deployKey in keys"
:key="deployKey.id">
<key
:deploy-key="deployKey"
:store="store" />
</li>
</ul>
<div
class="settings-message text-center"
v-else-if="showHelpBox">
No deploy keys found. Create one with the form above.
</div>
</div>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: document.getElementById('js-deploy-keys'),
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
};
},
components: {
deployKeysApp,
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
},
});
},
}));
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class DeployKeysService {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
});
}
getKeys() {
return this.resource.get()
.then(response => response.json());
}
enableKey(id) {
return this.resource.enable({ id }, {});
}
disableKey(id) {
return this.resource.disable({ id }, {});
}
}
export default class DeployKeysStore {
constructor() {
this.keys = {};
}
findEnabledKey(id) {
return this.keys.enabled_keys.find(key => key.id === id);
}
}
......@@ -53,8 +53,12 @@ import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import BlobViewer from './blob/viewer/index';
<<<<<<< HEAD
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
=======
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -204,6 +208,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
new AutoWidthDropdownSelect($('.js-target-branch-select')).init();
break;
case 'projects:tags:new':
new ZenMode();
......@@ -263,6 +268,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
}
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
......@@ -357,6 +363,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
case 'projects:artifacts:file':
new BlobViewer();
break;
case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break;
......
......@@ -3,11 +3,14 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
// Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
export {
DATA_TRIGGER,
DATA_DROPDOWN,
SELECTED_CLASS,
ACTIVE_CLASS,
TEMPLATE_REGEX,
IGNORE_CLASS,
};
......@@ -94,7 +94,7 @@ Object.assign(DropDown.prototype, {
},
renderChildren: function(data) {
var html = utils.t(this.templateString, data);
var html = utils.template(this.templateString, data);
var template = document.createElement('div');
template.innerHTML = html;
......
/* eslint-disable */
import { DATA_TRIGGER, DATA_DROPDOWN } from './constants';
import { template as _template } from 'underscore';
import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants';
const utils = {
toCamelCase(attr) {
return this.camelize(attr.split('-').slice(1).join(' '));
},
t(s, d) {
for (const p in d) {
if (Object.prototype.hasOwnProperty.call(d, p)) {
s = s.replace(new RegExp(`{{${p}}}`, 'g'), d[p]);
}
}
return s;
template(templateString, data) {
const template = _template(templateString, {
escape: TEMPLATE_REGEX,
});
return template(data);
},
camelize(str) {
......
......@@ -8,6 +8,11 @@ export default {
type: Array,
required: true,
},
isLocalStorageAvailable: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
......@@ -47,7 +52,12 @@ export default {
template: `
<div>
<ul v-if="hasItems">
<div
v-if="!isLocalStorageAvailable"
class="dropdown-info-note">
This feature requires local storage to be enabled
</div>
<ul v-else-if="hasItems">
<li
v-for="(item, index) in processedItems"
:key="index">
......
......@@ -62,7 +62,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
Object.assign({
icon: `fa-${icon}`,
hint,
tag: `&lt;${tag}&gt;`,
tag: `<${tag}>`,
}, type && { type }),
);
}
......
/* global Flash */
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
......@@ -15,11 +13,17 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
<<<<<<< HEAD
if (page === 'issues' || page === 'boards') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysWithWeights;
}
this.recentSearchesStore = new RecentSearchesStore();
=======
this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
});
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
let recentSearchesKey = 'issue-recent-searches';
if (page === 'merge_requests') {
recentSearchesKey = 'merge-request-recent-searches';
......@@ -28,9 +32,10 @@ class FilteredSearchManager {
// Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
.catch(() => {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
new Flash('An error occured while parsing recent searches');
new window.Flash('An error occured while parsing recent searches');
// Gracefully fail to empty array
return [];
})
......
import AjaxCache from '~/lib/utils/ajax_cache';
import '~/flash'; /* global Flash */
import FilteredSearchContainer from './container';
class FilteredSearchVisualTokens {
......@@ -48,6 +50,40 @@ class FilteredSearchVisualTokens {
`;
}
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
const labelsEndpoint = `${baseEndpoint}/labels.json`;
return AjaxCache.retrieve(labelsEndpoint)
.then((labels) => {
const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue);
if (!matchingLabel) {
return;
}
const tokenValueStyle = tokenValueContainer.style;
tokenValueStyle.backgroundColor = matchingLabel.color;
tokenValueStyle.color = matchingLabel.text_color;
if (matchingLabel.text_color === '#FFFFFF') {
const removeToken = tokenValueContainer.querySelector('.remove-token');
removeToken.classList.add('inverted');
}
})
.catch(() => new Flash('An error occurred while fetching label colors.'));
}
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
tokenValueContainer.querySelector('.value').innerText = tokenValue;
if (tokenName.toLowerCase() === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
}
}
static addVisualTokenElement(name, value, isSearchTerm) {
const li = document.createElement('li');
li.classList.add('js-visual-token');
......@@ -55,7 +91,7 @@ class FilteredSearchVisualTokens {
if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
li.querySelector('.value').innerText = value;
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
} else {
li.innerHTML = '<div class="name"></div>';
}
......@@ -74,7 +110,7 @@ class FilteredSearchVisualTokens {
const name = FilteredSearchVisualTokens.getLastTokenPartial();
lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
lastVisualToken.querySelector('.name').innerText = name;
lastVisualToken.querySelector('.value').innerText = value;
FilteredSearchVisualTokens.renderVisualTokenValue(lastVisualToken, name, value);
}
}
......@@ -183,6 +219,9 @@ class FilteredSearchVisualTokens {
static moveInputToTheRight() {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
if (!input) return;
const inputLi = input.parentElement;
const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
......
......@@ -29,12 +29,15 @@ class RecentSearchesRoot {
}
render() {
const state = this.store.state;
this.vm = new Vue({
el: this.wrapperElement,
data: this.store.state,
data() { return state; },
template: `
<recent-searches-dropdown-content
:items="recentSearches" />
:items="recentSearches"
:is-local-storage-available="isLocalStorageAvailable"
/>
`,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
......
import RecentSearchesServiceError from './recent_searches_service_error';
import AccessorUtilities from '../../lib/utils/accessor';
class RecentSearchesService {
constructor(localStorageKey = 'issuable-recent-searches') {
this.localStorageKey = localStorageKey;
}
fetch() {
if (!RecentSearchesService.isAvailable()) {
const error = new RecentSearchesServiceError();
return Promise.reject(error);
}
const input = window.localStorage.getItem(this.localStorageKey);
let searches = [];
......@@ -19,8 +27,14 @@ class RecentSearchesService {
}
save(searches = []) {
if (!RecentSearchesService.isAvailable()) return;
window.localStorage.setItem(this.localStorageKey, JSON.stringify(searches));
}
static isAvailable() {
return AccessorUtilities.isLocalStorageAccessSafe();
}
}
export default RecentSearchesService;
class RecentSearchesServiceError {
constructor(message) {
this.name = 'RecentSearchesServiceError';
this.message = message || 'Recent Searches Service is unavailable';
}
}
// Can't use `extends` for builtin prototypes and get true inheritance yet
RecentSearchesServiceError.prototype = Error.prototype;
export default RecentSearchesServiceError;
......@@ -101,9 +101,17 @@ window.gl.GfmAutoComplete = {
}
}
},
setup: function(input) {
setup: function(input, enableMap = {
emojis: true,
members: true,
issues: true,
milestones: true,
mergeRequests: true,
labels: true
}) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
this.enableMap = enableMap;
this.setupLifecycle();
},
setupLifecycle() {
......@@ -115,7 +123,84 @@ window.gl.GfmAutoComplete = {
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
});
},
setupAtWho: function($input) {
if (this.enableMap.emojis) this.setupEmoji($input);
if (this.enableMap.members) this.setupMembers($input);
if (this.enableMap.issues) this.setupIssues($input);
if (this.enableMap.milestones) this.setupMilestones($input);
if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
if (this.enableMap.labels) this.setupLabels($input);
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
displayTpl: function(value) {
if (this.isLoading(value)) return this.Loading.template;
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
}
if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>';
}
if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>';
}
tpl += '</li>';
return _.template(tpl)(value);
}.bind(this),
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
if (value.params.length > 0) {
reference_prefix = value.params[0][0];
if (/^[@%~]/.test(reference_prefix)) {
tpl += '<%- reference_prefix %>';
}
}
return _.template(tpl)({ reference_prefix: reference_prefix });
},
suffix: '',
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(commands) {
if (gl.GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, function(c) {
var search = c.name;
if (c.aliases.length > 0) {
search = search + " " + c.aliases.join(" ");
}
return {
name: c.name,
aliases: c.aliases,
params: c.params,
description: c.description,
search: search
};
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
var match = regexp.exec(subtext);
if (match) {
return match[1];
} else {
return null;
}
}
}
});
return;
},
setupEmoji($input) {
// Emoji
$input.atwho({
at: ':',
......@@ -139,6 +224,9 @@ window.gl.GfmAutoComplete = {
}
}
});
},
setupMembers($input) {
// Team Members
$input.atwho({
at: '@',
......@@ -180,6 +268,9 @@ window.gl.GfmAutoComplete = {
}
}
});
},
setupIssues($input) {
$input.atwho({
at: '#',
alias: 'issues',
......@@ -208,6 +299,9 @@ window.gl.GfmAutoComplete = {
}
}
});
},
setupMilestones($input) {
$input.atwho({
at: '%',
alias: 'milestones',
......@@ -236,6 +330,9 @@ window.gl.GfmAutoComplete = {
}
}
});
},
setupMergeRequests($input) {
$input.atwho({
at: '!',
alias: 'mergerequests',
......@@ -264,6 +361,9 @@ window.gl.GfmAutoComplete = {
}
}
});
},
setupLabels($input) {
$input.atwho({
at: '~',
alias: 'labels',
......@@ -298,73 +398,8 @@ window.gl.GfmAutoComplete = {
}
}
});
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
displayTpl: function(value) {
if (this.isLoading(value)) return this.Loading.template;
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
}
if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>';
}
if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>';
}
tpl += '</li>';
return _.template(tpl)(value);
}.bind(this),
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
if (value.params.length > 0) {
reference_prefix = value.params[0][0];
if (/^[@%~]/.test(reference_prefix)) {
tpl += '<%- reference_prefix %>';
}
}
return _.template(tpl)({ reference_prefix: reference_prefix });
},
suffix: '',
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(commands) {
if (gl.GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, function(c) {
var search = c.name;
if (c.aliases.length > 0) {
search = search + " " + c.aliases.join(" ");
}
return {
name: c.name,
aliases: c.aliases,
params: c.params,
description: c.description,
search: search
};
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
var match = regexp.exec(subtext);
if (match) {
return match[1];
} else {
return null;
}
}
}
});
return;
},
fetchData: function($input, at) {
if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true;
......
let instanceCount = 0;
class AutoWidthDropdownSelect {
constructor(selectElement) {
this.$selectElement = $(selectElement);
this.dropdownClass = `js-auto-width-select-dropdown-${instanceCount}`;
instanceCount += 1;
}
init() {
const dropdownClass = this.dropdownClass;
this.$selectElement.select2({
dropdownCssClass: dropdownClass,
dropdownCss() {
let resultantWidth = 'auto';
const $dropdown = $(`.${dropdownClass}`);
// We have to look at the parent because
// `offsetParent` on a `display: none;` is `null`
const offsetParentWidth = $(this).parent().offsetParent().width();
// Reset any width to let it naturally flow
$dropdown.css('width', 'auto');
if ($dropdown.outerWidth(false) > offsetParentWidth) {
resultantWidth = offsetParentWidth;
}
return {
width: resultantWidth,
maxWidth: offsetParentWidth,
};
},
});
return this;
}
}
export default AutoWidthDropdownSelect;
function isPropertyAccessSafe(base, property) {
let safe;
try {
safe = !!base[property];
} catch (error) {
safe = false;
}
return safe;
}
function isFunctionCallSafe(base, functionName, ...args) {
let safe = true;
try {
base[functionName](...args);
} catch (error) {
safe = false;
}
return safe;
}
function isLocalStorageAccessSafe() {
let safe;
const TEST_KEY = 'isLocalStorageAccessSafe';
const TEST_VALUE = 'true';
safe = isPropertyAccessSafe(window, 'localStorage');
if (!safe) return safe;
safe = isFunctionCallSafe(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE);
if (safe) window.localStorage.removeItem(TEST_KEY);
return safe;
}
const AccessorUtilities = {
isPropertyAccessSafe,
isFunctionCallSafe,
isLocalStorageAccessSafe,
};
export default AccessorUtilities;
const AjaxCache = {
internalStorage: { },
get(endpoint) {
return this.internalStorage[endpoint];
},
hasData(endpoint) {
return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint);
},
purge(endpoint) {
delete this.internalStorage[endpoint];
},
retrieve(endpoint) {
if (AjaxCache.hasData(endpoint)) {
return Promise.resolve(AjaxCache.get(endpoint));
}
return new Promise((resolve, reject) => {
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return
.then(data => resolve(data),
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${endpoint}: ${errorThrown}`);
error.textStatus = textStatus;
reject(error);
},
);
})
.then((data) => { this.internalStorage[endpoint] = data; })
.then(() => AjaxCache.get(endpoint));
},
};
export default AjaxCache;
......@@ -35,6 +35,14 @@
});
};
w.gl.utils.ajaxPost = function(url, data) {
return $.ajax({
type: 'POST',
url: url,
data: data,
});
};
w.gl.utils.extractLast = function(term) {
return this.split(term).pop();
};
......
var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:37-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":[""],"Commit":["",""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Showing %d event":["",""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file
var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":[""],"Commit":["",""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Showing %d event":["",""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file
var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":["por"],"Commit":["Cambio","Cambios"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"Last %d day":["Último %d día","Últimos %d días"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Leer más"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de codificación muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"You need permission.":["Necesitas permisos."],"day":["día","días"]}}};
\ No newline at end of file
import Jed from 'jed';
/**
This is required to require all the translation folders in the current directory
this saves us having to do this manually & keep up to date with new languages
**/
function requireAll(requireContext) { return requireContext.keys().map(requireContext); }
const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/));
const locales = allLocales.reduce((d, obj) => {
const data = d;
const localeKey = Object.keys(obj)[0];
data[localeKey] = obj[localeKey];
return data;
}, {});
let lang = document.querySelector('html').getAttribute('lang') || 'en';
lang = lang.replace(/-/g, '_');
const locale = new Jed(locales[lang]);
/**
Translates `text`
@param text The text to be translated
@returns {String} The translated text
**/
const gettext = locale.gettext.bind(locale);
/**
Translate the text with a number
if the number is more than 1 it will use the `pluralText` translation.
This method allows for contexts, see below re. contexts
@param text Singular text to translate (eg. '%d day')
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
**/
const ngettext = (text, pluralText, count) => {
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
return translated[translated.length - 1];
};
/**
Translate context based text
Either pass in the context translation like `Context|Text to translate`
or allow for dynamic text by doing passing in the context first & then the text to translate
@param keyOrContext Can be either the key to translate including the context
(eg. 'Context|Text') or just the context for the translation
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
**/
const pgettext = (keyOrContext, key) => {
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
const translated = gettext(normalizedKey).split('|');
return translated[translated.length - 1];
};
export { lang };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
export default locale;
......@@ -44,6 +44,7 @@
return $el.text();
},
clicked: (options) => {
<<<<<<< HEAD
const $link = options.$el;
if (!$link.data('revert')) {
......@@ -59,6 +60,9 @@
$dateInput.enable();
});
}
=======
this.formSubmit(null, options.$el);
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
},
});
});
......
......@@ -313,7 +313,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
MergeRequestWidget.prototype.updateCommitUrls = function(id) {
const commitsUrl = this.opts.commits_path;
$('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
$('.js-commit-link').text(id).attr('href', [commitsUrl, id].join('/'));
};
MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
......
......@@ -126,6 +126,7 @@
return $value.css('display', '');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
<<<<<<< HEAD
hideRow: function(milestone) {
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
!$dropdown.closest('.add-issues-modal').length && gl.issueBoards.BoardsStore.state.currentBoard.milestone) {
......@@ -142,6 +143,8 @@
return true;
},
=======
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
clicked: function(options) {
const { $el, e } = options;
let selected = options.selectedObj;
......
......@@ -34,6 +34,7 @@
filterByText: true,
remote: false,
fieldName: $branchSelect.data('field-name'),
filterInput: 'input[type="search"]',
selectable: true,
isSelectable: function(branch, $el) {
return !$el.hasClass('is-active');
......@@ -50,6 +51,21 @@
}
}
});
const $dropdownContainer = $branchSelect.closest('.dropdown');
const $fieldInput = $(`input[name="${$branchSelect.data('field-name')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
$fieldInput.val(text);
$('.dropdown-toggle-text', $branchSelect).text(text);
$dropdownContainer.removeClass('open');
});
};
NewBranchForm.prototype.setupRestrictions = function() {
......
This diff is collapsed.
......@@ -40,6 +40,6 @@ export default class PipelinesService {
* @return {Promise}
*/
postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true });
return Vue.http.post(`${endpoint}.json`);
}
}
......@@ -54,8 +54,12 @@
}
},
clicked(opts) {
<<<<<<< HEAD
const { $el, e } = opts;
const item = opts.selectedObj;
=======
const { e } = opts;
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
e.preventDefault();
......
import RavenConfig from './raven_config';
const index = function index() {
RavenConfig.init({
sentryDsn: gon.sentry_dsn,
currentUserId: gon.current_user_id,
whitelistUrls: [gon.gitlab_url],
isProduction: process.env.NODE_ENV,
});
return RavenConfig;
};
index();
export default index;
import Raven from 'raven-js';
const IGNORE_ERRORS = [
// Random plugins/extensions
'top.GLOBALS',
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
'Can\'t find variable: ZiteReader',
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
// reduce this. (thanks @acdha)
// See http://stackoverflow.com/questions/4113268
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
];
const IGNORE_URLS = [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
/connect\.facebook\.net\/en_US\/all\.js/i,
// Woopra flakiness
/eatdifferent\.com\.woopra-ns\.com/i,
/static\.woopra\.com\/js\/woopra\.js/i,
// Chrome extensions
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
];
const SAMPLE_RATE = 95;
const RavenConfig = {
IGNORE_ERRORS,
IGNORE_URLS,
SAMPLE_RATE,
init(options = {}) {
this.options = options;
this.configure();
this.bindRavenErrors();
if (this.options.currentUserId) this.setUser();
},
configure() {
Raven.config(this.options.sentryDsn, {
whitelistUrls: this.options.whitelistUrls,
environment: this.options.isProduction ? 'production' : 'development',
ignoreErrors: this.IGNORE_ERRORS,
ignoreUrls: this.IGNORE_URLS,
shouldSendCallback: this.shouldSendSample.bind(this),
}).install();
},
setUser() {
Raven.setUserContext({
id: this.options.currentUserId,
});
},
bindRavenErrors() {
window.$(document).on('ajaxError.raven', this.handleRavenErrors);
},
handleRavenErrors(event, req, config, err) {
const error = err || req.statusText;
const responseText = req.responseText || 'Unknown response text';
Raven.captureMessage(error, {
extra: {
type: config.type,
url: config.url,
data: config.data,
status: req.status,
response: responseText,
error,
event,
},
});
},
shouldSendSample() {
return Math.random() * 100 <= this.SAMPLE_RATE;
},
};
export default RavenConfig;
......@@ -84,7 +84,11 @@ export default {
return !this.showLess || (index < this.defaultRenderCount && this.showLess);
},
avatarUrl(user) {
<<<<<<< HEAD
return user.avatarUrl || user.avatar_url;
=======
return user.avatar || user.avatar_url;
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
},
assigneeUrl(user) {
return `${this.rootPath}${user.username}`;
......
......@@ -33,12 +33,23 @@ export default {
saveAssignees() {
this.loading = true;
<<<<<<< HEAD
this.mediator.saveAssignees(this.field)
.then(() => {
this.loading = false;
})
.catch(() => {
this.loading = false;
=======
function setLoadingFalse() {
this.loading = false;
}
this.mediator.saveAssignees(this.field)
.then(setLoadingFalse.bind(this))
.catch(() => {
setLoadingFalse();
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
return new Flash('Error occurred when saving assignees');
});
},
......
......@@ -17,6 +17,7 @@ export default {
},
methods: {
listenForSlashCommands() {
<<<<<<< HEAD
$(document).on('ajax:success', '.gfm-form', (e, data) => {
const subscribedCommands = ['spend_time', 'time_estimate'];
const changedCommands = data.commands_changes
......@@ -26,6 +27,23 @@ export default {
this.mediator.fetch();
}
});
=======
$(document).on('ajax:success', '.gfm-form', this.slashCommandListened);
},
slashCommandListened(e, data) {
const subscribedCommands = ['spend_time', 'time_estimate'];
let changedCommands;
if (data !== undefined) {
changedCommands = data.commands_changes
? Object.keys(data.commands_changes)
: [];
} else {
changedCommands = [];
}
if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
this.mediator.fetch();
}
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
},
},
mounted() {
......
......@@ -4,7 +4,11 @@ import sidebarAssignees from './components/assignees/sidebar_assignees';
import Mediator from './sidebar_mediator';
<<<<<<< HEAD
document.addEventListener('DOMContentLoaded', () => {
=======
function domContentLoaded() {
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
const mediator = new Mediator(gl.sidebarOptions);
mediator.fetch();
......@@ -17,5 +21,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
<<<<<<< HEAD
});
=======
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default domContentLoaded;
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
......@@ -30,8 +30,13 @@ export default class SidebarMediator {
this.service.get()
.then((response) => {
const data = response.json();
<<<<<<< HEAD
this.store.processAssigneeData(data);
this.store.processTimeTrackingData(data);
=======
this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data);
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
})
.catch(() => new Flash('Error occured when fetching sidebar data'));
}
......
......@@ -17,13 +17,21 @@ export default class SidebarStore {
return SidebarStore.singleton;
}
<<<<<<< HEAD
processAssigneeData(data) {
=======
setAssigneeData(data) {
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
if (data.assignees) {
this.assignees = data.assignees;
}
}
<<<<<<< HEAD
processTimeTrackingData(data) {
=======
setTimeTrackingData(data) {
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent;
this.humanTimeEstimate = data.human_time_estimate;
......
/* eslint no-param-reassign: ["error", { "props": false }]*/
/* eslint no-new: "off" */
import AccessorUtilities from './lib/utils/accessor';
((global) => {
/**
* Memorize the last selected tab after reloading a page.
......@@ -9,6 +11,8 @@
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.bootstrap();
}
......@@ -37,11 +41,15 @@
}
saveData(val) {
localStorage.setItem(this.currentTabKey, val);
if (!this.isLocalStorageAvailable) return undefined;
return window.localStorage.setItem(this.currentTabKey, val);
}
readData() {
return localStorage.getItem(this.currentTabKey);
if (!this.isLocalStorageAvailable) return null;
return window.localStorage.getItem(this.currentTabKey);
}
}
......
......@@ -397,10 +397,12 @@ import eventHub from './sidebar/event_hub';
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (user.id !== selectedId);
selectedId = isSelecting ? user.id : selectedIdDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
const isSelecting = (user.id !== selectedId);
selectedId = isSelecting ? user.id : selectedIdDefault;
if (selectedId === gon.current_user_id) {
$('.assign-to-me-link').hide();
} else {
......
import {
__,
n__,
s__,
} from '../locale';
export default (Vue) => {
Vue.mixin({
methods: {
/**
Translates `text`
@param text The text to be translated
@returns {String} The translated text
**/
__,
/**
Translate the text with a number
if the number is more than 1 it will use the `pluralText` translation.
This method allows for contexts, see below re. contexts
@param text Singular text to translate (eg. '%d day')
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
**/
n__,
/**
Translate context based text
Either pass in the context translation like `Context|Text to translate`
or allow for dynamic text by doing passing in the context first & then the text to translate
@param keyOrContext Can be either the key to translate including the context
(eg. 'Context|Text') or just the context for the translation
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
**/
s__,
},
});
};
......@@ -159,3 +159,31 @@ a {
.fade-in {
animation: fadeIn $fade-in-duration 1;
}
@keyframes fadeInHalf {
0% {
opacity: 0;
}
100% {
opacity: 0.5;
}
}
.fade-in-half {
animation: fadeInHalf $fade-in-duration 1;
}
@keyframes fadeInFull {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.fade-in-full {
animation: fadeInFull $fade-in-duration 1;
}
......@@ -162,6 +162,18 @@
&.code {
padding: 0;
}
.list-inline.previews {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: flex-start;
align-items: baseline;
.preview {
padding: $gl-padding;
}
}
}
}
......
......@@ -114,11 +114,21 @@
padding-right: 8px;
.fa-close {
color: $gl-text-color-disabled;
color: $gl-text-color-secondary;
}
&:hover .fa-close {
color: $gl-text-color-secondary;
color: $gl-text-color;
}
&.inverted {
.fa-close {
color: $gl-text-color-secondary-inverted;
}
&:hover .fa-close {
color: $gl-text-color-inverted;
}
}
}
......
......@@ -101,6 +101,8 @@ $gl-font-size: 14px;
$gl-text-color: rgba(0, 0, 0, .85);
$gl-text-color-secondary: rgba(0, 0, 0, .55);
$gl-text-color-disabled: rgba(0, 0, 0, .35);
$gl-text-color-inverted: rgba(255, 255, 255, 1.0);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600;
$gl-text-red: $red-500;
$gl-text-orange: $orange-600;
......
......@@ -284,10 +284,18 @@
.avatar-counter {
display: none;
vertical-align: middle;
<<<<<<< HEAD
line-height: 18px;
height: 20px;
padding-left: 3px;
padding-right: 3px;
=======
min-width: 20px;
line-height: 19px;
height: 20px;
padding-left: 2px;
padding-right: 2px;
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
border-radius: 2em;
}
......
......@@ -3,6 +3,25 @@
margin: 24px auto 0;
position: relative;
.landing {
margin-top: 10px;
.inner-content {
white-space: normal;
h4,
p {
margin: 7px 0 0;
max-width: 480px;
padding: 0 $gl-padding;
@media (max-width: $screen-sm-min) {
margin: 0 auto;
}
}
}
}
.col-headers {
ul {
margin: 0;
......@@ -175,7 +194,7 @@
}
.stage-nav-item {
display: block;
display: flex;
line-height: 65px;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
......@@ -209,14 +228,10 @@
}
.stage-nav-item-cell {
float: left;
&.stage-name {
width: 65%;
}
&.stage-median {
width: 35%;
margin-left: auto;
margin-right: $gl-padding;
min-width: calc(35% - #{$gl-padding});
}
}
......
......@@ -450,6 +450,10 @@
margin: -5px;
}
<<<<<<< HEAD
=======
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
.user-list {
display: flex;
flex-wrap: wrap;
......
......@@ -116,7 +116,7 @@
}
.manage-labels-list {
> li:not(.empty-message) {
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
cursor: move;
cursor: -webkit-grab;
......
......@@ -482,6 +482,10 @@
}
}
.target-branch-select-dropdown-container {
position: relative;
}
.assign-to-me-link {
padding-left: 12px;
white-space: nowrap;
......
......@@ -57,6 +57,25 @@ ul.notes {
position: relative;
border-bottom: 1px solid $white-normal;
&.being-posted {
pointer-events: none;
opacity: 0.5;
.dummy-avatar {
display: inline-block;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%);
}
.note-headline-light,
.fa-spinner {
margin-left: 3px;
}
}
&.note-discussion {
&.timeline-entry {
padding: 14px 10px;
......@@ -111,12 +130,6 @@ ul.notes {
}
.note-header {
padding-bottom: 8px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@media (max-width: $screen-xs-min) {
.inline {
......@@ -365,10 +378,15 @@ ul.notes {
.note-header {
display: flex;
justify-content: space-between;
@media (max-width: $screen-xs-max) {
flex-flow: row wrap;
}
}
.note-header-info {
min-width: 0;
padding-bottom: 5px;
}
.note-headline-light {
......@@ -416,6 +434,11 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
@media (max-width: $screen-xs-max) {
float: none;
margin-left: 0;
}
.note-action-button {
margin-left: 8px;
}
......@@ -687,6 +710,10 @@ ul.notes {
}
}
.discussion-notes .flash-container {
margin-bottom: 0;
}
// Merge request notes in diffs
.diff-file {
// Diff is side by side
......
......@@ -277,6 +277,7 @@
.stage-container {
display: inline-block;
position: relative;
vertical-align: middle;
height: 22px;
margin: 3px 6px 3px 0;
......@@ -320,6 +321,32 @@
}
}
.build-failures {
.build-state {
padding: 20px 2px;
.build-name {
float: right;
font-weight: 500;
}
.ci-status-icon-failed svg {
vertical-align: middle;
}
.stage {
color: $gl-text-color-secondary;
font-weight: 500;
vertical-align: middle;
}
}
.build-log {
border: none;
line-height: initial;
}
}
// Pipeline graph
.pipeline-graph {
width: 100%;
......
......@@ -54,8 +54,9 @@
background-color: $white-light;
&:hover {
border-color: $white-dark;
border-color: $white-normal;
background-color: $gray-light;
border-top: 1px solid transparent;
.todo-avatar,
.todo-item {
......
......@@ -134,6 +134,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:signup_enabled,
:sentry_dsn,
:sentry_enabled,
:clientside_sentry_dsn,
:clientside_sentry_enabled,
:send_user_confirmation_email,
:shared_runners_enabled,
:shared_runners_text,
......
......@@ -16,6 +16,8 @@ class Admin::ServicesController < Admin::ApplicationController
def update
if service.update_attributes(service_params[:service])
PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
else
......
......@@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
around_action :set_locale
protect_from_forgery with: :exception
helper_method :can?, :current_application_settings
......@@ -273,4 +275,12 @@ class ApplicationController < ActionController::Base
def u2f_app_id
request.base_url
end
def set_locale
Gitlab::I18n.set_locale(current_user)
yield
ensure
Gitlab::I18n.reset_locale
end
end
......@@ -3,7 +3,7 @@ class Dashboard::LabelsController < Dashboard::ApplicationController
labels = LabelsFinder.new(current_user).execute
respond_to do |format|
format.json { render json: labels.as_json(only: [:id, :title, :color]) }
format.json { render json: LabelSerializer.new.represent_appearance(labels) }
end
end
end
......@@ -15,7 +15,7 @@ class Groups::LabelsController < Groups::ApplicationController
format.json do
available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
render json: available_labels.as_json(only: [:id, :title, :color])
render json: LabelSerializer.new.represent_appearance(available_labels)
end
end
end
......
......@@ -67,7 +67,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def omniauth_error
@provider = params[:provider]
@error = params[:error]
render 'errors/omniauth_error', layout: "errors", status: 422
render 'errors/omniauth_error', layout: "oauth_error", status: 422
end
def cas3
......
......@@ -85,7 +85,8 @@ class ProfilesController < Profiles::ApplicationController
:twitter,
:username,
:website_url,
:organization
:organization,
:preferred_language
)
end
end
class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath
include RendersBlob
layout 'project'
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
before_action :validate_artifacts!
before_action :set_path_and_entry, only: [:file, :raw]
def download
if artifacts_file.file_storage?
......@@ -24,15 +26,24 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def file
entry = build.artifacts_metadata_entry(params[:path])
blob = @entry.blob
override_max_blob_size(blob)
if entry.exists?
send_artifacts_entry(build, entry)
else
render_404
respond_to do |format|
format.html do
render 'file'
end
format.json do
render_blob_json(blob)
end
end
end
def raw
send_artifacts_entry(build, @entry)
end
def keep
build.keep_artifacts!
redirect_to namespace_project_build_path(project.namespace, project, build)
......@@ -81,4 +92,11 @@ class Projects::ArtifactsController < Projects::ApplicationController
def artifacts_file
@artifacts_file ||= build.artifacts_file
end
def set_path_and_entry
@path = params[:path]
@entry = build.artifacts_metadata_entry(@path)
render_404 unless @entry.exists?
end
end
......@@ -8,7 +8,12 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout "project_settings"
def index
redirect_to_repository_settings(@project)
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.json do
render json: Projects::Settings::DeployKeysPresenter.new(@project, current_user: current_user).as_json
end
end
end
def new
......@@ -20,8 +25,11 @@ class Projects::DeployKeysController < Projects::ApplicationController
unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
<<<<<<< HEAD
else
log_audit_event(@key.title, action: :create)
=======
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
end
redirect_to_repository_settings(@project)
end
......@@ -31,7 +39,10 @@ class Projects::DeployKeysController < Projects::ApplicationController
Projects::EnableDeployKeyService.new(@project, current_user, params).execute
log_audit_event(@key.title, action: :create)
redirect_to_repository_settings(@project)
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.json { head :ok }
end
end
def disable
......@@ -40,9 +51,17 @@ class Projects::DeployKeysController < Projects::ApplicationController
load_key
deploy_key_project.destroy!
<<<<<<< HEAD
log_audit_event(@key.title, action: :destroy)
redirect_to_repository_settings(@project)
=======
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.json { head :ok }
end
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
end
protected
......
......@@ -291,7 +291,11 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
<<<<<<< HEAD
:title, :position, :description, :confidential, :weight,
=======
:title, :assignee_id, :position, :description, :confidential,
>>>>>>> 6ce1df41e175c7d62ca760b1e66cf1bf86150284
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: [],
)
end
......
......@@ -19,7 +19,7 @@ class Projects::LabelsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
render json: @available_labels.as_json(only: [:id, :title, :color])
render json: LabelSerializer.new.represent_appearance(@available_labels)
end
end
end
......
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :commit, only: [:show, :builds]
before_action :commit, only: [:show, :builds, :failures]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :builds_enabled, only: :charts
wrap_parameters Ci::Pipeline
def index
@scope = params[:scope]
@pipelines = PipelinesFinder
......@@ -67,10 +69,14 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def builds
respond_to do |format|
format.html do
render 'show'
end
render_show
end
def failures
if @pipeline.statuses.latest.failed.present?
render_show
else
redirect_to pipeline_path(@pipeline)
end
end
......@@ -92,13 +98,25 @@ class Projects::PipelinesController < Projects::ApplicationController
def retry
pipeline.retry_failed(current_user)
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
respond_to do |format|
format.html do
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
format.json { head :no_content }
end
end
def cancel
pipeline.cancel_running
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
respond_to do |format|
format.html do
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
format.json { head :no_content }
end
end
def charts
......@@ -111,6 +129,14 @@ class Projects::PipelinesController < Projects::ApplicationController
private
def render_show
respond_to do |format|
format.html do
render 'show'
end
end
end
def create_params
params.require(:pipeline).permit(:ref)
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.
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