Commit c833a09d authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'master' into jivl-update-katex

parents 46ae0362 0a22ff26
......@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.4.3 (2018-02-05)
### Security (4 changes)
- Fix namespace access issue for GitHub, BitBucket, and GitLab.com project importers.
- Fix stored XSS in code blocks that ignore highlighting.
- Fix wilcard protected tags protecting all branches.
- Restrict Todo API mark_as_done endpoint to the user's todos only.
## 10.4.2 (2018-01-30)
### Fixed (6 changes)
......@@ -197,6 +207,16 @@ entry.
- Use a background migration for issues.closed_at.
## 10.3.7 (2018-02-05)
### Security (4 changes)
- Fix namespace access issue for GitHub, BitBucket, and GitLab.com project importers.
- Fix stored XSS in code blocks that ignore highlighting.
- Fix wilcard protected tags protecting all branches.
- Restrict Todo API mark_as_done endpoint to the user's todos only.
## 10.3.6 (2018-01-22)
### Fixed (17 changes, 2 of them are from the community)
......@@ -415,6 +435,16 @@ entry.
- Clean up schema of the "merge_requests" table.
## 10.2.8 (2018-02-07)
### Security (4 changes)
- Fix namespace access issue for GitHub, BitBucket, and GitLab.com project importers.
- Fix stored XSS in code blocks that ignore highlighting.
- Fix wilcard protected tags protecting all branches.
- Restrict Todo API mark_as_done endpoint to the user's todos only.
## 10.2.7 (2018-01-18)
- No changes.
......
......@@ -294,7 +294,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta44'
gem 'prometheus-client-mmap', '~> 0.9.1'
gem 'raindrops', '~> 0.18'
end
......@@ -410,7 +410,9 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.84.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -285,7 +285,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.83.0)
gitaly-proto (0.84.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -340,7 +340,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1.1)
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0)
googleauth (0.5.3)
......@@ -636,7 +636,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta44)
prometheus-client-mmap (0.9.1)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
......@@ -1056,7 +1056,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.83.0)
gitaly-proto (~> 0.84.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
......@@ -1066,6 +1066,7 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.13.6)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
grape-entity (~> 0.6.0)
......@@ -1131,7 +1132,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta44)
prometheus-client-mmap (~> 0.9.1)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......
{"iconCount":189,"spriteSize":85900,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
\ No newline at end of file
{"iconCount":191,"spriteSize":86607,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="104" viewBox="0 0 142 104"><g fill="none" fill-rule="evenodd"><g transform="translate(112 4)"><path fill="#FFF" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#FC6D26" rx="5"/></g><g transform="translate(5 74)"><rect width="30" height="30" fill="#FFF" rx="8"/><path fill="#E1DBF1" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#6B4FBB" rx="5"/></g><path fill="#FFF" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#FC6D26" rx="4"/><g transform="translate(112 77)"><rect width="24" height="24" fill="#FFF" rx="6"/><path fill="#E1DBF1" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#6B4FBB" rx="4"/></g><g transform="translate(46 29)"><rect width="46" height="46" y="2" fill="#E1DBF1" rx="10"/><rect width="46" height="46" fill="#E1DBF1" rx="10"/><path fill="#C3B8E3" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v26a6 6 0 0 0 6 6h26a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h26c5.523 0 10 4.477 10 10v26c0 5.523-4.477 10-10 10H10C4.477 46 0 41.523 0 36V10C0 4.477 4.477 0 10 0z"/><rect width="14" height="14" x="16" y="16" fill="#6B4FBB" rx="2"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M98.413 35.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#C3B8E3" d="M104.78 29.32a2 2 0 0 1-2.826-2.829l2.122-2.12a2 2 0 0 1 2.827 2.83l-2.122 2.12z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M42.413 89.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#E1DBF1" d="M48.78 83.32a2 2 0 1 1-2.826-2.829l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.122 2.12z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M27.713 26.531a2 2 0 1 1 2.574-3.062l2.296 1.93a2 2 0 1 1-2.573 3.062l-2.297-1.93z"/><path fill="#C3B8E3" d="M34.604 32.321a2 2 0 1 1 2.573-3.062l2.297 1.93A2 2 0 0 1 36.9 34.25l-2.297-1.93z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M93.74 74.553a2 2 0 0 1 2.52-3.106l2.33 1.891a2 2 0 1 1-2.521 3.106l-2.33-1.891z"/><path fill="#E1DBF1" d="M100.727 80.225a2 2 0 1 1 2.521-3.105l2.33 1.89a2 2 0 1 1-2.522 3.106l-2.33-1.89z"/></g></svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
import Sortable from 'vendor/Sortable';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list';
import boardList from './board_list.vue';
import boardBlankState from './board_blank_state';
import './board_delete';
......
<script>
import Sortable from 'vendor/Sortable';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card.vue';
......@@ -8,6 +9,11 @@ const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardList',
components: {
boardCard,
boardNewIssue,
loadingIcon,
},
props: {
disabled: {
type: Boolean,
......@@ -42,46 +48,6 @@ export default {
showIssueForm: false,
};
},
components: {
boardCard,
boardNewIssue,
loadingIcon,
},
methods: {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
scrollToTop() {
this.$refs.list.scrollTop = 0;
},
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
this.list.loadingMore = false;
};
if (getIssues) {
this.list.loadingMore = true;
getIssues
.then(loadingDone)
.catch(loadingDone);
}
},
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage();
}
},
},
watch: {
filters: {
handler() {
......@@ -157,51 +123,90 @@ export default {
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
this.$refs.list.removeEventListener('scroll', this.onScroll);
},
template: `
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<loading-icon />
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-issue-id="-1">
methods: {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
scrollToTop() {
this.$refs.list.scrollTop = 0;
},
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
this.list.loadingMore = false;
};
<loading-icon
v-show="list.loadingMore"
label="Loading more issues"
/>
if (getIssues) {
this.list.loadingMore = true;
getIssues
.then(loadingDone)
.catch(loadingDone);
}
},
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage();
}
},
},
};
</script>
<span v-if="list.issues.length === list.issuesSize">
Showing all issues
</span>
<span v-else>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
<template>
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<loading-icon />
</div>
`,
};
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-issue-id="-1">
<loading-icon
v-show="list.loadingMore"
label="Loading more issues"
/>
<span
v-if="list.issues.length === list.issuesSize"
>
Showing all issues
</span>
<span
v-else
>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
</div>
</template>
/* eslint-disable func-names, no-new, space-before-function-paren, one-var,
promise/catch-or-return */
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
......@@ -28,9 +29,9 @@ gl.issueBoards.newListDropdownInit = () => {
$this.glDropdown({
data(term, callback) {
$.get($this.attr('data-list-labels-path'))
.then((resp) => {
callback(resp);
axios.get($this.attr('data-list-labels-path'))
.then(({ data }) => {
callback(data);
});
},
renderRow (label) {
......
import _ from 'underscore';
import axios from '../lib/utils/axios_utils';
import { s__ } from '../locale';
import Flash from '../flash';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list';
function generateErrorBoxContent(errors) {
const errorList = [].concat(errors).map(errorString => `
<li>
${_.escape(errorString)}
</li>
`);
return `
<p>
${s__('CiVariable|Validation failed')}
</p>
<ul>
${errorList.join('')}
</ul>
`;
}
// Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList {
constructor({
container,
saveButton,
errorBox,
formField = 'variables',
saveEndpoint,
}) {
this.container = container;
this.saveButton = saveButton;
this.errorBox = errorBox;
this.saveEndpoint = saveEndpoint;
this.variableList = new VariableList({
container: this.container,
formField,
});
this.bindEvents();
this.variableList.init();
}
bindEvents() {
this.saveButton.addEventListener('click', this.onSaveClicked.bind(this));
}
onSaveClicked() {
const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon');
loadingIcon.classList.toggle('hide', false);
this.errorBox.classList.toggle('hide', true);
// We use this to prevent a user from changing a key before we have a chance
// to match it up in `updateRowsWithPersistedVariables`
this.variableList.toggleEnableRow(false);
return axios.patch(this.saveEndpoint, {
variables_attributes: this.variableList.getAllData(),
}, {
// We want to be able to process the `res.data` from a 400 error response
// and print the validation messages such as duplicate variable keys
validateStatus: status => (
status >= statusCodes.OK &&
status < statusCodes.MULTIPLE_CHOICES
) ||
status === statusCodes.BAD_REQUEST,
})
.then((res) => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables);
} else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data);
this.errorBox.classList.toggle('hide', false);
}
})
.catch(() => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
Flash(s__('CiVariable|Error occured while saving variables'));
});
}
updateRowsWithPersistedVariables(persistedVariables = []) {
const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({
...variableMap,
[variable.key]: variable,
}), {});
this.container.querySelectorAll('.js-row').forEach((row) => {
// If we submitted a row that was destroyed, remove it so we don't try
// to destroy it again which would cause a BE error
const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
if (convertPermissionToBoolean(destroyInput.value)) {
row.remove();
// Update the ID input so any future edits and `_destroy` will apply on the BE
} else {
const key = row.querySelector('.js-ci-variable-input-key').value;
const persistedVariable = persistedVariableMap[key];
if (persistedVariable) {
// eslint-disable-next-line no-param-reassign
row.querySelector('.js-ci-variable-input-id').value = persistedVariable.id;
row.setAttribute('data-is-persisted', 'true');
}
}
});
}
}
......@@ -11,7 +11,7 @@ function createEnvironmentItem(value) {
return {
title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
id: value,
text: value,
text: value === '*' ? s__('CiVariable|* (All environments)') : value,
};
}
......@@ -39,13 +39,13 @@ export default class VariableList {
},
protected: {
selector: '.js-ci-variable-input-protected',
default: 'true',
default: 'false',
},
environment: {
environment_scope: {
// We can't use a `.js-` class here because
// gl_dropdown replaces the <input> and doesn't copy over the class
// See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458
selector: `input[name="${this.formField}[variables_attributes][][environment]"]`,
selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
default: '*',
},
_destroy: {
......@@ -104,12 +104,15 @@ export default class VariableList {
setupToggleButtons($row[0]);
// Reset the resizable textarea
$row.find(this.inputMap.value.selector).css('height', '');
const $environmentSelect = $row.find('.js-variable-environment-toggle');
if ($environmentSelect.length) {
const createItemDropdown = new CreateItemDropdown({
$dropdown: $environmentSelect,
defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
fieldName: `${this.formField}[variables_attributes][][environment]`,
fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
getData: (term, callback) => callback(this.getEnvironmentValues()),
createNewItemFromValue: createEnvironmentItem,
onSelect: () => {
......@@ -117,7 +120,7 @@ export default class VariableList {
// so they have the new value we just picked
this.refreshDropdownData();
$row.find(this.inputMap.environment.selector).trigger('trigger-change');
$row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
},
});
......@@ -143,7 +146,8 @@ export default class VariableList {
$row.after($rowClone);
}
removeRow($row) {
removeRow(row) {
const $row = $(row);
const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
......@@ -155,6 +159,10 @@ export default class VariableList {
} else {
$row.remove();
}
// Refresh the other dropdowns in the variable list
// so any value with the variable deleted is gone
this.refreshDropdownData();
}
checkIfRowTouched($row) {
......@@ -165,6 +173,11 @@ export default class VariableList {
});
}
toggleEnableRow(isEnabled = true) {
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
}
getAllData() {
// Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems.
......@@ -185,7 +198,7 @@ export default class VariableList {
}
getEnvironmentValues() {
const valueMap = this.$container.find(this.inputMap.environment.selector).toArray()
const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray()
.reduce((prevValueMap, envInput) => ({
...prevValueMap,
[envInput.value]: envInput.value,
......
......@@ -32,13 +32,16 @@ export default class Clusters {
installIngressPath,
installRunnerPath,
installPrometheusPath,
managePrometheusPath,
clusterStatus,
clusterStatusReason,
helpPath,
ingressHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
this.store.setHelpPath(helpPath);
this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({
......@@ -93,6 +96,8 @@ export default class Clusters {
props: {
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
},
});
},
......@@ -172,7 +177,7 @@ export default class Clusters {
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
appList: appTitles.join(', '),
});
Flash(text, 'notice', this.successApplicationContainer);
......
......@@ -32,6 +32,10 @@
type: String,
required: false,
},
manageLink: {
type: String,
required: false,
},
description: {
type: String,
required: true,
......@@ -89,6 +93,12 @@
return label;
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.status === APPLICATION_ERROR ||
this.requestStatus === REQUEST_FAILURE;
......@@ -141,9 +151,21 @@
<div v-html="description"></div>
</div>
<div
class="table-section table-button-footer section-15 section-align-top"
class="table-section table-button-footer section-align-top"
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }"
role="gridcell"
>
<div
v-if="showManageButton"
class="btn-group table-action-buttons"
>
<a
class="btn"
:href="manageLink"
>
{{ manageButtonLabel }}
</a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
class="js-cluster-application-install-button"
......
......@@ -18,13 +18,24 @@
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__(`ClusterIntegration|Install applications on your cluster.
Read more about %{helpLink}`)),
{
_.escape(s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
......@@ -34,7 +45,7 @@
},
helmTillerDescription() {
return _.escape(s__(
`ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
`ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`,
));
......@@ -49,7 +60,7 @@
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
the hosting provider Kubernetes is installed on. If you are using GKE,
the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
you can %{pricingLink}.`,
)), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
......@@ -59,13 +70,28 @@
false,
);
const externalIpParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
)), {
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
return `
<p>
${descriptionParagraph}
</p>
<p class="append-bottom-0">
<p>
${extraCostParagraph}
</p>
<p class="settings-message append-bottom-0">
${externalIpParagraph}
</p>
`;
},
gitlabRunnerDescription() {
......@@ -76,11 +102,12 @@
},
prometheusDescription() {
return sprintf(
_.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`)),
{
_.escape(s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
)), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
},
false,
......@@ -129,6 +156,7 @@ target="_blank" rel="noopener noreferrer">
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
......
......@@ -4,6 +4,7 @@ export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
ingressHelpPath: null,
status: null,
statusReason: null,
applications: {
......@@ -39,8 +40,13 @@ export default class ClusterStore {
};
}
setHelpPath(helpPath) {
setHelpPaths(helpPath, ingressHelpPath) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
this.state.managePrometheusPath = managePrometheusPath;
}
updateStatus(status) {
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import 'vendor/jquery.waitforimages';
// Width where images must fits in, for 2-up this gets divided by 2
const availWidth = 900;
......
......@@ -6,5 +6,5 @@ import 'vendor/jquery.endless-scroll';
import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'vendor/jquery.waitforimages';
import 'jquery.waitforimages';
import 'select2/select2';
......@@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches ||
while (i >= 0 && elms.item(i) !== this) { i -= 1; }
return i > -1;
};
// From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
((arr) => {
arr.forEach((item) => {
if (Object.prototype.hasOwnProperty.call(item, 'remove')) {
return;
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove() {
if (this.parentNode !== null) {
this.parentNode.removeChild(this);
}
},
});
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
......@@ -69,7 +72,9 @@ export default class Diff {
const view = file.data('view');
const params = { since, to, bottom, offset, unfold, view };
$.get(link, params, response => $target.parent().replaceWith(response));
axios.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
.catch(() => flash(__('An error occurred while loading diff')));
}
openAnchoredDiff(cb) {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import MergeRequest from './merge_request';
import Flash from './flash';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import initNotes from './init_notes';
import initIssuableSidebar from './init_issuable_sidebar';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import ShortcutsIssuable from './shortcuts_issuable';
import Diff from './diff';
import SearchAutocomplete from './search_autocomplete';
var Dispatcher;
......@@ -109,6 +103,21 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'admin:projects:index':
import('./pages/admin/projects/index/index')
.then(callDefault)
.catch(fail);
break;
case 'admin:users:index':
import('./pages/admin/users/shared')
.then(callDefault)
.catch(fail);
break;
case 'admin:users:show':
import('./pages/admin/users/shared')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:projects:index':
case 'dashboard:projects:starred':
import('./pages/dashboard/projects')
......@@ -247,17 +256,10 @@ var Dispatcher;
.catch(fail);
break;
case 'projects:merge_requests:show':
new Diff();
new ZenMode();
initIssuableSidebar();
initNotes();
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
shortcut_handler = new ShortcutsIssuable(true);
import('./pages/projects/merge_requests/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'dashboard:activity':
import('./pages/dashboard/activity')
......
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '../../lib/utils/text_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
......@@ -21,14 +22,18 @@
export default {
components: {
userAvatarLink,
'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
'monitoring-button-component': MonitoringButtonComponent,
UserAvatarLink,
CommitComponent,
ActionsComponent,
ExternalUrlComponent,
StopComponent,
RollbackComponent,
TerminalButtonComponent,
MonitoringButtonComponent,
},
directives: {
tooltip,
},
props: {
......@@ -443,7 +448,11 @@
v-if="!model.isFolder"
class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
<span class="flex-truncate-child">{{ model.name }}</span>
<span
class="flex-truncate-child"
v-tooltip
:title="model.name"
>{{ model.name }}</span>
</a>
<span
v-else
......
import _ from 'underscore';
import {
getSelector,
togglePopover,
inserted,
mouseenter,
mouseleave,
} from './feature_highlight_helper';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false);
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
.data('content', $popoverContent.prop('outerHTML'))
.popover({
html: true,
// Override the existing template to add custom CSS classes
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-content"></div>
</div>
`,
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave)
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll);
})
.on('hide.bs.popover', () => {
window.removeEventListener('scroll', hideOnScroll);
})
// Display feature highlight
.removeAttr('disabled');
}
export function findHighestPriorityFeature() {
let priorityFeature;
const sortedFeatureEls = [].slice.call(document.querySelectorAll('.js-feature-highlight')).sort((a, b) =>
(a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0));
const [priorityFeatureEl] = sortedFeatureEls;
if (priorityFeatureEl) {
priorityFeature = priorityFeatureEl.dataset.highlight;
}
return priorityFeature;
}
export function highlightFeatures() {
const priorityFeature = findHighestPriorityFeature();
if (priorityFeature) {
setupFeatureHighlightPopover(priorityFeature);
}
return priorityFeature;
}
import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import Flash from '../flash';
import LazyLoader from '../lazy_loader';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export function togglePopover(show) {
const isAlreadyShown = this.hasClass('js-popover-show');
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
return false;
}
this.popover(show ? 'show' : 'hide');
this.toggleClass('disable-animation js-popover-show', show);
return true;
}
export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId,
})
.catch(() => Flash(__('An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.')));
togglePopover.call(this, false);
this.hide();
}
export function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $featureHighlight = $(this);
togglePopover.call($featureHighlight, false);
}
}
export function mouseenter() {
const $featureHighlight = $(this);
const showedPopover = togglePopover.call($featureHighlight, true);
if (showedPopover) {
$('.popover')
.on('mouseleave', mouseleave.bind($featureHighlight));
}
}
export function inserted() {
const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight;
const $popover = $(this);
const dismissWrapper = dismiss.bind($popover, highlightId);
$(`#${popoverId} .dismiss-feature-highlight`)
.on('click', dismissWrapper);
const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0];
if (lazyImg) {
LazyLoader.loadImage(lazyImg);
}
}
import { highlightFeatures } from './feature_highlight';
import bp from '../breakpoints';
export default function domContentLoaded() {
if (bp.getBreakpointSize() === 'lg') {
highlightFeatures();
return true;
}
return false;
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
......@@ -3,7 +3,6 @@ import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
import './dropdown_utils';
import './filtered_search_token_keys';
import './filtered_search_dropdown_manager';
import './filtered_search_dropdown';
import './filtered_search_manager';
......
......@@ -3,11 +3,11 @@ import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container';
class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', tokenizer, page) {
constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchTokenKeys = filteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
......@@ -29,7 +29,15 @@ class FilteredSearchDropdownManager {
}
setupMapping() {
this.mapping = {
const supportedTokens = this.filteredSearchTokenKeys.getKeys();
const allowedMappings = {
hint: {
reference: null,
gl: 'DropdownHint',
element: this.container.querySelector('#js-dropdown-hint'),
},
};
const availableMappings = {
author: {
reference: null,
gl: 'DropdownUser',
......@@ -64,12 +72,15 @@ class FilteredSearchDropdownManager {
gl: 'DropdownEmoji',
element: this.container.querySelector('#js-dropdown-my-reaction'),
},
hint: {
reference: null,
gl: 'DropdownHint',
element: this.container.querySelector('#js-dropdown-hint'),
},
};
supportedTokens.forEach((type) => {
if (availableMappings[type]) {
allowedMappings[type] = availableMappings[type];
}
});
this.mapping = allowedMappings;
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
......
......@@ -3,20 +3,33 @@ import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class FilteredSearchManager {
constructor(page) {
constructor({
page,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
this.isGroup = false;
this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page;
this.container = FilteredSearchContainer.container;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.filteredSearchInputForm = this.filteredSearchInput.form;
this.clearSearchButton = this.container.querySelector('.clear-search');
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchTokenKeys = filteredSearchTokenKeys;
this.stateFiltersSelector = stateFiltersSelector;
this.recentsStorageKeyNames = {
issues: 'issue-recent-searches',
merge_requests: 'merge-request-recent-searches',
};
this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
......@@ -25,11 +38,7 @@ class FilteredSearchManager {
this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const fullPath = this.searchHistoryDropdownElement ?
this.searchHistoryDropdownElement.dataset.fullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches';
if (this.page === 'merge_requests') {
recentSearchesPagePrefix = 'merge-request-recent-searches';
}
const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`;
const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
......@@ -58,7 +67,13 @@ class FilteredSearchManager {
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, this.page);
this.dropdownManager = new gl.FilteredSearchDropdownManager(
this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
this.tokenizer,
this.page,
this.isGroup,
this.filteredSearchTokenKeys,
);
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
......@@ -86,40 +101,33 @@ class FilteredSearchManager {
}
bindStateEvents() {
this.stateFilters = document.querySelector('.container-fluid .issues-state-filters');
this.stateFilters = document.querySelector(`.container-fluid ${this.stateFiltersSelector}`);
if (this.stateFilters) {
this.searchStateWrapper = this.searchState.bind(this);
this.stateFilters.querySelector('[data-state="opened"]')
.addEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="closed"]')
.addEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="all"]')
.addEventListener('click', this.searchStateWrapper);
this.mergedState = this.stateFilters.querySelector('[data-state="merged"]');
if (this.mergedState) {
this.mergedState.addEventListener('click', this.searchStateWrapper);
}
this.applyToStateFilters((filterEl) => {
filterEl.addEventListener('click', this.searchStateWrapper);
});
}
}
unbindStateEvents() {
if (this.stateFilters) {
this.stateFilters.querySelector('[data-state="opened"]')
.removeEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="closed"]')
.removeEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="all"]')
.removeEventListener('click', this.searchStateWrapper);
if (this.mergedState) {
this.mergedState.removeEventListener('click', this.searchStateWrapper);
}
this.applyToStateFilters((filterEl) => {
filterEl.removeEventListener('click', this.searchStateWrapper);
});
}
}
applyToStateFilters(callback) {
this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => {
if (this.states.indexOf(filterEl.dataset.state) > -1) {
callback(filterEl);
}
});
}
bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
......
......@@ -71,7 +71,7 @@ const conditions = [{
value: 'none',
}];
class FilteredSearchTokenKeys {
export default class FilteredSearchTokenKeys {
static get() {
return tokenKeys;
}
......@@ -121,6 +121,3 @@ class FilteredSearchTokenKeys {
.find(condition => condition.tokenKey === key && condition.value === value) || null;
}
}
window.gl = window.gl || {};
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
/* global autosize */
import autosize from 'autosize';
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import textUtils from './lib/utils/text_markdown';
......
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
......@@ -5,13 +10,13 @@ export default class GpgBadges {
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({
url: form.data('signatures-path'),
data: form.serialize(),
}).done((response) => {
response.signatures.forEach((signature) => {
const params = parseQueryStringIntoObject(form.serialize());
return axios.get(form.data('signatures-path'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
});
})
.catch(() => flash(__('An error occurred while loading commits')));
}
}
......@@ -152,14 +152,14 @@ export default {
showLeaveGroupModal(group, parentGroup) {
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.showModal = true;
this.updateModal = true;
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
},
hideLeaveGroupModal() {
this.showModal = false;
this.updateModal = false;
},
leaveGroup() {
this.showModal = false;
this.updateModal = false;
this.targetGroup.isBeingRemoved = true;
this.service.leaveGroup(this.targetGroup.leavePath)
.then(res => res.json())
......
export default class TransferDropdown {
constructor() {
this.groupDropdown = $('.js-groups-dropdown');
this.parentInput = $('#new_parent_group_id');
this.data = this.groupDropdown.data('data');
this.init();
}
init() {
this.buildDropdown();
}
buildDropdown() {
const extraOptions = [{ id: '', text: 'No parent group' }, 'divider'];
this.groupDropdown.glDropdown({
selectable: true,
filterable: true,
toggleLabel: item => item.text,
search: { fields: ['text'] },
data: extraOptions.concat(this.data),
text: item => item.text,
clicked: (options) => {
const { e } = options;
e.preventDefault();
this.assignSelected(options.selectedObj);
},
});
}
assignSelected(selected) {
this.parentInput.val(selected.id);
}
}
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
class ImporterStatus {
constructor(jobsUrl, importUrl) {
this.jobsUrl = jobsUrl;
......@@ -9,29 +13,7 @@ class ImporterStatus {
initStatusPage() {
$('.js-add-to-import')
.off('click')
.on('click', (event) => {
const $btn = $(event.currentTarget);
const $tr = $btn.closest('tr');
const $targetField = $tr.find('.import-target');
const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
const id = $tr.attr('id').replace('repo_', '');
let targetNamespace;
let newName;
if ($namespaceInput.length > 0) {
targetNamespace = $namespaceInput[0].innerHTML;
newName = $targetField.find('#path').prop('value');
$targetField.empty().append(`${targetNamespace}/${newName}`);
}
$btn.disable().addClass('is-loading');
return $.post(this.importUrl, {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
}, {
dataType: 'script',
});
});
.on('click', this.addToImport.bind(this));
$('.js-import-all')
.off('click')
......@@ -44,6 +26,39 @@ class ImporterStatus {
});
}
addToImport(event) {
const $btn = $(event.currentTarget);
const $tr = $btn.closest('tr');
const $targetField = $tr.find('.import-target');
const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
const id = $tr.attr('id').replace('repo_', '');
let targetNamespace;
let newName;
if ($namespaceInput.length > 0) {
targetNamespace = $namespaceInput[0].innerHTML;
newName = $targetField.find('#path').prop('value');
$targetField.empty().append(`${targetNamespace}/${newName}`);
}
$btn.disable().addClass('is-loading');
return axios.post(this.importUrl, {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
})
.then(({ data }) => {
const job = $(`tr#repo_${id}`);
job.attr('id', `project_${data.id}`);
job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job);
job.addClass('active');
job.find('.import-actions').html('<i class="fa fa-spinner fa-spin" aria-label="importing"></i> started');
})
.catch(() => flash(__('An error occurred while importing project')));
}
setAutoUpdate() {
return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => {
const jobItem = $(`#project_${job.id}`);
......@@ -71,7 +86,7 @@ class ImporterStatus {
}
// eslint-disable-next-line consistent-return
export default function initImporterStatus() {
function initImporterStatus() {
const importerStatus = document.querySelector('.js-importer-status');
if (importerStatus) {
......@@ -79,3 +94,8 @@ export default function initImporterStatus() {
return new ImporterStatus(data.jobsImportPath, data.importPath);
}
}
export {
initImporterStatus as default,
ImporterStatus,
};
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import flash from './flash';
......
......@@ -217,7 +217,7 @@ export default class Job {
}
this.isLogComplete = log.complete;
if (!log.complete) {
if (log.complete === false) {
this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
......
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
......@@ -395,6 +396,26 @@ export const spriteIcon = (icon, className = '') => {
return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
};
/**
* This method takes in object with snake_case property names
* and returns new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*/
export const convertObjectPropsToCamelCase = (obj = {}) => {
if (obj === null) {
return {};
}
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
result[convertToCamelCase(prop)] = obj[prop];
return acc;
}, {});
};
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
window.gl = window.gl || {};
......
......@@ -9,6 +9,20 @@ import {
window.timeago = timeago;
window.dateFormat = dateFormat;
/**
* Returns i18n month names array.
* If `abbreviated` is provided, returns abbreviated
* name.
*
* @param {Boolean} abbreviated
*/
const getMonthNames = (abbreviated) => {
if (abbreviated) {
return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
}
return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
};
/**
* Given a date object returns the day of the week in English
* @param {date} date
......@@ -143,7 +157,6 @@ export const getDayDifference = (a, b) => {
* @param {Number} seconds
* @return {String}
*/
// eslint-disable-next-line import/prefer-default-export
export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
......@@ -158,7 +171,7 @@ export function timeIntervalInWords(intervalInSeconds) {
return text;
}
export function dateInWords(date, abbreviated = false) {
export function dateInWords(date, abbreviated = false, hideYear = false) {
if (!date) return date;
const month = date.getMonth();
......@@ -169,9 +182,115 @@ export function dateInWords(date, abbreviated = false) {
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
if (hideYear) {
return `${monthName} ${date.getDate()}`;
}
return `${monthName} ${date.getDate()}, ${year}`;
}
/**
* Returns month name based on provided date.
*
* @param {Date} date
* @param {Boolean} abbreviated
*/
export const monthInWords = (date, abbreviated = false) => {
if (!date) {
return '';
}
return getMonthNames(abbreviated)[date.getMonth()];
};
/**
* Returns number of days in a month for provided date.
* courtesy: https://stacko(verflow.com/a/1185804/414749
*
* @param {Date} date
*/
export const totalDaysInMonth = (date) => {
if (!date) {
return 0;
}
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
};
/**
* Returns list of Dates referring to Sundays of the month
* based on provided date
*
* @param {Date} date
*/
export const getSundays = (date) => {
if (!date) {
return [];
}
const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
const month = date.getMonth();
const year = date.getFullYear();
const sundays = [];
const dateOfMonth = new Date(year, month, 1);
while (dateOfMonth.getMonth() === month) {
const dayName = getDayName(dateOfMonth);
if (dayName === 'Sunday') {
sundays.push(new Date(dateOfMonth.getTime()));
}
const daysUntilNextSunday = daysToSunday.indexOf(dayName) + 1;
dateOfMonth.setDate(dateOfMonth.getDate() + daysUntilNextSunday);
}
return sundays;
};
/**
* Returns list of Dates representing a timeframe of Months from month of provided date (inclusive)
* up to provided length
*
* For eg;
* If current month is January 2018 and `length` provided is `6`
* Then this method will return list of Date objects as follows;
*
* [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ]
*
* If current month is March 2018 and `length` provided is `3`
* Then this method will return list of Date objects as follows;
*
* [ February 2018, March 2018, April 2018 ]
*
* @param {Number} length
* @param {Date} date
*/
export const getTimeframeWindow = (length, date) => {
if (!length) {
return [];
}
const currentDate = date instanceof Date ? date : new Date();
const currentMonthIndex = Math.floor(length / 2);
const timeframe = [];
// Move date object backward to the first month of timeframe
currentDate.setDate(1);
currentDate.setMonth(currentDate.getMonth() - currentMonthIndex);
// Iterate and update date for the size of length
// and push date reference to timeframe list
for (let i = 0; i < length; i += 1) {
timeframe.push(new Date(currentDate.getTime()));
currentDate.setMonth(currentDate.getMonth() + 1);
}
// Change date of last timeframe item to last date of the month
timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
return timeframe;
};
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -6,4 +6,6 @@ export default {
ABORTED: 0,
NO_CONTENT: 204,
OK: 200,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
};
......@@ -73,3 +73,10 @@ export function capitalizeFirstCharacter(text) {
* @returns {String}
*/
export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
/**
* Converts snake_case string to camelCase
*
* @param {*} string
*/
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
......@@ -26,6 +26,7 @@ import './gl_dropdown';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import initLayoutNav from './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
......
......@@ -2,7 +2,9 @@
/* global ace */
import Vue from 'vue';
import Flash from '../../flash';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
......@@ -49,27 +51,26 @@ import Flash from '../../flash';
loadEditor() {
this.loading = true;
$.get(this.file.content_path)
.done((file) => {
axios.get(this.file.content_path)
.then(({ data }) => {
const content = this.$el.querySelector('pre');
const fileContent = document.createTextNode(file.content);
const fileContent = document.createTextNode(data.content);
content.textContent = fileContent.textContent;
this.originalContent = file.content;
this.originalContent = data.content;
this.fileLoaded = true;
this.editor = ace.edit(content);
this.editor.$blockScrolling = Infinity; // Turn off annoying warning
this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`);
this.editor.getSession().setMode(`ace/mode/${data.blob_ace_mode}`);
this.editor.on('change', () => {
this.saveDiffResolution();
});
this.saveDiffResolution();
this.loading = false;
})
.fail(() => {
new Flash('Failed to load the file, please try again.');
})
.always(() => {
.catch(() => {
flash(__('An error occurred while loading the file'));
this.loading = false;
});
},
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
import 'vendor/jquery.waitforimages';
import { __ } from '~/locale';
import TaskList from './task_list';
import MergeRequestTabs from './merge_request_tabs';
......
......@@ -27,6 +27,7 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
clustersPath: metricsData.clustersPath,
tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics,
......@@ -132,6 +133,7 @@
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
......
......@@ -10,6 +10,11 @@
required: false,
default: '',
},
clustersPath: {
type: String,
required: false,
default: '',
},
selectedState: {
type: String,
required: true,
......@@ -35,7 +40,10 @@
title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`,
buttonText: 'Configure Prometheus',
buttonText: 'Install Prometheus on clusters',
buttonPath: this.clustersPath,
secondaryButtonText: 'Configure existing Prometheus',
secondaryButtonPath: this.settingsPath,
},
loading: {
svgUrl: this.emptyLoadingSvgPath,
......@@ -43,6 +51,7 @@
description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
noData: {
svgUrl: this.emptyUnableToConnectSvgPath,
......@@ -50,12 +59,14 @@
description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
buttonText: 'Configure Prometheus',
buttonPath: this.settingsPath,
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
buttonPath: this.documentationPath,
},
},
};
......@@ -65,13 +76,6 @@
return this.states[this.selectedState];
},
buttonPath() {
if (this.selectedState === 'gettingStarted') {
return this.settingsPath;
}
return this.documentationPath;
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
......@@ -99,11 +103,21 @@
</p>
<div class="state-button">
<a
v-if="currentState.buttonPath"
class="btn btn-success"
:href="buttonPath"
:href="currentState.buttonPath"
>
{{ currentState.buttonText }}
</a>
</div>
<div class="state-button">
<a
v-if="currentState.secondaryButtonPath"
class="btn"
:href="currentState.secondaryButtonPath"
>
{{ currentState.secondaryButtonText }}
</a>
</div>
</div>
</template>
......@@ -102,6 +102,7 @@
.then(() => {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null;
$(this.$refs.noteBody.$el).renderGFM();
this.$refs.noteBody.resetAutoSave();
callback();
......
<script>
import _ from 'underscore';
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
modal,
},
props: {
deleteProjectUrl: {
type: String,
required: false,
default: '',
},
projectName: {
type: String,
required: false,
default: '',
},
csrfToken: {
type: String,
required: false,
default: '',
},
},
data() {
return {
enteredProjectName: '',
};
},
computed: {
title() {
return sprintf(s__('AdminProjects|Delete Project %{projectName}?'),
{
projectName: `'${_.escape(this.projectName)}'`,
},
false,
);
},
text() {
return sprintf(s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
and all related resources including issues, merge requests, etc.. Once you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
{
projectName: `<strong>${_.escape(this.projectName)}</strong>`,
strong_start: '<strong>',
strong_end: '</strong>',
},
false,
);
},
confirmationTextLabel() {
return sprintf(s__('AdminUsers|To confirm, type %{projectName}'),
{
projectName: `<code>${_.escape(this.projectName)}</code>`,
},
false,
);
},
primaryButtonLabel() {
return s__('AdminProjects|Delete project');
},
canSubmit() {
return this.enteredProjectName === this.projectName;
},
},
methods: {
onCancel() {
this.enteredProjectName = '';
},
onSubmit() {
this.$refs.form.submit();
this.enteredProjectName = '';
},
},
};
</script>
<template>
<modal
id="delete-project-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
:submit-disabled="!canSubmit"
@submit="onSubmit"
@cancel="onCancel"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
<p v-html="confirmationTextLabel"></p>
<form
ref="form"
:action="deleteProjectUrl"
method="post"
>
<input
ref="method"
type="hidden"
name="_method"
value="delete"
/>
<input
type="hidden"
name="authenticity_token"
:value="csrfToken"
/>
<input
name="projectName"
class="form-control"
type="text"
v-model="enteredProjectName"
aria-labelledby="input-label"
autocomplete="off"
/>
</form>
</template>
</modal>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
import deleteProjectModal from './components/delete_project_modal.vue';
export default () => {
Vue.use(Translate);
const deleteProjectModalEl = document.getElementById('delete-project-modal');
const deleteModal = new Vue({
el: deleteProjectModalEl,
data: {
deleteProjectUrl: '',
projectName: '',
},
render(createElement) {
return createElement(deleteProjectModal, {
props: {
deleteProjectUrl: this.deleteProjectUrl,
projectName: this.projectName,
csrfToken: csrf.token,
},
});
},
});
$(document).on('shown.bs.modal', (event) => {
if (event.relatedTarget.classList.contains('delete-project-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
deleteModal.projectName = buttonProps.projectName;
}
});
};
<script>
import _ from 'underscore';
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
modal,
},
props: {
deleteUserUrl: {
type: String,
required: false,
default: '',
},
blockUserUrl: {
type: String,
required: false,
default: '',
},
deleteContributions: {
type: Boolean,
required: false,
default: false,
},
username: {
type: String,
required: false,
default: '',
},
csrfToken: {
type: String,
required: false,
default: '',
},
},
data() {
return {
enteredUsername: '',
};
},
computed: {
title() {
const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
return sprintf(
this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, {
username: `'${_.escape(this.username)}'`,
}, false);
},
text() {
const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{
username: `<strong>${_.escape(this.username)}</strong>`,
strong_start: '<strong>',
strong_end: '</strong>',
},
false,
);
},
confirmationTextLabel() {
return sprintf(s__('AdminUsers|To confirm, type %{username}'),
{
username: `<code>${_.escape(this.username)}</code>`,
},
false,
);
},
primaryButtonLabel() {
const keepContributionsLabel = s__('AdminUsers|Delete user');
const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
},
secondaryButtonLabel() {
return s__('AdminUsers|Block user');
},
canSubmit() {
return this.enteredUsername === this.username;
},
},
methods: {
onCancel() {
this.enteredUsername = '';
},
onSecondaryAction() {
const form = this.$refs.form;
form.action = this.blockUserUrl;
this.$refs.method.value = 'put';
form.submit();
},
onSubmit() {
this.$refs.form.submit();
this.enteredUsername = '';
},
},
};
</script>
<template>
<modal
id="delete-user-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
:secondary-button-label="secondaryButtonLabel"
:submit-disabled="!canSubmit"
@submit="onSubmit"
@cancel="onCancel"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
<p v-html="confirmationTextLabel"></p>
<form
ref="form"
:action="deleteUserUrl"
method="post"
>
<input
ref="method"
type="hidden"
name="_method"
value="delete"
/>
<input
type="hidden"
name="authenticity_token"
:value="csrfToken"
/>
<input
type="text"
name="username"
class="form-control"
v-model="enteredUsername"
aria-labelledby="input-label"
autocomplete="off"
/>
</form>
</template>
<template
slot="secondary-button"
slot-scope="props"
>
<button
type="button"
class="btn js-secondary-button btn-warning"
:disabled="!canSubmit"
@click="onSecondaryAction"
data-dismiss="modal"
>
{{ secondaryButtonLabel }}
</button>
</template>
</modal>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
import deleteUserModal from './components/delete_user_modal.vue';
export default () => {
Vue.use(Translate);
const deleteUserModalEl = document.getElementById('delete-user-modal');
const deleteModal = new Vue({
el: deleteUserModalEl,
data: {
deleteUserUrl: '',
blockUserUrl: '',
deleteContributions: '',
username: '',
},
render(createElement) {
return createElement(deleteUserModal, {
props: {
deleteUserUrl: this.deleteUserUrl,
blockUserUrl: this.blockUserUrl,
deleteContributions: this.deleteContributions,
username: this.username,
csrfToken: csrf.token,
},
});
},
});
$(document).on('shown.bs.modal', (event) => {
if (event.relatedTarget.classList.contains('delete-user-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteUserUrl = buttonProps.deleteUserUrl;
deleteModal.blockUserUrl = buttonProps.blockUserUrl;
deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions');
deleteModal.username = buttonProps.username;
}
});
};
......@@ -2,11 +2,18 @@ export default class CILintEditor {
constructor() {
this.editor = window.ace.edit('ci-editor');
this.textarea = document.querySelector('#content');
this.clearYml = document.querySelector('.clear-yml');
this.editor.getSession().setMode('ace/mode/yaml');
this.editor.on('input', () => {
const content = this.editor.getSession().getValue();
this.textarea.value = content;
});
this.clearYml.addEventListener('click', this.clear.bind(this));
}
clear() {
this.editor.setValue('');
}
}
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
export default groupAvatar;
export default () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
};
......@@ -3,6 +3,8 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
export default () => {
initFilteredSearch(FILTERED_SEARCH.ISSUES);
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
});
projectSelect();
};
......@@ -3,6 +3,8 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
export default () => {
initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
});
projectSelect();
};
import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default () => {
const secretVariableTable = document.querySelector('.js-secret-variable-table');
if (secretVariableTable) {
const secretVariableTableValues = new SecretValues({
container: secretVariableTable,
});
secretVariableTableValues.init();
}
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
};
......@@ -8,7 +8,9 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch(FILTERED_SEARCH.ISSUES);
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
});
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
new ShortcutsNavigation();
......
......@@ -6,7 +6,9 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
});
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
......
import MergeRequest from '~/merge_request';
import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
export default () => {
new Diff(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar(); // eslint-disable-line no-new
initNotes(); // eslint-disable-line no-new
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
};
import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default function () {
// Initialize expandable settings panels
initSettingsPanels();
const runnerToken = document.querySelector('.js-secret-runner-token');
if (runnerToken) {
const runnerTokenSecretValue = new SecretValues({
......@@ -12,11 +14,12 @@ export default function () {
runnerTokenSecretValue.init();
}
const secretVariableTable = document.querySelector('.js-secret-variable-table');
if (secretVariableTable) {
const secretVariableTableValues = new SecretValues({
container: secretVariableTable,
});
secretVariableTableValues.init();
}
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
}
export default (page) => {
export default ({ page }) => {
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager(page);
const filteredSearchManager = new gl.FilteredSearchManager({ page });
filteredSearchManager.setup();
}
};
......@@ -31,10 +31,9 @@
type: String,
required: true,
},
confirmActionMessage: {
type: String,
required: false,
default: '',
id: {
type: Number,
required: true,
},
},
data() {
......@@ -49,11 +48,10 @@
},
methods: {
onClick() {
if (this.confirmActionMessage !== '' && confirm(this.confirmActionMessage)) {
this.makeRequest();
} else if (this.confirmActionMessage === '') {
this.makeRequest();
}
eventHub.$emit('actionConfirmationModal', {
id: this.id,
callback: this.makeRequest,
});
},
makeRequest() {
this.isLoading = true;
......
<script>
import pipelinesTableRowComponent from './pipelines_table_row.vue';
import stopConfirmationModal from './stop_confirmation_modal.vue';
import retryConfirmationModal from './retry_confirmation_modal.vue';
/**
* Pipelines Table Component.
......@@ -9,6 +11,8 @@
export default {
components: {
pipelinesTableRowComponent,
stopConfirmationModal,
retryConfirmationModal,
},
props: {
pipelines: {
......@@ -70,5 +74,7 @@
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
<stop-confirmation-modal />
<retry-confirmation-modal />
</div>
</template>
......@@ -305,6 +305,9 @@
css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry"
icon="repeat"
:id="pipeline.id"
data-toggle="modal"
data-target="#retry-confirmation-modal"
/>
<async-button-component
......@@ -313,7 +316,9 @@
css-class="js-pipelines-cancel-button btn-remove"
title="Cancel"
icon="close"
confirm-action-message="Are you sure you want to cancel this pipeline?"
:id="pipeline.id"
data-toggle="modal"
data-target="#stop-confirmation-modal"
/>
</div>
</div>
......
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Retry pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to retry pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="retry-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Stop pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to stop pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Stop pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="stop-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie';
import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
import { getPagePath } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import flash from '../flash';
((global) => {
class Profile {
......@@ -57,8 +59,8 @@ import { getPagePath } from '../lib/utils/common_utils';
onUpdateNotifs(e, data) {
return data.saved ?
new Flash("Notification settings saved", "notice") :
new Flash("Failed to save new settings", "alert");
flash(__('Notification settings saved'), 'notice') :
flash(__('Failed to save new settings'));
}
saveForm() {
......@@ -70,21 +72,18 @@ import { getPagePath } from '../lib/utils/common_utils';
formData.append('user[avatar]', avatarBlob, 'avatar.png');
}
return $.ajax({
axios({
method: this.form.attr('method'),
url: this.form.attr('action'),
type: this.form.attr('method'),
data: formData,
dataType: "json",
processData: false,
contentType: false,
success: response => new Flash(response.message, 'notice'),
error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
complete: () => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
return self.form.find(':input[disabled]').enable();
}
});
})
.then(({ data }) => flash(data.message, 'notice'))
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
self.form.find(':input[disabled]').enable();
})
.catch(error => flash(error.message));
}
setNewRepoCookie() {
......
......@@ -30,6 +30,9 @@ export default function renderMermaid($els) {
$els.each((i, el) => {
const source = el.textContent;
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
mermaid.init(undefined, el, (id) => {
const svg = document.getElementById(id);
......
/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
import axios from './lib/utils/axios_utils';
import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
/**
......@@ -146,23 +147,25 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true;
return $.get(this.autocompletePath, {
project_id: this.projectId,
project_ref: this.projectRef,
term: term,
}, (response) => {
var firstCategory, i, lastCategory, len, suggestion;
return axios.get(this.autocompletePath, {
params: {
project_id: this.projectId,
project_ref: this.projectRef,
term: term,
},
}).then((response) => {
// Hide dropdown menu if no suggestions returns
if (!response.length) {
if (!response.data.length) {
this.disableAutocomplete();
return;
}
const data = [];
// List results
firstCategory = true;
for (i = 0, len = response.length; i < len; i += 1) {
suggestion = response[i];
let firstCategory = true;
let lastCategory;
for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
......@@ -177,7 +180,7 @@ export default class SearchAutocomplete {
lastCategory = suggestion.category;
}
data.push({
id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
......@@ -187,13 +190,17 @@ export default class SearchAutocomplete {
if (data.length) {
data.push('separator');
data.push({
text: "Result name contains \"" + term + "\"",
url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()),
text: `Result name contains "${term}"`,
url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
return callback(data);
})
.always(() => { this.loadingSuggestions = false; });
callback(data);
this.loadingSuggestions = false;
}).catch(() => {
this.loadingSuggestions = false;
});
}
getCategoryContents() {
......
......@@ -2,6 +2,7 @@
import Flash from '../../../flash';
import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale';
export default {
components: {
......@@ -40,8 +41,7 @@
this.service.update('issue', { confidential })
.then(() => location.reload())
.catch(() => {
Flash(`Something went wrong trying to
change the confidentiality of this issue`);
Flash(__('Something went wrong trying to change the confidentiality of this issue'));
});
},
},
......@@ -58,7 +58,7 @@
/>
</div>
<div class="title hide-collapsed">
Confidentiality
{{ __('Confidentiality') }}
<a
v-if="isEditable"
class="pull-right confidential-edit"
......@@ -84,7 +84,7 @@
aria-hidden="true"
class="sidebar-item-icon inline"
/>
Not confidential
{{ __('Not confidential') }}
</div>
<div
v-else
......@@ -95,7 +95,7 @@
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
This issue is confidential
{{ __('This issue is confidential') }}
</div>
</div>
</div>
......
<script>
import editFormButtons from './edit_form_buttons.vue';
import { s__ } from '../../../locale';
export default {
components: {
......@@ -19,6 +20,14 @@
type: Function,
},
},
computed: {
confidentialityOnWarning() {
return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
},
confidentialityOffWarning() {
return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
},
},
};
</script>
......@@ -26,15 +35,13 @@
<div class="dropdown open">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!isConfidential">
You are going to turn on the confidentiality. This means that only team members with
<strong>at least Reporter access</strong>
are able to see and leave comments on the issue.
<p
v-if="!isConfidential"
v-html="confidentialityOnWarning">
</p>
<p v-else>
You are going to turn off the confidentiality. This means
<strong>everyone</strong>
will be able to see and leave a comment on this issue.
<p
v-else
v-html="confidentialityOffWarning">
</p>
<edit-form-buttons
:is-confidential="isConfidential"
......
......@@ -32,7 +32,7 @@ export default {
class="btn btn-default append-right-10"
@click="toggleForm"
>
Cancel
{{ __('Cancel') }}
</button>
<button
type="button"
......
<script>
import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import { __, sprintf } from '../../../locale';
export default {
components: {
......@@ -25,6 +26,14 @@
type: Function,
},
},
computed: {
lockWarning() {
return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
},
unlockWarning() {
return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
},
},
};
</script>
......@@ -33,19 +42,14 @@
<div class="dropdown-menu sidebar-item-warning-message">
<p
class="text"
v-if="isLocked">
Unlock this {{ issuableDisplayName }}?
<strong>Everyone</strong>
will be able to comment.
v-if="isLocked"
v-html="unlockWarning">
</p>
<p
class="text"
v-else>
Lock this {{ issuableDisplayName }}?
Only
<strong>project members</strong>
will be able to comment.
v-else
v-html="lockWarning">
</p>
<edit-form-buttons
......
......@@ -72,7 +72,7 @@
</div>
<div class="title hide-collapsed">
Lock {{ issuableDisplayName }}
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button
v-if="isEditable"
class="pull-right lock-edit btn btn-blank"
......
......@@ -68,7 +68,7 @@ export default {
<div class="compare-display-container">
<div class="compare-display pull-left">
<span class="compare-label">
Spent
{{ s__('TimeTracking|Spent') }}
</span>
<span class="compare-value spent">
{{ timeSpentHumanReadable }}
......@@ -76,7 +76,7 @@ export default {
</div>
<div class="compare-display estimated pull-right">
<span class="compare-label">
Est
{{ s__('TimeTrackingEstimated|Est') }}
</span>
<span class="compare-value">
{{ timeEstimateHumanReadable }}
......
......@@ -9,7 +9,7 @@ export default {
template: `
<div class="time-tracking-estimate-only-pane">
<span class="bold">
Estimated:
{{ s__('TimeTracking|Estimated:') }}
</span>
{{ timeEstimateHumanReadable }}
</div>
......
import { sprintf, s__ } from '../../../locale';
export default {
name: 'time-tracking-help-state',
props: {
......@@ -10,33 +12,39 @@ export default {
href() {
return `${this.rootPath}help/workflow/time_tracking.md`;
},
estimateText() {
return sprintf(
s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), {
slash_command: '<code>/estimate</code>',
}, false,
);
},
spendText() {
return sprintf(
s__('spendCommand|%{slash_command} will update the sum of the time spent.'), {
slash_command: '<code>/spend</code>',
}, false,
);
},
},
template: `
<div class="time-tracking-help-state">
<div class="time-tracking-info">
<h4>
Track time with quick actions
{{ __('Track time with quick actions') }}
</h4>
<p>
Quick actions can be used in the issues description and comment boxes.
{{ __('Quick actions can be used in the issues description and comment boxes.') }}
</p>
<p>
<code>
/estimate
</code>
will update the estimated time with the latest command.
<p v-html="estimateText">
</p>
<p>
<code>
/spend
</code>
will update the sum of the time spent.
<p v-html="spendText">
</p>
<a
class="btn btn-default learn-more-button"
:href="href"
>
Learn more
{{ __('Learn more') }}
</a>
</div>
</div>
......
......@@ -3,7 +3,7 @@ export default {
template: `
<div class="time-tracking-no-tracking-pane">
<span class="no-value">
No estimate or time spent
{{ __('No estimate or time spent') }}
</span>
</div>
`,
......
......@@ -2,7 +2,7 @@ import _ from 'underscore';
import '~/smart_interval';
import timeTracker from './time_tracker';
import IssuableTimeTracker from './time_tracker.vue';
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
......@@ -16,7 +16,7 @@ export default {
};
},
components: {
'issuable-time-tracker': timeTracker,
IssuableTimeTracker,
},
methods: {
listenForQuickActions() {
......
<script>
import timeTrackingHelpState from './help_state';
import timeTrackingCollapsedState from './collapsed_state';
import timeTrackingSpentOnlyPane from './spent_only_pane';
......@@ -8,7 +9,15 @@ import timeTrackingComparisonPane from './comparison_pane';
import eventHub from '../../event_hub';
export default {
name: 'issuable-time-tracker',
name: 'IssuableTimeTracker',
components: {
'time-tracking-collapsed-state': timeTrackingCollapsedState,
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
'time-tracking-comparison-pane': timeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState,
},
props: {
time_estimate: {
type: Number,
......@@ -38,14 +47,6 @@ export default {
showHelp: false,
};
},
components: {
'time-tracking-collapsed-state': timeTrackingCollapsedState,
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
'time-tracking-comparison-pane': timeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState,
},
computed: {
timeSpent() {
return this.time_spent;
......@@ -81,6 +82,9 @@ export default {
return !!this.showHelp;
},
},
created() {
eventHub.$on('timeTracker:updateData', this.update);
},
methods: {
toggleHelpState(show) {
this.showHelp = show;
......@@ -92,72 +96,73 @@ export default {
this.human_time_spent = data.human_time_spent;
},
},
created() {
eventHub.$on('timeTracker:updateData', this.update);
},
template: `
<div
class="time_tracker time-tracking-component-wrap"
v-cloak
>
<time-tracking-collapsed-state
:show-comparison-state="showComparisonState"
:show-no-time-tracking-state="showNoTimeTrackingState"
:show-help-state="showHelpState"
:show-spent-only-state="showSpentOnlyState"
:show-estimate-only-state="showEstimateOnlyState"
};
</script>
<template>
<div
class="time_tracker time-tracking-component-wrap"
v-cloak
>
<time-tracking-collapsed-state
:show-comparison-state="showComparisonState"
:show-no-time-tracking-state="showNoTimeTrackingState"
:show-help-state="showHelpState"
:show-spent-only-state="showSpentOnlyState"
:show-estimate-only-state="showEstimateOnlyState"
:time-spent-human-readable="timeSpentHumanReadable"
:time-estimate-human-readable="timeEstimateHumanReadable"
/>
<div class="title hide-collapsed">
{{ __('Time tracking') }}
<div
class="help-button pull-right"
v-if="!showHelpState"
@click="toggleHelpState(true)"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
>
</i>
</div>
<div
class="close-help-button pull-right"
v-if="showHelpState"
@click="toggleHelpState(false)"
>
<i
class="fa fa-close"
aria-hidden="true"
>
</i>
</div>
</div>
<div class="time-tracking-content hide-collapsed">
<time-tracking-estimate-only-pane
v-if="showEstimateOnlyState"
:time-estimate-human-readable="timeEstimateHumanReadable"
/>
<time-tracking-spent-only-pane
v-if="showSpentOnlyState"
:time-spent-human-readable="timeSpentHumanReadable"
/>
<time-tracking-no-tracking-pane
v-if="showNoTimeTrackingState"
/>
<time-tracking-comparison-pane
v-if="showComparisonState"
:time-estimate="timeEstimate"
:time-spent="timeSpent"
:time-spent-human-readable="timeSpentHumanReadable"
:time-estimate-human-readable="timeEstimateHumanReadable"
/>
<div class="title hide-collapsed">
Time tracking
<div
class="help-button pull-right"
v-if="!showHelpState"
@click="toggleHelpState(true)"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
/>
</div>
<div
class="close-help-button pull-right"
<transition name="help-state-toggle">
<time-tracking-help-state
v-if="showHelpState"
@click="toggleHelpState(false)"
>
<i
class="fa fa-close"
aria-hidden="true"
/>
</div>
</div>
<div class="time-tracking-content hide-collapsed">
<time-tracking-estimate-only-pane
v-if="showEstimateOnlyState"
:time-estimate-human-readable="timeEstimateHumanReadable"
:root-path="rootPath"
/>
<time-tracking-spent-only-pane
v-if="showSpentOnlyState"
:time-spent-human-readable="timeSpentHumanReadable"
/>
<time-tracking-no-tracking-pane
v-if="showNoTimeTrackingState"
/>
<time-tracking-comparison-pane
v-if="showComparisonState"
:time-estimate="timeEstimate"
:time-spent="timeSpent"
:time-spent-human-readable="timeSpentHumanReadable"
:time-estimate-human-readable="timeEstimateHumanReadable"
/>
<transition name="help-state-toggle">
<time-tracking-help-state
v-if="showHelpState"
:rootPath="rootPath"
/>
</transition>
</div>
</transition>
</div>
`,
};
</div>
</template>
......@@ -8,7 +8,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils';
```
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if enabled?}",
'aria-label': _('Toggle Cluster') }
'aria-label': _('Toggle Kubernetes Cluster') }
%input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? }
```
*/
......
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
props: {
mr: { type: Object, required: true },
},
directives: {
tooltip,
},
components: {
'mr-widget-merge-help': mrWidgetMergeHelp,
statusIcon,
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
message() {
return `If the ${this.missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line`;
},
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
{{missingBranchName}}
</span> branch does not exist.
Please restore it or use a different {{missingBranchName}} branch
<i
v-tooltip
class="fa fa-question-circle"
:title="message"
:aria-label="message"></i>
</span>
</div>
</div>
`,
};
<script>
import { sprintf, s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
directives: {
tooltip,
},
components: {
mrWidgetMergeHelp,
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
missingBranchNameMessage() {
return sprintf(s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'), {
missingBranchName: this.missingBranchName,
});
},
message() {
return sprintf(s__('mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line'), {
missingBranchName: this.missingBranchName,
});
},
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
{{ missingBranchName }}
</span> {{ s__("mrWidget|branch does not exist.") }}
{{ missingBranchNameMessage }}
<i
v-tooltip
class="fa fa-question-circle"
:title="message"
:aria-label="message"
>
</i>
</span>
</div>
</div>
</template>
......@@ -24,7 +24,7 @@ export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
......
......@@ -118,7 +118,7 @@
<template>
<div class="branch-commit">
<template v-if="hasCommitRef && showBranch">
<div class="icon-container hidden-xs">
<div class="icon-container">
<i
v-if="tag"
class="fa fa-tag"
......@@ -132,7 +132,7 @@
</div>
<a
class="ref-name hidden-xs"
class="ref-name"
:href="commitRef.ref_url"
v-tooltip
data-container="body"
......
<script>
import _ from 'underscore';
import { __, sprintf } from '~/locale';
export default {
props: {
inputId: {
type: String,
required: true,
},
confirmationKey: {
type: String,
required: true,
},
confirmationValue: {
type: String,
required: true,
},
shouldEscapeConfirmationValue: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
inputLabel() {
let value = this.confirmationValue;
if (this.shouldEscapeConfirmationValue) {
value = _.escape(value);
}
return sprintf(
__('Type %{value} to confirm:'),
{ value: `<code>${value}</code>` },
false,
);
},
},
methods: {
hasCorrectValue() {
return this.$refs.enteredValue.value === this.confirmationValue;
},
},
};
</script>
<template>
<div>
<label
v-html="inputLabel"
:for="inputId"
>
</label>
<input
:id="inputId"
:name="confirmationKey"
type="text"
ref="enteredValue"
class="form-control"
/>
</div>
</template>
......@@ -46,6 +46,11 @@
required: false,
default: '',
},
secondaryButtonLabel: {
type: String,
required: false,
default: '',
},
submitDisabled: {
type: Boolean,
required: false,
......@@ -129,6 +134,21 @@
>
{{ closeButtonLabel }}
</button>
<slot
v-if="secondaryButtonLabel"
name="secondary-button"
>
<button
v-if="secondaryButtonLabel"
type="button"
class="btn"
data-dismiss="modal"
>
{{ secondaryButtonLabel }}
</button>
</slot>
<button
v-if="primaryButtonLabel"
type="button"
......
......@@ -21,7 +21,7 @@
@import "framework/flash";
@import "framework/forms";
@import "framework/gfm";
@import "framework/gitlab-theme";
@import "framework/gitlab_theme";
@import "framework/header";
@import "framework/highlight";
@import "framework/issue_box";
......@@ -35,10 +35,10 @@
@import "framework/pagination";
@import "framework/panels";
@import "framework/popup";
@import "framework/secondary-navigation-elements";
@import "framework/secondary_navigation_elements";
@import "framework/selects";
@import "framework/sidebar";
@import "framework/contextual-sidebar";
@import "framework/contextual_sidebar";
@import "framework/tables";
@import "framework/notes";
@import "framework/tabs";
......@@ -49,15 +49,16 @@
@import "framework/zen";
@import "framework/blank";
@import "framework/wells";
@import "framework/page-header";
@import "framework/page_header";
@import "framework/awards";
@import "framework/images";
@import "framework/broadcast-messages";
@import "framework/broadcast_messages";
@import "framework/emojis";
@import "framework/emoji-sprites";
@import "framework/emoji_sprites";
@import "framework/icons";
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive_tables";
@import "framework/stacked-progress-bar";
@import "framework/stacked_progress_bar";
@import "framework/ci_variable_list";
@import "framework/feature_highlight";
......@@ -8,7 +8,11 @@
.ci-variable-row {
display: flex;
align-items: flex-end;
align-items: flex-start;
@media (max-width: $screen-xs-max) {
align-items: flex-end;
}
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
......@@ -41,6 +45,7 @@
.ci-variable-row-body {
display: flex;
align-items: flex-start;
width: 100%;
@media (max-width: $screen-xs-max) {
......@@ -65,6 +70,8 @@
flex: 0 1 auto;
display: flex;
align-items: center;
padding-top: 5px;
padding-bottom: 5px;
}
.ci-variable-row-remove-button {
......@@ -85,4 +92,8 @@
outline: none;
color: $gl-text-color;
}
&[disabled] {
color: $gl-text-color-disabled;
}
}
.feature-highlight {
position: relative;
margin-left: $gl-padding;
width: 20px;
height: 20px;
cursor: pointer;
&::before {
content: '';
display: block;
position: absolute;
top: 6px;
left: 6px;
width: 8px;
height: 8px;
background-color: $blue-500;
border-radius: 50%;
box-shadow: 0 0 0 rgba($blue-500, 0.4);
animation: pulse-highlight 2s infinite;
}
&:hover::before,
&.disable-animation::before {
animation: none;
}
&[disabled]::before {
display: none;
}
}
.is-showing-fly-out {
.feature-highlight {
display: none;
}
}
.feature-highlight-popover-content {
display: none;
hr {
margin: $gl-padding * 0.5 0;
}
.btn-link {
svg {
@include btn-svg;
path {
fill: currentColor;
}
}
}
.feature-highlight-illustration {
width: 100%;
height: 100px;
padding-top: 12px;
padding-bottom: 12px;
background-color: $indigo-50;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom: 1px solid darken($gray-normal, 8%);
}
}
.popover .feature-highlight-popover-content {
display: block;
}
.feature-highlight-popover {
width: 240px;
padding: 0;
border: 1px solid $dropdown-border-color;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.right > .arrow {
border-right-color: $dropdown-border-color;
}
.popover-content {
padding: 0;
}
}
.feature-highlight-popover-sub-content {
padding: 9px 14px;
}
@include keyframes(pulse-highlight) {
0% {
box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
}
70% {
box-shadow: 0 0 0 10px transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
......@@ -4,6 +4,11 @@
.page-title {
margin-top: 0;
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
}
}
}
......
......@@ -214,6 +214,7 @@ $tooltip-font-size: 12px;
* Padding
*/
$gl-padding: 16px;
$gl-padding-8: 8px;
$gl-col-padding: 15px;
$gl-btn-padding: 10px;
$gl-input-padding: 10px;
......@@ -558,6 +559,7 @@ $jq-ui-default-color: #777;
* Label
*/
$label-padding: 7px;
$label-padding-modal: 10px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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