Commit 387f2e04 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into 3775-show-sast-results-in-mr-widget

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 5e11a314 0ddc1f7c
...@@ -241,7 +241,7 @@ linters: ...@@ -241,7 +241,7 @@ linters:
# Numeric values should not contain unnecessary fractional portions. # Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa: UnnecessaryMantissa:
enabled: false enabled: true
# Do not use parent selector references (&) when they would otherwise # Do not use parent selector references (&) when they would otherwise
# be unnecessary. # be unnecessary.
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 10.1.4 (2017-11-14)
- No changes.
## 10.1.3 (2017-11-10) ## 10.1.3 (2017-11-10)
- [FIXED] Fix: Failed to rebase MR from forked repo. - [FIXED] Fix: Failed to rebase MR from forked repo.
......
...@@ -2,6 +2,16 @@ ...@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.1.4 (2017-11-14)
### Fixed (4 changes)
- Don't try to create fork network memberships for forks with a missing source. !15366
- Formats bytes to human reabale number in registry table.
- Prevent error when authorizing an admin-created OAauth application without a set owner.
- Prevents position update for image diff notes.
## 10.1.3 (2017-11-10) ## 10.1.3 (2017-11-10)
- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization. - [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
......
...@@ -414,7 +414,7 @@ group :ed25519 do ...@@ -414,7 +414,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -298,7 +298,7 @@ GEM ...@@ -298,7 +298,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.51.0) gitaly-proto (0.52.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1061,7 +1061,7 @@ DEPENDENCIES ...@@ -1061,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.51.0) gitaly-proto (~> 0.52.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
...@@ -1224,4 +1224,4 @@ DEPENDENCIES ...@@ -1224,4 +1224,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.15.4 1.16.0
...@@ -141,21 +141,29 @@ the stable branch are: ...@@ -141,21 +141,29 @@ the stable branch are:
* Fixes for security issues * Fixes for security issues
* New or updated translations (as long as they do not touch application code) * New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the upcoming During the feature freeze all merge requests that are meant to go into the
release should have the correct milestone assigned _and_ have the label upcoming release should have the correct milestone assigned _and_ the
~"Pick into Stable" set, so that release managers can find and pick them. `Pick into X.Y` label where `X.Y` is equal to the milestone, so that release
Merge requests without a milestone and this label will managers can find and pick them.
not be merged into any stable branches. Merge requests without this label will not be picked into the stable release.
Fixes marked like this will be shipped in the next RC for that release. Once For example, if the upcoming release is `10.2.0` you will need to set the
the final RC has been prepared ready for release on the 22nd, further fixes `Pick into 10.2` label.
marked ~"Pick into Stable" will go into a patch for that release.
Fixes marked like this will be shipped in the next RC (before the 22nd), or the
If a merge request is to be picked into more than one release it will also need next patch release.
the ~"Pick into Backports" label set to remind the release manager to change
the milestone after cherry-picking. As before, it should still have the If a merge request is to be picked into more than one release it will need one
~"Pick into Stable" label and the milestone of the highest release it will be `Pick into X.Y` label per release where the merge request should be back-ported
picked into. to.
For example, if the current patch release is `10.1.1` and a regression fix needs
to be backported down to the `9.5` release, you will need to assign it the
`10.1` milestone and the following labels:
- `Pick into 10.1`
- `Pick into 10.0`
- `Pick into 9.5`
### Asking for an exception ### Asking for an exception
......
import { truncate } from './lib/utils/text_utility';
const MAX_MESSAGE_LENGTH = 500; const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
...@@ -15,7 +17,7 @@ export default class AbuseReports { ...@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) { if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage); $messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true'); $messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
} }
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import './lists_dropdown'; import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
...@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ ...@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() { submitText() {
const count = ModalStore.selectedCount(); const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
}, },
}, },
methods: { methods: {
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
prefer-template, object-shorthand, prefer-arrow-callback */ prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */ /* global Pager */
import { pluralize } from './lib/utils/text_utility';
export default (function () { export default (function () {
const CommitsList = {}; const CommitsList = {};
...@@ -86,7 +88,7 @@ export default (function () { ...@@ -86,7 +88,7 @@ export default (function () {
// Update commits count in the previous commits header. // Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`); $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
} }
gl.utils.localTimeAgo($processedData.find('.js-timeago')); gl.utils.localTimeAgo($processedData.find('.js-timeago'));
......
/* eslint-disable func-names, prefer-arrow-callback */ /* eslint-disable func-names, prefer-arrow-callback */
import Api from './api'; import Api from './api';
import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown { export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) { constructor($el, namespacePath, projectPath) {
...@@ -107,7 +108,7 @@ export default class CreateLabelDropdown { ...@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message; errors = label.message;
} else { } else {
errors = Object.keys(label.message).map(key => errors = Object.keys(label.message).map(key =>
`${gl.text.humanize(key)} ${label.message[key].join(', ')}`, `${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>'); ).join('<br/>');
} }
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { __ } from '../locale'; import { __ } from '../locale';
import '../lib/utils/text_utility'; import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects'; import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = { const EMPTY_STAGE_TEXTS = {
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
}); });
newData.stages.forEach((item) => { newData.stages.forEach((item) => {
const stageSlug = gl.text.dasherize(item.name.toLowerCase()); const stageSlug = dasherize(item.name.toLowerCase());
item.active = false; item.active = false;
item.isUserAllowed = data.permissions[stageSlug]; item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility'; import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import StopComponent from './environment_stop.vue';
...@@ -134,7 +134,7 @@ export default { ...@@ -134,7 +134,7 @@ export default {
if (this.hasManualActions) { if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => { return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = { const parsedAction = {
name: gl.text.humanize(action.name), name: humanize(action.name),
play_path: action.play_path, play_path: action.play_path,
playable: action.playable, playable: action.playable,
}; };
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
import { parseSeconds, stringifyTime } from './lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from './lib/utils/pretty_time';
import { addDelimiter } from './lib/utils/text_utility';
const healthyClass = 'geo-node-healthy'; const healthyClass = 'geo-node-healthy';
const unhealthyClass = 'geo-node-unhealthy'; const unhealthyClass = 'geo-node-unhealthy';
...@@ -52,7 +53,7 @@ class GeoNodeStatus { ...@@ -52,7 +53,7 @@ class GeoNodeStatus {
static formatCountAndPercentage(count, total, percentage) { static formatCountAndPercentage(count, total, percentage) {
if (count !== null || total != null) { if (count !== null || total != null) {
return `${gl.text.addDelimiter(count)}/${gl.text.addDelimiter(total)} (${percentage})`; return `${addDelimiter(count)}/${addDelimiter(total)} (${percentage})`;
} }
return notAvailable; return notAvailable;
...@@ -60,7 +61,7 @@ class GeoNodeStatus { ...@@ -60,7 +61,7 @@ class GeoNodeStatus {
static formatCount(count) { static formatCount(count) {
if (count !== null) { if (count !== null) {
return gl.text.addDelimiter(count); return addDelimiter(count);
} }
return notAvailable; return notAvailable;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input'; import dropzoneInput from './dropzone_input';
import textUtils from './lib/utils/text_markdown';
export default class GLForm { export default class GLForm {
constructor(form, enableGFM = false) { constructor(form, enableGFM = false) {
...@@ -46,7 +47,7 @@ export default class GLForm { ...@@ -46,7 +47,7 @@ export default class GLForm {
} }
// form and textarea event listeners // form and textarea event listeners
this.addEventListeners(); this.addEventListeners();
gl.text.init(this.form); textUtils.init(this.form);
// hide discard button // hide discard button
this.form.find('.js-note-discard').hide(); this.form.find('.js-note-discard').hide();
this.form.show(); this.form.show();
...@@ -85,7 +86,7 @@ export default class GLForm { ...@@ -85,7 +86,7 @@ export default class GLForm {
clearEventListeners() { clearEventListeners() {
this.textarea.off('focus'); this.textarea.off('focus');
this.textarea.off('blur'); this.textarea.off('blur');
gl.text.removeListeners(this.form); textUtils.removeListeners(this.form);
} }
addEventListeners() { addEventListeners() {
......
...@@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
render: createElement => createElement('related-issues-root', { render: createElement => createElement('related-issues-root', {
props: { props: {
endpoint: relatedIssuesRootElement.dataset.endpoint, endpoint: relatedIssuesRootElement.dataset.endpoint,
canAddRelatedIssues: convertPermissionToBoolean( canAdmin: convertPermissionToBoolean(
relatedIssuesRootElement.dataset.canAddRelatedIssues, relatedIssuesRootElement.dataset.canAddRelatedIssues,
), ),
helpPath: relatedIssuesRootElement.dataset.helpPath, helpPath: relatedIssuesRootElement.dataset.helpPath,
......
...@@ -43,12 +43,15 @@ export default { ...@@ -43,12 +43,15 @@ export default {
computed: { computed: {
inputPlaceholder() { inputPlaceholder() {
return 'Paste issue link or <#issue id>'; return `Paste issue link${this.allowAutoComplete ? ' or <#issue id>' : ''}`;
}, },
isSubmitButtonDisabled() { isSubmitButtonDisabled() {
return (this.inputValue.length === 0 && this.pendingReferences.length === 0) return (this.inputValue.length === 0 && this.pendingReferences.length === 0)
|| this.isSubmitting; || this.isSubmitting;
}, },
allowAutoComplete() {
return Object.keys(this.autoCompleteSources).length > 0;
},
}, },
methods: { methods: {
...@@ -86,12 +89,14 @@ export default { ...@@ -86,12 +89,14 @@ export default {
mounted() { mounted() {
const $input = $(this.$refs.input); const $input = $(this.$refs.input);
if (this.allowAutoComplete) {
this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources); this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources);
this.gfmAutoComplete.setup($input, { this.gfmAutoComplete.setup($input, {
issues: true, issues: true,
}); });
$input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true)); $input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
$input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, false)); $input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, false));
}
this.$refs.input.focus(); this.$refs.input.focus();
}, },
...@@ -114,15 +119,22 @@ export default { ...@@ -114,15 +119,22 @@ export default {
role="button" role="button"
@click="onInputWrapperClick"> @click="onInputWrapperClick">
<ul class="add-issuable-form-input-token-list"> <ul class="add-issuable-form-input-token-list">
<!--
We need to ensure this key changes any time the pendingReferences array is updated
else two consecutive pending ref strings in an array with the same name will collide
and cause odd behavior when one is removed.
-->
<li <li
:key="reference" :key="`${pendingReferences.length}-${reference}`"
v-for="(reference, index) in pendingReferences" v-for="(reference, index) in pendingReferences"
class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item"> class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item">
<issue-token <issue-token
event-namespace="pendingIssuable" event-namespace="pendingIssuable"
:id-key="index" :id-key="index"
:display-reference="reference" :display-reference="reference"
:can-remove="true" /> :can-remove="true"
:is-condensed="true"
/>
</li> </li>
<li class="add-issuable-form-input-list-item"> <li class="add-issuable-form-input-list-item">
<input <input
...@@ -144,11 +156,10 @@ export default { ...@@ -144,11 +156,10 @@ export default {
class="js-add-issuable-form-add-button btn btn-new pull-left" class="js-add-issuable-form-add-button btn btn-new pull-left"
:disabled="isSubmitButtonDisabled"> :disabled="isSubmitButtonDisabled">
Add Add
<loadingIcon <loading-icon
ref="loadingIcon" ref="loadingIcon"
v-if="isSubmitting" v-if="isSubmitting"
:inline="true" :inline="true" />
label="Submitting related issues" />
</button> </button>
<button <button
type="button" type="button"
......
...@@ -4,7 +4,11 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -4,7 +4,11 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default { export default {
name: 'IssueToken', name: 'IssueToken',
data() {
return {
removeDisabled: false,
};
},
props: { props: {
idKey: { idKey: {
type: Number, type: Number,
...@@ -39,6 +43,11 @@ export default { ...@@ -39,6 +43,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
isCondensed: {
type: Boolean,
required: false,
default: false,
},
}, },
directives: { directives: {
...@@ -47,11 +56,16 @@ export default { ...@@ -47,11 +56,16 @@ export default {
computed: { computed: {
removeButtonLabel() { removeButtonLabel() {
return `Remove related issue ${this.displayReference}`; return `Remove ${this.displayReference}`;
}, },
hasState() { hasState() {
return this.state && this.state.length > 0; return this.state && this.state.length > 0;
}, },
stateTitle() {
if (this.isCondensed) return '';
return this.isOpen ? 'Open' : 'Closed';
},
isOpen() { isOpen() {
return this.state === 'opened'; return this.state === 'opened';
}, },
...@@ -67,6 +81,12 @@ export default { ...@@ -67,6 +81,12 @@ export default {
computedPath() { computedPath() {
return this.path.length ? this.path : null; return this.path.length ? this.path : null;
}, },
innerComponentType() {
return this.isCondensed ? 'span' : 'div';
},
issueTitle() {
return this.isCondensed ? this.title : '';
},
}, },
methods: { methods: {
...@@ -77,53 +97,81 @@ export default { ...@@ -77,53 +97,81 @@ export default {
} }
eventHub.$emit(`${namespacePrefix}removeRequest`, this.idKey); eventHub.$emit(`${namespacePrefix}removeRequest`, this.idKey);
this.removeDisabled = true;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="issue-token"> <div :class="{
'issue-token': isCondensed,
'flex-row issue-info-container': !isCondensed,
}">
<component <component
v-tooltip v-tooltip
:is="this.computedLinkElementType" :is="this.computedLinkElementType"
ref="link" ref="link"
class="issue-token-link" :class="{
'issue-token-link': isCondensed,
'issue-main-info': !isCondensed,
}"
:href="computedPath" :href="computedPath"
:title="title" :title="issueTitle"
data-placement="top"> data-placement="top"
<span >
<component
:is="innerComponentType"
v-if="hasTitle"
ref="title"
class="js-issue-token-title"
:class="{
'issue-token-title issue-token-end': isCondensed,
'issue-title block-truncated': !isCondensed,
'issue-token-title-standalone': !canRemove
}">
<span class="issue-token-title-text">
{{ title }}
</span>
</component>
<component
:is="innerComponentType"
ref="reference" ref="reference"
class="issue-token-reference"> :class="{
'issue-token-reference': isCondensed,
'issuable-info': !isCondensed,
}">
<i <i
ref="stateIcon" ref="stateIcon"
v-if="hasState" v-if="hasState"
v-tooltip
class="fa" class="fa"
:class="{ :class="{
'issue-token-state-icon-open fa-circle-o': isOpen, 'issue-token-state-icon-open fa-circle-o': isOpen,
'issue-token-state-icon-closed fa-minus': isClosed, 'issue-token-state-icon-closed fa-minus': isClosed,
}" }"
:aria-label="state"> :title="stateTitle"
</i> :aria-label="state"
{{ displayReference }} >
</span> </i>{{ displayReference }}
<span </component>
v-if="hasTitle"
ref="title"
class="js-issue-token-title issue-token-title"
:class="{ 'issue-token-title-standalone': !canRemove }">
<span class="issue-token-title-text">
{{ title }}
</span>
</span>
</component> </component>
<button <button
v-if="canRemove" v-if="canRemove"
v-tooltip
ref="removeButton" ref="removeButton"
type="button" type="button"
class="js-issue-token-remove-button issue-token-remove-button" class="js-issue-token-remove-button"
:class="{
'issue-token-remove-button': isCondensed,
'btn btn-default': !isCondensed
}"
:title="removeButtonLabel"
:aria-label="removeButtonLabel" :aria-label="removeButtonLabel"
@click="onRemoveRequest"> :disabled="removeDisabled"
@click="onRemoveRequest"
>
<i <i
class="fa fa-times" class="fa fa-times"
aria-hidden="true"> aria-hidden="true">
......
...@@ -24,7 +24,7 @@ export default { ...@@ -24,7 +24,7 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
canAddRelatedIssues: { canAdmin: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
...@@ -54,6 +54,11 @@ export default { ...@@ -54,6 +54,11 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
title: {
type: String,
required: false,
default: 'Related issues',
},
}, },
directives: { directives: {
...@@ -100,7 +105,7 @@ export default { ...@@ -100,7 +105,7 @@ export default {
class="panel-heading" class="panel-heading"
:class="{ 'panel-empty-heading': !this.hasBody }"> :class="{ 'panel-empty-heading': !this.hasBody }">
<h3 class="panel-title"> <h3 class="panel-title">
Related issues {{ title }}
<a <a
v-if="hasHelpPath" v-if="hasHelpPath"
:href="helpPath"> :href="helpPath">
...@@ -112,11 +117,11 @@ export default { ...@@ -112,11 +117,11 @@ export default {
<div class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge"> <div class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge">
<span <span
class="issue-count-badge-count" class="issue-count-badge-count"
:class="{ 'has-btn': this.canAddRelatedIssues }"> :class="{ 'has-btn': this.canAdmin }">
{{ badgeLabel }} {{ badgeLabel }}
</span> </span>
<button <button
v-if="canAddRelatedIssues" v-if="canAdmin"
ref="issueCountBadgeAddButton" ref="issueCountBadgeAddButton"
type="button" type="button"
class="js-issue-count-badge-add-button issue-count-badge-add-button btn btn-sm btn-default" class="js-issue-count-badge-add-button issue-count-badge-add-button btn btn-sm btn-default"
...@@ -156,11 +161,11 @@ export default { ...@@ -156,11 +161,11 @@ export default {
label="Fetching related issues" /> label="Fetching related issues" />
</div> </div>
<ul <ul
class="related-issues-token-list"> class="flex-list content-list issuable-list">
<li <li
:key="issue.id" :key="issue.id"
v-for="issue in relatedIssues" v-for="issue in relatedIssues"
class="js-related-issues-token-list-item related-issues-token-list-item"> class="js-related-issues-token-list-item">
<issue-token <issue-token
event-namespace="relatedIssue" event-namespace="relatedIssue"
:id-key="issue.id" :id-key="issue.id"
...@@ -168,7 +173,8 @@ export default { ...@@ -168,7 +173,8 @@ export default {
:title="issue.title" :title="issue.title"
:path="issue.path" :path="issue.path"
:state="issue.state" :state="issue.state"
:can-remove="true" /> :can-remove="canAdmin"
/>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -40,7 +40,7 @@ export default { ...@@ -40,7 +40,7 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
canAddRelatedIssues: { canAdmin: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
...@@ -50,6 +50,16 @@ export default { ...@@ -50,6 +50,16 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
title: {
type: String,
required: false,
default: 'Related issues',
},
allowAutoComplete: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
...@@ -70,6 +80,7 @@ export default { ...@@ -70,6 +80,7 @@ export default {
computed: { computed: {
autoCompleteSources() { autoCompleteSources() {
if (!this.allowAutoComplete) return {};
return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources; return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources;
}, },
}, },
...@@ -86,13 +97,11 @@ export default { ...@@ -86,13 +97,11 @@ export default {
}) })
.catch((res) => { .catch((res) => {
if (res && res.status !== 404) { if (res && res.status !== 404) {
// eslint-disable-next-line no-new Flash('An error occurred while removing issues.');
new Flash('An error occurred while removing related issues.');
} }
}); });
} else { } else {
// eslint-disable-next-line no-new Flash('We could not determine the path to remove the issue');
new Flash('We could not determine the path to remove the related issue');
} }
}, },
onToggleAddRelatedIssuesForm() { onToggleAddRelatedIssuesForm() {
...@@ -119,8 +128,11 @@ export default { ...@@ -119,8 +128,11 @@ export default {
}) })
.catch((res) => { .catch((res) => {
this.isSubmitting = false; this.isSubmitting = false;
// eslint-disable-next-line no-new let errorMessage = 'We can\'t find an issue that matches what you are looking for.';
new Flash(res.data.message || 'We can\'t find an issue that matches what you are looking for.'); if (res.data && res.data.message) {
errorMessage = res.data.message;
}
Flash(errorMessage);
}); });
} }
}, },
...@@ -137,7 +149,11 @@ export default { ...@@ -137,7 +149,11 @@ export default {
this.store.setRelatedIssues(issues); this.store.setRelatedIssues(issues);
this.isFetching = false; this.isFetching = false;
}) })
.catch(() => new Flash('An error occurred while fetching related issues.')); .catch(() => {
this.store.setRelatedIssues([]);
this.isFetching = false;
Flash('An error occurred while fetching issues.');
});
}, },
onInput(newValue, caretPos) { onInput(newValue, caretPos) {
...@@ -211,9 +227,11 @@ export default { ...@@ -211,9 +227,11 @@ export default {
:is-fetching="isFetching" :is-fetching="isFetching"
:is-submitting="isSubmitting" :is-submitting="isSubmitting"
:related-issues="state.relatedIssues" :related-issues="state.relatedIssues"
:can-add-related-issues="canAddRelatedIssues" :can-admin="canAdmin"
:pending-references="state.pendingReferences" :pending-references="state.pendingReferences"
:is-form-visible="isFormVisible" :is-form-visible="isFormVisible"
:input-value="inputValue" :input-value="inputValue"
:auto-complete-sources="autoCompleteSources" /> :auto-complete-sources="autoCompleteSources"
:title="title"
/>
</template> </template>
...@@ -8,7 +8,7 @@ class RelatedIssuesStore { ...@@ -8,7 +8,7 @@ class RelatedIssuesStore {
}; };
} }
setRelatedIssues(issues) { setRelatedIssues(issues = []) {
this.state.relatedIssues = issues; this.state.relatedIssues = issues;
} }
......
/* 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 */ /* 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 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility'; import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash'; import Flash from './flash';
import TaskList from './task_list'; import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
...@@ -73,7 +73,7 @@ export default class Issue { ...@@ -73,7 +73,7 @@ export default class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) { if (this.createMergeRequestDropdown) {
if (isClosed) { if (isClosed) {
......
...@@ -29,6 +29,11 @@ export default { ...@@ -29,6 +29,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
...@@ -92,6 +97,11 @@ export default { ...@@ -92,6 +97,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
issuableType: {
type: String,
required: false,
default: 'issue',
},
}, },
data() { data() {
const store = new Store({ const store = new Store({
...@@ -157,21 +167,21 @@ export default { ...@@ -157,21 +167,21 @@ export default {
}) })
.catch(() => { .catch(() => {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
window.Flash('Error updating issue'); window.Flash(`Error updating ${this.issuableType}`);
}); });
}, },
deleteIssuable() { deleteIssuable() {
this.service.deleteIssuable() this.service.deleteIssuable()
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then((data) => {
// Stop the poll so we don't get 404's with the issue not existing // Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop(); this.poll.stop();
gl.utils.visitUrl(data.web_url); gl.utils.visitUrl(data.web_url);
}) })
.catch(() => { .catch(() => {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
window.Flash('Error deleting issue'); window.Flash(`Error deleting ${this.issuableType}`);
}); });
}, },
}, },
...@@ -223,6 +233,7 @@ export default { ...@@ -223,6 +233,7 @@ export default {
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
/> />
<div v-else> <div v-else>
<title-component <title-component
......
...@@ -13,6 +13,11 @@ ...@@ -13,6 +13,11 @@
type: Object, type: Object,
required: true, required: true,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -23,6 +28,9 @@ ...@@ -23,6 +28,9 @@
isSubmitEnabled() { isSubmitEnabled() {
return this.formState.title.trim() !== ''; return this.formState.title.trim() !== '';
}, },
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
}, },
methods: { methods: {
closeForm() { closeForm() {
...@@ -62,7 +70,7 @@ ...@@ -62,7 +70,7 @@
Cancel Cancel
</button> </button>
<button <button
v-if="canDestroy" v-if="shouldShowDeleteButton"
class="btn btn-danger pull-right append-right-default" class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }" :class="{ disabled: deleteLoading }"
type="button" type="button"
......
...@@ -36,6 +36,11 @@ ...@@ -36,6 +36,11 @@
type: String, type: String,
required: true, required: true,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
}, },
components: { components: {
lockedWarning, lockedWarning,
...@@ -81,6 +86,7 @@ ...@@ -81,6 +86,7 @@
:markdown-docs-path="markdownDocsPath" /> :markdown-docs-path="markdownDocsPath" />
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" /> :can-destroy="canDestroy"
:show-delete-button="showDeleteButton" />
</form> </form>
</template> </template>
...@@ -14,8 +14,8 @@ export default class Job { ...@@ -14,8 +14,8 @@ export default class Job {
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
this.$document = $(document); this.$document = $(document);
this.$window = $(window);
this.logBytes = 0; this.logBytes = 0;
this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this); this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace'); this.$buildTrace = $('#build-trace');
...@@ -54,23 +54,18 @@ export default class Job { ...@@ -54,23 +54,18 @@ export default class Job {
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
$(window) this.$window
.off('scroll') .off('scroll')
.on('scroll', () => { .on('scroll', () => {
const contentHeight = this.$buildTraceOutput.height(); if (!this.isScrolledToBottom()) {
if (contentHeight > this.windowSize) {
// means the user did not scroll, the content was updated.
this.windowSize = contentHeight;
} else {
// User scrolled
this.hasBeenScrolled = true;
this.toggleScrollAnimation(false); this.toggleScrollAnimation(false);
} else if (this.isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
} }
this.scrollThrottled(); this.scrollThrottled();
}); });
$(window) this.$window
.off('resize.build') .off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
...@@ -99,14 +94,14 @@ export default class Job { ...@@ -99,14 +94,14 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
canScroll() { canScroll() {
return $(document).height() > $(window).height(); return this.$document.height() > this.$window.height();
} }
toggleScroll() { toggleScroll() {
const currentPosition = $(document).scrollTop(); const currentPosition = this.$document.scrollTop();
const scrollHeight = $(document).height(); const scrollHeight = this.$document.height();
const windowHeight = $(window).height(); const windowHeight = this.$window.height();
if (this.canScroll()) { if (this.canScroll()) {
if (currentPosition > 0 && if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) { (scrollHeight - currentPosition !== windowHeight)) {
...@@ -119,7 +114,7 @@ export default class Job { ...@@ -119,7 +114,7 @@ export default class Job {
this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (scrollHeight - currentPosition === windowHeight) { } else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log. // User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false); this.toggleDisableButton(this.$scrollTopBtn, false);
...@@ -131,9 +126,17 @@ export default class Job { ...@@ -131,9 +126,17 @@ export default class Job {
} }
} }
isScrolledToBottom() {
const currentPosition = this.$document.scrollTop();
const scrollHeight = this.$document.height();
const windowHeight = this.$window.height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
scrollDown() { scrollDown() {
$(document).scrollTop($(document).height()); this.$document.scrollTop(this.$document.height());
} }
scrollToBottom() { scrollToBottom() {
...@@ -143,7 +146,7 @@ export default class Job { ...@@ -143,7 +146,7 @@ export default class Job {
} }
scrollToTop() { scrollToTop() {
$(document).scrollTop(0); this.$document.scrollTop(0);
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.toggleScroll(); this.toggleScroll();
} }
...@@ -174,7 +177,7 @@ export default class Job { ...@@ -174,7 +177,7 @@ export default class Job {
this.state = log.state; this.state = log.state;
} }
this.windowSize = this.$buildTraceOutput.height(); this.isScrollInBottom = this.isScrolledToBottom();
if (log.append) { if (log.append) {
this.$buildTraceOutput.append(log.html); this.$buildTraceOutput.append(log.html);
...@@ -194,14 +197,9 @@ export default class Job { ...@@ -194,14 +197,9 @@ export default class Job {
} else { } else {
this.$truncatedInfo.addClass('hidden'); this.$truncatedInfo.addClass('hidden');
} }
this.isLogComplete = log.complete;
if (!log.complete) { if (!log.complete) {
if (!this.hasBeenScrolled) {
this.toggleScrollAnimation(true);
} else {
this.toggleScrollAnimation(false);
}
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.getBuildTrace(); this.getBuildTrace();
}, 4000); }, 4000);
...@@ -218,7 +216,7 @@ export default class Job { ...@@ -218,7 +216,7 @@ export default class Job {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
}) })
.then(() => { .then(() => {
if (!this.hasBeenScrolled) { if (this.isScrollInBottom) {
this.scrollDown(); this.scrollDown();
} }
}) })
......
...@@ -172,7 +172,6 @@ export const getSelectedFragment = () => { ...@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment; return documentFragment;
}; };
// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => { export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart; const selectionStart = target.selectionStart;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import timeago from 'timeago.js'; import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format'; import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
import { import {
lang, lang,
...@@ -142,9 +143,9 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -142,9 +143,9 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = ''; let text = '';
if (minutes >= 1) { if (minutes >= 1) {
text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`; text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else { } else {
text = `${seconds} ${gl.text.pluralize('second', seconds)}`; text = `${seconds} ${pluralize('second', seconds)}`;
} }
return text; return text;
} }
......
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
const textUtils = {};
textUtils.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
textUtils.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
textUtils.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
textUtils.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
if (removedLastNewLine) {
insertText += '\n';
}
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos);
}
};
textUtils.updateText = function(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
textUtils.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
textUtils.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
textUtils.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
export default textUtils;
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ /**
* Adds a , to a string composed by numbers, at every 3 chars.
import 'vendor/latinise'; *
* 2333 -> 2,333
var base; * 232324 -> 232,324
var w = window; *
if (w.gl == null) { * @param {String} text
w.gl = {}; * @returns {String}
} */
if ((base = w.gl).text == null) { export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
base.text = {};
}
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
/** /**
* Returns '99+' for numbers bigger than 99. * Returns '99+' for numbers bigger than 99.
...@@ -20,182 +15,50 @@ gl.text.addDelimiter = function(text) { ...@@ -20,182 +15,50 @@ gl.text.addDelimiter = function(text) {
* @param {Number} count * @param {Number} count
* @return {Number|String} * @return {Number|String}
*/ */
export function highCountTrim(count) { export const highCountTrim = count => (count > 99 ? '99+' : count);
return count > 99 ? '99+' : count;
}
export function capitalizeFirstCharacter(text) { /**
return `${text[0].toUpperCase()}${text.slice(1)}`; * Converst first char to uppercase and replaces undercores with spaces
} * @param {String} string
* @requires {String}
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
gl.text.getTextWidth = function(text, font) {
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
*
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/ */
// re-use canvas object for better performance export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
var context = canvas.getContext('2d');
context.font = font;
return context.measureText(text).width;
};
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
gl.text.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
gl.text.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
gl.text.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; /**
* Adds an 's' to the end of the string when count is bigger than 0
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { * @param {String} str
if (blockTag != null && blockTag !== '') { * @param {Number} count
insertText = this.blockTagText(text, textArea, blockTag, selected); * @returns {String}
} else { */
insertText = selectedSplit.map(function(val) { export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
if (removedLastNewLine) { /**
insertText += '\n'; * Replaces underscores with dashes
} * @param {*} str
* @returns {String}
*/
export const dasherize = str => str.replace(/[_\s]+/g, '-');
if (document.queryCommandSupported('insertText')) { /**
inserted = document.execCommand('insertText', false, insertText); * Removes accents and converts to lower case
} * @param {String} str
if (!inserted) { * @returns {String}
try { */
document.execCommand("ms-beginUndoUnit"); export const slugify = str => str.trim().toLowerCase();
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) { /**
pos -= 1; * Truncates given text
} *
* @param {String} string
* @param {Number} maxLength
* @returns {String}
*/
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
return textArea.setSelectionRange(pos, pos); /**
} * Capitalizes first character.
}; *
gl.text.updateText = function(textArea, tag, blockTag, wrap) { * @param {String} text
var $textArea, selected, text; * @returns {String}
$textArea = $(textArea); */
textArea = $textArea.get(0); export const capitalizeFirstCharacter = text => `${text[0].toUpperCase()}${text.slice(1)}`;
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
gl.text.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
gl.text.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
};
gl.text.pluralize = function(str, count) {
return str + (count > 1 || count === 0 ? 's' : '');
};
gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
gl.text.dasherize = function(str) {
return str.replace(/[_\s]+/g, '-');
};
gl.text.slugify = function(str) {
return str.trim().toLowerCase().latinise();
};
...@@ -30,7 +30,6 @@ import './commit/image_file'; ...@@ -30,7 +30,6 @@ import './commit/image_file';
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
import './lib/utils/pretty_time'; import './lib/utils/pretty_time';
import './lib/utils/text_utility';
import './lib/utils/url_utility'; import './lib/utils/url_utility';
// behaviors // behaviors
......
...@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages'; ...@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list'; import TaskList from './task_list';
import './merge_request_tabs'; import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper'; import IssuablesHelper from './helpers/issuables_helper';
import { addDelimiter } from './lib/utils/text_utility';
(function() { (function() {
this.MergeRequest = (function() { this.MergeRequest = (function() {
...@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter'); const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
$el.text(gl.text.addDelimiter(count)); $el.text(addDelimiter(count));
}; };
MergeRequest.prototype.hideCloseButton = function() { MergeRequest.prototype.hideCloseButton = function() {
......
...@@ -357,7 +357,8 @@ ...@@ -357,7 +357,8 @@
@click="handleSave(true)" @click="handleSave(true)"
v-if="canUpdateIssue" v-if="canUpdateIssue"
:class="actionButtonClassNames" :class="actionButtonClassNames"
class="btn btn-comment btn-comment-and-close"> :disabled="isSubmitting"
class="btn btn-comment btn-comment-and-close js-action-button">
{{issueActionButtonTitle}} {{issueActionButtonTitle}}
</button> </button>
<button <button
......
<script> <script>
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
import { dasherize } from '../../../lib/utils/text_utility';
/** /**
* Renders either a cancel, retry or play icon pointing to the given path. * Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead. * TODO: Remove UJS from here and use an async request instead.
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = gl.text.dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
}, },
......
...@@ -64,6 +64,9 @@ export default { ...@@ -64,6 +64,9 @@ export default {
); );
this.monacoInstance.setModel(newModel); this.monacoInstance.setModel(newModel);
this.monacoInstance.updateOptions({
readOnly: !!this.activeFile.file_lock,
});
}, },
addMonacoEvents() { addMonacoEvents() {
this.monacoInstance.onKeyUp(() => { this.monacoInstance.onKeyUp(() => {
...@@ -87,7 +90,7 @@ export default { ...@@ -87,7 +90,7 @@ export default {
'activeFileExtension', 'activeFileExtension',
]), ]),
shouldHideEditor() { shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw; return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
}, },
}, },
}; };
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
export default { export default {
mixins: [ mixins: [
...@@ -9,6 +10,7 @@ ...@@ -9,6 +10,7 @@
], ],
components: { components: {
skeletonLoadingContainer, skeletonLoadingContainer,
fileStatusIcon,
}, },
props: { props: {
file: { file: {
...@@ -70,6 +72,9 @@ ...@@ -70,6 +72,9 @@
class="repo-file-name" class="repo-file-name"
> >
{{ file.name }} {{ file.name }}
<fileStatusIcon
:file="file">
</fileStatusIcon>
</a> </a>
<template v-if="isSubmodule && file.id"> <template v-if="isSubmodule && file.id">
@ @
......
<script>
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/datetime_utility';
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
},
},
};
</script>
<template>
<span
v-if="file.file_lock"
v-tooltip
:title="lockTooltip"
data-container="body"
>
<icon
name="lock"
css-classes="file-status-icon"
>
</icon>
</span>
</template>
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import fileStatusIcon from './repo_file_status_icon.vue';
export default { export default {
props: { props: {
...@@ -9,6 +10,10 @@ export default { ...@@ -9,6 +10,10 @@ export default {
}, },
}, },
components: {
fileStatusIcon,
},
computed: { computed: {
closeLabel() { closeLabel() {
if (this.tab.changed || this.tab.tempFile) { if (this.tab.changed || this.tab.tempFile) {
...@@ -57,6 +62,9 @@ export default { ...@@ -57,6 +62,9 @@ export default {
:title="tab.url" :title="tab.url"
@click.prevent.stop="setFileActive(tab)"> @click.prevent.stop="setFileActive(tab)">
{{tab.name}} {{tab.name}}
<fileStatusIcon
:file="tab">
</fileStatusIcon>
</a> </a>
</li> </li>
</template> </template>
...@@ -3,7 +3,7 @@ import flash from '../../flash'; ...@@ -3,7 +3,7 @@ import flash from '../../flash';
import service from '../services'; import service from '../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const redirectToUrl = url => gl.utils.visitUrl(url); export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
...@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n ...@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) { if (newMr) {
redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`); dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else { } else {
commit(types.SET_COMMIT_REF, data.id); commit(types.SET_COMMIT_REF, data.id);
......
...@@ -3,16 +3,16 @@ import * as types from '../mutation_types'; ...@@ -3,16 +3,16 @@ import * as types from '../mutation_types';
import { pushState } from '../utils'; import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch( export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
rootState.project.id, state.project.id,
{ {
branch, branch,
ref: rootState.currentBranch, ref: state.currentBranch,
}, },
).then(res => res.json()) ).then(res => res.json())
.then((data) => { .then((data) => {
const branchName = data.name; const branchName = data.name;
const url = location.href.replace(rootState.currentBranch, branchName); const url = location.href.replace(state.currentBranch, branchName);
pushState(url); pushState(url);
......
...@@ -51,6 +51,9 @@ export const decorateData = (entity) => { ...@@ -51,6 +51,9 @@ export const decorateData = (entity) => {
parentTreeUrl = '', parentTreeUrl = '',
level = 0, level = 0,
base64 = false, base64 = false,
file_lock,
} = entity; } = entity;
return { return {
...@@ -72,6 +75,9 @@ export const decorateData = (entity) => { ...@@ -72,6 +75,9 @@ export const decorateData = (entity) => {
renderError, renderError,
content, content,
base64, base64,
file_lock,
}; };
}; };
......
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
...@@ -14,7 +14,7 @@ export default { ...@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0; return this.mr.divergedCommitsCount > 0;
}, },
commitsText() { commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount); return pluralize('commit', this.mr.divergedCommitsCount);
}, },
branchNameClipboardData() { branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that // This supports code in app/assets/javascripts/copy_to_clipboard.js that
......
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
return this.mr.hasCI; return this.mr.hasCI;
}, },
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return this.mr.relatedLinks; return !!this.mr.relatedLinks;
}, },
shouldRenderDeployments() { shouldRenderDeployments() {
return this.mr.deployments.length; return this.mr.deployments.length;
......
...@@ -35,6 +35,11 @@ export default { ...@@ -35,6 +35,11 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
containerClass: {
type: String,
required: false,
default: 'btn btn-align-content',
},
}, },
components: { components: {
loadingIcon, loadingIcon,
...@@ -49,9 +54,9 @@ export default { ...@@ -49,9 +54,9 @@ export default {
<template> <template>
<button <button
class="btn btn-align-content"
@click="onClick" @click="onClick"
type="button" type="button"
:class="containerClass"
:disabled="loading || disabled" :disabled="loading || disabled"
> >
<transition name="fade"> <transition name="fade">
......
import bp from './breakpoints'; import bp from './breakpoints';
import { slugify } from './lib/utils/text_utility';
export default class Wikis { export default class Wikis {
constructor() { constructor() {
...@@ -23,7 +24,7 @@ export default class Wikis { ...@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return; if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
const slug = gl.text.slugify(slugInput.value); const slug = slugify(slugInput.value);
if (slug.length > 0) { if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path'); const wikisPath = slugInput.getAttribute('data-wikis-path');
......
...@@ -353,3 +353,7 @@ ...@@ -353,3 +353,7 @@
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
} }
.flex-right {
margin-left: auto;
}
...@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
} }
} }
.new-epic-dropdown {
.dropdown-menu {
padding-left: $gl-padding-top;
padding-right: $gl-padding-top;
}
.form-control {
width: 100%;
}
.btn-save {
display: flex;
margin-top: $gl-btn-padding;
}
}
.empty-state .new-epic-dropdown {
display: inline-flex;
.btn-save {
margin-left: 0;
margin-bottom: 0;
}
.btn-new {
margin: 0;
}
}
...@@ -101,13 +101,13 @@ ...@@ -101,13 +101,13 @@
@for $i from 0 through 5 { @for $i from 0 through 5 {
.legend-box-#{$i} { .legend-box-#{$i} {
background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
} }
} }
@for $i from 1 through 4 { @for $i from 1 through 4 {
.legend-box-#{$i + 5} { .legend-box-#{$i + 5} {
background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
} }
} }
} }
...@@ -200,13 +200,13 @@ ...@@ -200,13 +200,13 @@
@for $i from 0 through 5 { @for $i from 0 through 5 {
td.blame-commit-age-#{$i} { td.blame-commit-age-#{$i} {
border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
} }
} }
@for $i from 1 through 4 { @for $i from 1 through 4 {
td.blame-commit-age-#{$i + 5} { td.blame-commit-age-#{$i + 5} {
border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
} }
} }
} }
......
...@@ -401,10 +401,13 @@ ...@@ -401,10 +401,13 @@
.breadcrumbs-list { .breadcrumbs-list {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
flex-wrap: wrap;
margin-bottom: 0; margin-bottom: 0;
line-height: 16px; line-height: 16px;
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
> li { > li {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -412,24 +415,35 @@ ...@@ -412,24 +415,35 @@
padding: 2px 0; padding: 2px 0;
&:not(:last-child) { &:not(:last-child) {
margin-right: 20px; padding-right: 20px;
&:not(.dropdown) {
overflow: hidden;
}
} }
> a { > a {
font-size: 12px; font-size: 12px;
color: currentColor; color: currentColor;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 0 1 auto;
} }
} }
} }
.breadcrumb-item-text { .breadcrumb-item-text {
@include str-truncated(128px);
text-decoration: inherit; text-decoration: inherit;
@media (max-width: $screen-xs-max) {
@include str-truncated(128px);
}
} }
.breadcrumbs-list-angle { .breadcrumbs-list-angle {
position: absolute; position: absolute;
right: -12px; right: 7px;
top: 50%; top: 50%;
color: $gl-text-color-tertiary; color: $gl-text-color-tertiary;
transform: translateY(-50%); transform: translateY(-50%);
......
...@@ -164,7 +164,7 @@ $gl-text-color: #2e2e2e; ...@@ -164,7 +164,7 @@ $gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070; $gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494; $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6; $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1.0); $gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600; $gl-text-green: $green-600;
$gl-text-green-hover: $green-700; $gl-text-green-hover: $green-700;
...@@ -493,8 +493,8 @@ $callout-success-color: $green-700; ...@@ -493,8 +493,8 @@ $callout-success-color: $green-700;
/* /*
* Commit Page * Commit Page
*/ */
$commit-max-width-marker-color: rgba(0, 0, 0, 0.0); $commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0.0); $commit-message-text-area-bg: rgba(0, 0, 0, 0);
/* /*
* Common * Common
......
...@@ -333,7 +333,7 @@ ...@@ -333,7 +333,7 @@
.prometheus-graph-overlay { .prometheus-graph-overlay {
fill: none; fill: none;
opacity: 0.0; opacity: 0;
pointer-events: all; pointer-events: all;
} }
......
...@@ -697,6 +697,7 @@ ...@@ -697,6 +697,7 @@
.issue-main-info { .issue-main-info {
flex: 1 auto; flex: 1 auto;
margin-right: 10px; margin-right: 10px;
min-width: 0;
} }
.issuable-meta { .issuable-meta {
......
@import "./issues/issue_count_badge"; @import "./issues/issue_count_badge";
@import "./issues/related_issues";
.issues-list { .issues-list {
.issue { .issue {
......
...@@ -18,7 +18,7 @@ $token_spacing_bottom: 0.5em; ...@@ -18,7 +18,7 @@ $token_spacing_bottom: 0.5em;
} }
.related-issues-token-body { .related-issues-token-body {
padding-bottom: calc(#{$gl-padding} - #{$token_spacing_bottom}); padding: 0;
transition-property: max-height, padding, opacity; transition-property: max-height, padding, opacity;
transition-duration: $general-hover-transition-duration; transition-duration: $general-hover-transition-duration;
transition-timing-function: $general-hover-transition-curve; transition-timing-function: $general-hover-transition-curve;
...@@ -30,6 +30,18 @@ $token_spacing_bottom: 0.5em; ...@@ -30,6 +30,18 @@ $token_spacing_bottom: 0.5em;
padding-bottom: 0; padding-bottom: 0;
opacity: 0; opacity: 0;
} }
li .issue-info-container {
padding-left: $gl-padding;
}
a.issue-main-info:hover {
text-decoration: none;
.issue-token-title-text {
text-decoration: underline;
}
}
} }
.related-issues-loading-icon { .related-issues-loading-icon {
...@@ -50,3 +62,7 @@ $token_spacing_bottom: 0.5em; ...@@ -50,3 +62,7 @@ $token_spacing_bottom: 0.5em;
margin-bottom: $token_spacing_bottom; margin-bottom: $token_spacing_bottom;
margin-right: 5px; margin-right: 5px;
} }
.issue-token-end {
order: 1;
}
...@@ -715,13 +715,16 @@ ...@@ -715,13 +715,16 @@
.approvals-footer { .approvals-footer {
display: flex; display: flex;
.approvers-prefix, .approvers-prefix {
.approvers-list {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.approvers-list { .approvers-list {
display: flex;
align-items: center;
.link-to-member-avatar:not(:first-child) { .link-to-member-avatar:not(:first-child) {
img { img {
margin-left: 0; margin-left: 0;
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.diff-file .diff-content { .diff-file .diff-content {
tr.line_holder:hover > td .line_note_link { tr.line_holder:hover > td .line_note_link {
opacity: 1.0; opacity: 1;
filter: alpha(opacity = 100); filter: alpha(opacity = 100);
} }
} }
......
...@@ -289,6 +289,13 @@ ...@@ -289,6 +289,13 @@
color: $almost-black; color: $almost-black;
} }
} }
.file-status-icon {
width: 10px;
height: 10px;
margin-left: 3px;
}
} }
.render-error { .render-error {
......
...@@ -274,3 +274,22 @@ ...@@ -274,3 +274,22 @@
} }
} }
} }
.modal-doorkeepr-auth,
.doorkeeper-app-form {
.scope-description {
color: $theme-gray-700;
}
}
.modal-doorkeepr-auth {
.modal-body {
padding: $gl-padding;
}
}
.doorkeeper-app-form {
.scope-description {
margin: 0 0 5px 17px;
}
}
...@@ -76,7 +76,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -76,7 +76,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def redirect_out_of_range(todos) def redirect_out_of_range(todos)
total_pages = total_pages =
if todo_params.except(:sort, :page).empty? if todo_params.except(:sort, :page).empty?
(current_user.todos_pending_count / todos.limit_value).ceil (current_user.todos_pending_count.to_f / todos.limit_value).ceil
else else
todos.total_pages todos.total_pages
end end
......
...@@ -52,15 +52,13 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -52,15 +52,13 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs) contents.push(*tree.blobs)
contents.push(*tree.submodules) contents.push(*tree.submodules)
show_path_locks = @project.feature_available?(:file_locks) && @project.path_locks.any?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
@logs = Gitlab::GitalyClient.allow_n_plus_1_calls do @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
contents[@offset, @limit].to_a.map do |content| contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file) last_commit = @repo.last_commit_for_path(@commit.id, file)
commit_path = project_commit_path(@project, last_commit) if last_commit commit_path = project_commit_path(@project, last_commit) if last_commit
path_lock = show_path_locks && @project.find_path_lock(file) path_lock = @project.find_path_lock(file)
{ {
file_name: content.name, file_name: content.name,
......
module EE module EE
module LockHelper module LockHelper
def lock_file_link(project = @project, path = @path, html_options: {}) def lock_file_link(project = @project, path = @path, html_options: {})
return unless project.feature_available?(:file_locks) && current_user return unless current_user
return if path.blank? return if path.blank?
path_lock = project.find_path_lock(path, downstream: true) path_lock = project.find_path_lock(path, downstream: true)
...@@ -67,7 +67,6 @@ module EE ...@@ -67,7 +67,6 @@ module EE
end end
def render_lock_icon(path) def render_lock_icon(path)
return unless @project.feature_available?(:file_locks)
return unless @project.root_ref?(@ref) return unless @project.root_ref?(@ref)
if file_lock = @project.find_path_lock(path, exact_match: true) if file_lock = @project.find_path_lock(path, exact_match: true)
......
...@@ -23,10 +23,17 @@ module IconsHelper ...@@ -23,10 +23,17 @@ module IconsHelper
render "shared/icons/#{icon_name}.svg", size: size render "shared/icons/#{icon_name}.svg", size: size
end end
def sprite_icon_path
# SVG Sprites currently don't work across domains, so in the case of a CDN
# we have to set the current path deliberately to prevent addition of asset_host
sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
end
def sprite_icon(icon_name, size: nil, css_class: nil) def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : "" css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank? css_classes << " #{css_class}" unless css_class.blank?
content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end end
def audit_icon(names, options = {}) def audit_icon(names, options = {})
......
...@@ -215,6 +215,7 @@ module IssuablesHelper ...@@ -215,6 +215,7 @@ module IssuablesHelper
endpoint: issuable_path(issuable), endpoint: issuable_path(issuable),
canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable), canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable), canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
canAdmin: can?(current_user, :"admin_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(parent), markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
...@@ -228,6 +229,7 @@ module IssuablesHelper ...@@ -228,6 +229,7 @@ module IssuablesHelper
if parent.is_a?(Group) if parent.is_a?(Group)
data[:groupPath] = parent.path data[:groupPath] = parent.path
data[:issueLinksEndpoint] = group_epic_issues_path(parent, issuable)
else else
data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path) data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
end end
......
...@@ -4,15 +4,26 @@ module Avatarable ...@@ -4,15 +4,26 @@ module Avatarable
def avatar_path(only_path: true) def avatar_path(only_path: true)
return unless self[:avatar].present? return unless self[:avatar].present?
# If only_path is true then use the relative path of avatar.
# Otherwise use full path (including host).
asset_host = ActionController::Base.asset_host asset_host = ActionController::Base.asset_host
gitlab_host = only_path ? gitlab_config.relative_url_root : gitlab_config.url use_asset_host = asset_host.present?
# If asset_host is set then it is expected that assets are handled by a standalone host. # Avatars for private and internal groups and projects require authentication to be viewed,
# That means we do not want to get GitLab's relative_url_root option anymore. # which means they can only be served by Rails, on the regular GitLab host.
host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host # If an asset host is configured, we need to return the fully qualified URL
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
if use_asset_host && respond_to?(:public?) && !public?
use_asset_host = false
only_path = false
end
url_base = ""
if use_asset_host
url_base << asset_host unless only_path
else
url_base << gitlab_config.base_url unless only_path
url_base << gitlab_config.relative_url_root
end
[host, avatar.url].join url_base + avatar.url
end end
end end
...@@ -38,6 +38,9 @@ class Issue < ActiveRecord::Base ...@@ -38,6 +38,9 @@ class Issue < ActiveRecord::Base
has_many :issue_assignees has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees
has_one :epic_issue
has_one :epic, through: :epic_issue
validates :project, presence: true validates :project, presence: true
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
......
...@@ -891,7 +891,19 @@ class MergeRequest < ActiveRecord::Base ...@@ -891,7 +891,19 @@ class MergeRequest < ActiveRecord::Base
# #
def all_commit_shas def all_commit_shas
if persisted? if persisted?
column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha') # MySQL doesn't support LIMIT in a subquery.
diffs_relation =
if Gitlab::Database.postgresql?
merge_request_diffs.order(id: :desc).limit(100)
else
merge_request_diffs
end
column_shas = MergeRequestDiffCommit
.where(merge_request_diff: diffs_relation)
.limit(10_000)
.pluck('sha')
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas) serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq (column_shas + serialised_shas).uniq
......
...@@ -635,6 +635,8 @@ class Project < ActiveRecord::Base ...@@ -635,6 +635,8 @@ class Project < ActiveRecord::Base
else else
super super
end end
rescue
super
end end
def valid_import_url? def valid_import_url?
......
...@@ -1124,6 +1124,10 @@ class Repository ...@@ -1124,6 +1124,10 @@ class Repository
blob_data_at(sha, path) blob_data_at(sha, path)
end end
def fetch_ref(source_repository, source_ref:, target_ref:)
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
private private
# TODO Generice finder, later split this on finders by Ref or Oid # TODO Generice finder, later split this on finders by Ref or Oid
......
...@@ -10,4 +10,10 @@ class BlobEntity < Grape::Entity ...@@ -10,4 +10,10 @@ class BlobEntity < Grape::Entity
expose :url do |blob| expose :url do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path)) project_blob_path(request.project, File.join(request.ref, blob.path))
end end
expose :file_lock, using: FileLockEntity do |blob|
if request.project.root_ref?(request.ref)
request.project.find_path_lock(blob.path, exact_match: true)
end
end
end end
class FileLockEntity < Grape::Entity
expose :user, using: API::Entities::UserSafe
end
...@@ -3,7 +3,6 @@ class IssueEntity < IssuableEntity ...@@ -3,7 +3,6 @@ class IssueEntity < IssuableEntity
expose :state expose :state
expose :deleted_at expose :deleted_at
expose :branch_name
expose :confidential expose :confidential
expose :discussion_locked expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic expose :assignees, using: API::Entities::UserBasic
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# when the user is destroyed. # when the user is destroyed.
module Users module Users
class MigrateToGhostUserService class MigrateToGhostUserService
prepend EE::Users::MigrateToGhostUserService
extend ActiveSupport::Concern extend ActiveSupport::Concern
attr_reader :ghost_user, :user attr_reader :ghost_user, :user
......
= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f| = form_for application, url: doorkeeper_submit_path(application), html: { role: 'form', class: 'doorkeeper-app-form' } do |f|
= form_errors(application) = form_errors(application)
.form-group .form-group
......
- auth_app_owner = @pre_auth.client.application.owner
%main{ :role => "main" } %main{ :role => "main" }
.modal-no-backdrop .modal-no-backdrop.modal-doorkeepr-auth
.modal-content .modal-content
.modal-header .modal-header
%h3.page-title %h3.page-title
...@@ -16,14 +18,21 @@ ...@@ -16,14 +18,21 @@
%strong= @pre_auth.client.name %strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution. will allow them to interact with GitLab as an admin as well. Proceed with caution.
%p %p
You are about to authorize An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer' = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
to use your account. is requesting access to your GitLab account. This application was created by
= succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner)
Please note that this application is not provided by GitLab and you should verify its authenticity before
allowing access.
- if @pre_auth.scopes - if @pre_auth.scopes
%p
This application will be able to: This application will be able to:
%ul %ul
- @pre_auth.scopes.each do |scope| - @pre_auth.scopes.each do |scope|
%li= t scope, scope: [:doorkeeper, :scopes] %li
%strong= t scope, scope: [:doorkeeper, :scopes]
.scope-description= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right .form-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid = hidden_field_tag :client_id, @pre_auth.client.uid
......
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- epics = EpicsFinder.new(current_user, group_id: @group.id).execute
- epics_items = ['epics#show', 'epics#index']
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] - issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards) - if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show') - issues_sub_menu_items.push('boards#index', 'boards#show')
...@@ -45,20 +43,8 @@ ...@@ -45,20 +43,8 @@
%span %span
Contribution Analytics Contribution Analytics
-# TODO: Add the flag check to only show epics if available
= nav_link(path: epics_items) do = render "layouts/nav/ee/epic_link", group: @group
= link_to group_epics_path(@group) do
.nav-icon-container
= sprite_icon('epic')
%span.nav-item-name
Epics
%span.badge.count= number_with_delimiter(epics.count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: epics_items, html_options: { class: "fly-out-top-item" } ) do
= link_to group_epics_path(@group) do
%strong.fly-out-top-item-name
#{ _('Epics') }
%span.badge.count.epic_counter.fly-out-badge= number_with_delimiter(epics.count)
= nav_link(path: issues_sub_menu_items) do = nav_link(path: issues_sub_menu_items) do
= link_to issues_group_path(@group) do = link_to issues_group_path(@group) do
......
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag ("#{prefix}_scopes_#{scope}"), scope = label_tag ("#{prefix}_scopes_#{scope}"), scope
%span= t(scope, scope: [:doorkeeper, :scopes]) %span= t(scope, scope: [:doorkeeper, :scopes])
.scope-description= t scope, scope: [:doorkeeper, :scope_desc]
...@@ -2,23 +2,18 @@ ...@@ -2,23 +2,18 @@
# vim: ft=ruby # vim: ft=ruby
require 'rubygems' require 'rubygems'
require 'bundler/setup' require 'bundler/setup'
# loads rails environment / initializers
require "#{File.dirname(__FILE__)}/../config/environment"
require 'optparse' require 'optparse'
class GeoLogCursorOptionParser class GeoLogCursorOptionParser
def self.parse(argv) def self.parse(argv)
options = { full_scan: false } options = {}
version = Gitlab::Geo::LogCursor::Daemon::VERSION
op = OptionParser.new op = OptionParser.new
op.banner = 'GitLab Geo: Log Cursor' op.banner = 'GitLab Geo: Log Cursor'
op.separator '' op.separator ''
op.separator 'Usage: ./geo_log_cursor [options]' op.separator 'Usage: ./geo_log_cursor [options]'
op.separator '' op.separator ''
op.on('-f', '--full-scan', 'Performs full-scan to lookup for un-replicated data') { options[:full_scan] = true } op.on('-d', '--debug', 'Enable detailed logging with extra debug information') { options[:debug] = true }
op.separator 'Common options:' op.separator 'Common options:'
op.on('-h', '--help') do op.on('-h', '--help') do
...@@ -26,7 +21,10 @@ class GeoLogCursorOptionParser ...@@ -26,7 +21,10 @@ class GeoLogCursorOptionParser
exit exit
end end
op.on('-v', '--version') do op.on('-v', '--version') do
puts version # Load only necessary libraries for faster startup
require "#{File.dirname(__FILE__)}/../lib/gitlab/geo/log_cursor/daemon"
puts Gitlab::Geo::LogCursor::Daemon::VERSION
exit exit
end end
op.separator '' op.separator ''
...@@ -40,5 +38,8 @@ end ...@@ -40,5 +38,8 @@ end
if $0 == __FILE__ if $0 == __FILE__
options = GeoLogCursorOptionParser.parse(ARGV) options = GeoLogCursorOptionParser.parse(ARGV)
# We load rails environment / initializers only here to get faster command line startup when `--help` and `--version`
require "#{File.dirname(__FILE__)}/../config/environment"
Gitlab::Geo::LogCursor::Daemon.new(options).run! Gitlab::Geo::LogCursor::Daemon.new(options).run!
end end
---
title: Fix Merge Request Widget Approvals responsiveness on mobile
merge_request:
author:
type: fixed
---
title: Introduce EEU lincese with epics as the first feature
merge_request:
author:
type: added
---
title: Add ability to create new epics
merge_request:
author:
type: added
---
title: Remove the full-scan option from the Geo log cursor
merge_request: 3412
author:
type: removed
---
title: Fix error when entering an invalid url to push to or pull from a remote repository
merge_request: 3389
author:
type: fixed
--- ---
title: Adds typescript support title: Add delete epic button
merge_request: merge_request:
author: author:
type: added type: added
---
title: Hide Approvals section when Merge Request Widget is showing the empty state
merge_request: 3376
author:
type: fixed
---
title: Make GeoLogCursor Highly Available
merge_request: 3305
author:
type: added
---
title: Enables scroll to bottom once user has scrolled back to bottom in job log
merge_request:
author:
type: fixed
---
title: Fix acceptance of username for Mattermost service update
merge_request: 15275
author:
type: fixed
---
title: Clean up schema of the "issues" table
merge_request:
author:
type: other
---
title: Always return full avatar URL for private/internal groups/projects when asset
host is set
merge_request:
author:
type: fixed
---
title: Ensure merge requests with lots of version don't time out when searching for
pipelines
merge_request:
author:
type: performance
---
title: Fix access to the final page of todos
merge_request:
author:
type: fixed
---
title: Export text utils functions as es6 module and add tests
merge_request:
author:
type: other
...@@ -62,7 +62,15 @@ en: ...@@ -62,7 +62,15 @@ en:
read_user: Read the authenticated user's personal information read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect openid: Authenticate using OpenID Connect
sudo: Perform API actions as any user in the system (if the authenticated user is an admin) sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
scope_desc:
api:
Full access to GitLab as the user, including read/write on all their groups and projects
read_user:
Read-only access to the user's profile information, like username, public email and full name
openid:
The ability to authenticate using GitLab, and read-only access to the user's profile information
sudo:
Access to the Sudo feature, to perform API actions as any user in the system (only available for admins)
flash: flash:
applications: applications:
create: create:
......
...@@ -145,10 +145,10 @@ ...@@ -145,10 +145,10 @@
- container_memory_usage_bytes - container_memory_usage_bytes
weight: 1 weight: 1
queries: queries:
- query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"})) /1024/1024' - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024'
label: Average label: Average
unit: MB unit: MB
- query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"})) /1024/1024' - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job)) /1024/1024'
label: Average label: Average
unit: MB unit: MB
track: canary track: canary
...@@ -158,10 +158,10 @@ ...@@ -158,10 +158,10 @@
- container_cpu_usage_seconds_total - container_cpu_usage_seconds_total
weight: 1 weight: 1
queries: queries:
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) * 100' - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
label: CPU label: Average
unit: "%" unit: "%"
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) * 100' - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) without (job)) * 100'
label: CPU label: Average
unit: "%" unit: "%"
track: canary track: canary
...@@ -78,6 +78,8 @@ constraints(GroupUrlConstrainer.new) do ...@@ -78,6 +78,8 @@ constraints(GroupUrlConstrainer.new) do
member do member do
get :realtime_changes get :realtime_changes
end end
resources :epic_issues, only: [:index, :create, :destroy], as: 'issues', path: 'issues'
end end
legacy_ee_group_boards_redirect = redirect do |params, request| legacy_ee_group_boards_redirect = redirect do |params, request|
......
...@@ -41,6 +41,7 @@ var config = { ...@@ -41,6 +41,7 @@ var config = {
environments: './environments/environments_bundle.js', environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js', environments_folder: './environments/folder/environments_folder_bundle.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js', epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
new_epic: 'ee/epics/new_epic/new_epic_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js', graphs: './graphs/graphs_bundle.js',
graphs_charts: './graphs/graphs_charts.js', graphs_charts: './graphs/graphs_charts.js',
...@@ -117,10 +118,6 @@ var config = { ...@@ -117,10 +118,6 @@ var config = {
test: /\.vue$/, test: /\.vue$/,
loader: 'vue-loader', loader: 'vue-loader',
}, },
{
test: /\.ts$/,
loader: 'ts-loader',
},
{ {
test: /\.svg$/, test: /\.svg$/,
loader: 'raw-loader', loader: 'raw-loader',
...@@ -269,7 +266,7 @@ var config = { ...@@ -269,7 +266,7 @@ var config = {
], ],
resolve: { resolve: {
extensions: ['.js', '.ts'], extensions: ['.js'],
alias: { alias: {
'ee': path.join(ROOT_PATH, 'ee/app/assets/javascripts'), 'ee': path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
'~': path.join(ROOT_PATH, 'app/assets/javascripts'), '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class IssuesConfidentialNotNull < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
class Issue < ActiveRecord::Base
self.table_name = 'issues'
end
def up
Issue.where('confidential IS NULL').update_all(confidential: false)
change_column_null :issues, :confidential, false
end
def down
# There's no way / point to revert this.
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class IssuesMilestoneIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class Issue < ActiveRecord::Base
include EachBatch
self.table_name = 'issues'
def self.with_orphaned_milestones
where('NOT EXISTS (SELECT true FROM milestones WHERE milestones.id = issues.milestone_id)')
end
end
def up
Issue.with_orphaned_milestones.each_batch(of: 100) do |batch|
batch.update_all(milestone_id: nil)
end
add_concurrent_foreign_key(
:issues,
:milestones,
column: :milestone_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:issues, column: :milestone_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class IssuesUpdatedByIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class Issue < ActiveRecord::Base
include EachBatch
self.table_name = 'issues'
def self.with_orphaned_updaters
where('NOT EXISTS (SELECT true FROM users WHERE users.id = issues.updated_by_id)')
.where('updated_by_id IS NOT NULL')
end
end
def up
Issue.with_orphaned_updaters.each_batch(of: 100) do |batch|
batch.update_all(updated_by_id: nil)
end
# This index is only used for foreign keys, and those in turn will always
# specify a value. As such we can add a WHERE condition to make the index
# smaller.
add_concurrent_index(:issues, :updated_by_id, where: 'updated_by_id IS NOT NULL')
add_concurrent_foreign_key(
:issues,
:users,
column: :updated_by_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:issues, column: :updated_by_id)
remove_concurrent_index(:issues, :updated_by_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class IssuesMovedToIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class Issue < ActiveRecord::Base
include EachBatch
self.table_name = 'issues'
def self.with_orphaned_moved_to_issues
where('NOT EXISTS (SELECT true FROM issues WHERE issues.id = issues.moved_to_id)')
.where('moved_to_id IS NOT NULL')
end
end
def up
Issue.with_orphaned_moved_to_issues.each_batch(of: 100) do |batch|
batch.update_all(moved_to_id: nil)
end
add_concurrent_foreign_key(
:issues,
:issues,
column: :moved_to_id,
on_delete: :nullify
)
# We're using a partial index here so we only index the data we actually
# care about.
add_concurrent_index(:issues, :moved_to_id, where: 'moved_to_id IS NOT NULL')
end
def down
remove_foreign_key_without_error(:issues, column: :moved_to_id)
remove_concurrent_index(:issues, :moved_to_id)
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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