Commit 25b97e94 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'ee/master' into add-api-shared_runners_minutes_limit

* ee/master: (183 commits)
  Improve has_codeclimate_data? spec in the merge_request_spec.rb
  Refactor Elasticsearch checks to SystemCheck
  Refactor and move things around to improve in YAGNI perspective
  Fix BasicExecutor specs
  Changelog
  Fixed and improved some existing checks and SystemCheck library
  Fix codestyle
  Improve Specs and some fixes
  Add ActiveUsers Check
  Refactor gitlab:app:checks to use SystemCheck
  Added specs for BaseExecutor
  Added specs for SystemCheck and custom matcher
  Fix a few method signature checks
  Move rainbow monkey patch to String to spec_helper
  Some code-style fixes and documentation
  WIP SystemCheck library for executing checks from a rake task
  Persist Clone URL prefix in geo_nodes table to be used by the secondary
  Add small improvements to code climate ci docs
  Changelog
  Refactor codeclimate related code for build and pipeline
  ...
parents 0ed08366 eeb7245f
...@@ -473,24 +473,6 @@ lint:javascript:report: ...@@ -473,24 +473,6 @@ lint:javascript:report:
paths: paths:
- eslint-report.html - eslint-report.html
# Trigger docs build
# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
trigger_docs:
stage: post-test
image: "alpine"
<<: *dedicated-runner
before_script:
- apk update && apk add curl
variables:
GIT_STRATEGY: "none"
cache: {}
artifacts: {}
script:
- "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
- if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
only:
- master@gitlab-org/gitlab-ee
pages: pages:
before_script: [] before_script: []
stage: pages stage: pages
......
...@@ -11,11 +11,11 @@ linters: ...@@ -11,11 +11,11 @@ linters:
# !global, !important, and !optional flags. # !global, !important, and !optional flags.
BangFormat: BangFormat:
enabled: false enabled: false
# Whether or not to prefer `border: 0` over `border: none`. # Whether or not to prefer `border: 0` over `border: none`.
BorderZero: BorderZero:
enabled: false enabled: false
# Reports when you define a rule set using a selector with chained classes # Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes). # (a.k.a. adjoining classes).
ChainedClasses: ChainedClasses:
...@@ -25,13 +25,13 @@ linters: ...@@ -25,13 +25,13 @@ linters:
# (e.g. `color: green` is a color keyword) # (e.g. `color: green` is a color keyword)
ColorKeyword: ColorKeyword:
enabled: false enabled: false
# Prefer color literals (keywords or hexadecimal codes) to be used only in # Prefer color literals (keywords or hexadecimal codes) to be used only in
# variable declarations. They should be referred to via variables everywhere # variable declarations. They should be referred to via variables everywhere
# else. # else.
ColorVariable: ColorVariable:
enabled: true enabled: true
# Which form of comments to prefer in CSS. # Which form of comments to prefer in CSS.
Comment: Comment:
enabled: false enabled: false
...@@ -39,7 +39,7 @@ linters: ...@@ -39,7 +39,7 @@ linters:
# Reports @debug statements (which you probably left behind accidentally). # Reports @debug statements (which you probably left behind accidentally).
DebugStatement: DebugStatement:
enabled: false enabled: false
# Rule sets should be ordered as follows: # Rule sets should be ordered as follows:
# - @extend declarations # - @extend declarations
# - @include declarations without inner @content # - @include declarations without inner @content
...@@ -54,19 +54,19 @@ linters: ...@@ -54,19 +54,19 @@ linters:
# more information. # more information.
DisableLinterReason: DisableLinterReason:
enabled: true enabled: true
# Reports when you define the same property twice in a single rule set. # Reports when you define the same property twice in a single rule set.
DuplicateProperty: DuplicateProperty:
enabled: false enabled: true
# Separate rule, function, and mixin declarations with empty lines. # Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks: EmptyLineBetweenBlocks:
enabled: true enabled: true
# Reports when you have an empty rule set. # Reports when you have an empty rule set.
EmptyRule: EmptyRule:
enabled: true enabled: true
# Reports when you have an @extend directive. # Reports when you have an @extend directive.
ExtendDirective: ExtendDirective:
enabled: false enabled: false
...@@ -75,49 +75,49 @@ linters: ...@@ -75,49 +75,49 @@ linters:
# when adding lines to the file, since SCM systems such as git won't # when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line. # think that you touched the last line.
FinalNewline: FinalNewline:
enabled: false enabled: true
# HEX colors should use three-character values where possible. # HEX colors should use three-character values where possible.
HexLength: HexLength:
enabled: false enabled: false
# HEX color values should use lower-case colors to differentiate between # HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation: HexNotation:
enabled: true enabled: true
# Avoid using ID selectors. # Avoid using ID selectors.
IdSelector: IdSelector:
enabled: false enabled: false
# The basenames of @imported SCSS partials should not begin with an # The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension. # underscore and should not include the filename extension.
ImportPath: ImportPath:
enabled: false enabled: false
# Avoid using !important in properties. It is usually indicative of a # Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code. # misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule: ImportantRule:
enabled: false enabled: false
# Indentation should always be done in increments of 2 spaces. # Indentation should always be done in increments of 2 spaces.
Indentation: Indentation:
enabled: true enabled: true
width: 2 width: 2
# Don't write leading zeros for numeric values with a decimal point. # Don't write leading zeros for numeric values with a decimal point.
LeadingZero: LeadingZero:
enabled: false enabled: false
# Reports when you define the same selector twice in a single sheet. # Reports when you define the same selector twice in a single sheet.
MergeableSelector: MergeableSelector:
enabled: false enabled: false
# Functions, mixins, variables, and placeholders should be declared # Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores. # with all lowercase letters and hyphens instead of underscores.
NameFormat: NameFormat:
enabled: false enabled: false
# Avoid nesting selectors too deeply. # Avoid nesting selectors too deeply.
NestingDepth: NestingDepth:
enabled: false enabled: false
...@@ -129,12 +129,12 @@ linters: ...@@ -129,12 +129,12 @@ linters:
# Sort properties in a strict order. # Sort properties in a strict order.
PropertySortOrder: PropertySortOrder:
enabled: false enabled: false
# Reports when you use an unknown or disabled CSS property # Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties). # (ignoring vendor-prefixed properties).
PropertySpelling: PropertySpelling:
enabled: false enabled: false
# Configure which units are allowed for property values. # Configure which units are allowed for property values.
PropertyUnits: PropertyUnits:
enabled: false enabled: false
...@@ -144,25 +144,25 @@ linters: ...@@ -144,25 +144,25 @@ linters:
# be declared with one colon. # be declared with one colon.
PseudoElement: PseudoElement:
enabled: true enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying"). # Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement: QualifyingElement:
enabled: false enabled: false
# Don't write selectors with a depth of applicability greater than 3. # Don't write selectors with a depth of applicability greater than 3.
SelectorDepth: SelectorDepth:
enabled: false enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or # Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case. # snake_case.
SelectorFormat: SelectorFormat:
enabled: false enabled: false
convention: hyphenated_lowercase convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it. # Prefer the shortest shorthand form possible for properties that support it.
Shorthand: Shorthand:
enabled: true enabled: true
# Each property should have its own line, except in the special case of # Each property should have its own line, except in the special case of
# single line rulesets. # single line rulesets.
SingleLinePerProperty: SingleLinePerProperty:
...@@ -173,11 +173,11 @@ linters: ...@@ -173,11 +173,11 @@ linters:
# individual selector occupy a single line. # individual selector occupy a single line.
SingleLinePerSelector: SingleLinePerSelector:
enabled: true enabled: true
# Commas in lists should be followed by a space. # Commas in lists should be followed by a space.
SpaceAfterComma: SpaceAfterComma:
enabled: false enabled: false
# Properties should be formatted with a single space separating the colon # Properties should be formatted with a single space separating the colon
# from the property's value. # from the property's value.
SpaceAfterPropertyColon: SpaceAfterPropertyColon:
...@@ -197,12 +197,12 @@ linters: ...@@ -197,12 +197,12 @@ linters:
# colon. # colon.
SpaceAfterVariableName: SpaceAfterVariableName:
enabled: false enabled: false
# Operators should be formatted with a single space on both sides of an # Operators should be formatted with a single space on both sides of an
# infix operator. # infix operator.
SpaceAroundOperator: SpaceAroundOperator:
enabled: true enabled: true
# Opening braces should be preceded by a single space. # Opening braces should be preceded by a single space.
SpaceBeforeBrace: SpaceBeforeBrace:
enabled: true enabled: true
...@@ -210,7 +210,7 @@ linters: ...@@ -210,7 +210,7 @@ linters:
# Parentheses should not be padded with spaces. # Parentheses should not be padded with spaces.
SpaceBetweenParens: SpaceBetweenParens:
enabled: false enabled: false
# Enforces that string literals should be written with a consistent form # Enforces that string literals should be written with a consistent form
# of quotes (single or double). # of quotes (single or double).
StringQuotes: StringQuotes:
...@@ -241,7 +241,7 @@ linters: ...@@ -241,7 +241,7 @@ linters:
# be unnecessary. # be unnecessary.
UnnecessaryParentReference: UnnecessaryParentReference:
enabled: false enabled: false
# URLs should be valid and not contain protocols or domain names. # URLs should be valid and not contain protocols or domain names.
UrlFormat: UrlFormat:
enabled: true enabled: true
......
...@@ -366,7 +366,7 @@ GEM ...@@ -366,7 +366,7 @@ GEM
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grpc (1.2.5) grpc (1.3.4)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
gssapi (1.2.0) gssapi (1.2.0)
......
...@@ -124,12 +124,12 @@ import ApproversSelect from './approvers_select'; ...@@ -124,12 +124,12 @@ import ApproversSelect from './approvers_select';
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:builds:show': case 'projects:jobs:show':
new Build(); new Build();
break; break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
if (gl.FilteredSearchManager) { if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
const filteredSearchManager = new gl.FilteredSearchManager( const filteredSearchManager = new gl.FilteredSearchManager(
page === 'projects:issues:index' ? 'issues' : 'merge_requests', page === 'projects:issues:index' ? 'issues' : 'merge_requests',
); );
......
...@@ -194,6 +194,7 @@ window.DropzoneInput = (function() { ...@@ -194,6 +194,7 @@ window.DropzoneInput = (function() {
$(child).val(beforeSelection + formattedText + afterSelection); $(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`; textarea.style.height = `${textarea.scrollHeight}px`;
formTextarea.get(0).dispatchEvent(new Event('input'));
return formTextarea.trigger('input'); return formTextarea.trigger('input');
}; };
......
<script> <script>
/* global Flash */
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index'; import Service from '../services/index';
import Store from '../stores'; import Store from '../stores';
import titleComponent from './title.vue'; import titleComponent from './title.vue';
import descriptionComponent from './description.vue'; import descriptionComponent from './description.vue';
import formComponent from './form.vue';
import '../../lib/utils/url_utility';
export default { export default {
props: { props: {
...@@ -12,15 +16,27 @@ export default { ...@@ -12,15 +16,27 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
canMove: {
required: true,
type: Boolean,
},
canUpdate: { canUpdate: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
canDestroy: {
required: true,
type: Boolean,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
}, },
initialTitle: { initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -34,10 +50,40 @@ export default { ...@@ -34,10 +50,40 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
isConfidential: {
type: Boolean,
required: true,
},
markdownPreviewUrl: {
type: String,
required: true,
},
markdownDocs: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
}, },
data() { data() {
const store = new Store({ const store = new Store({
titleHtml: this.initialTitle, titleHtml: this.initialTitleHtml,
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml, descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText, descriptionText: this.initialDescriptionText,
}); });
...@@ -45,19 +91,97 @@ export default { ...@@ -45,19 +91,97 @@ export default {
return { return {
store, store,
state: store.state, state: store.state,
showForm: false,
}; };
}, },
computed: {
formState() {
return this.store.formState;
},
},
components: { components: {
descriptionComponent, descriptionComponent,
titleComponent, titleComponent,
formComponent,
},
methods: {
openForm() {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
confidential: this.isConfidential,
description: this.state.descriptionText,
lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false,
});
}
},
closeForm() {
this.showForm = false;
},
updateIssuable() {
const canPostUpdate = this.store.formState.move_to_project_id !== 0 ?
confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert
if (!canPostUpdate) {
this.store.setFormState({
updateLoading: false,
});
return;
}
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
} else if (data.confidential !== this.isConfidential) {
gl.utils.visitUrl(location.pathname);
}
return this.service.getData();
})
.then(res => res.json())
.then((data) => {
this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error updating issue');
});
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.then((data) => {
// Stop the poll so we don't get 404's with the issue not existing
this.poll.stop();
gl.utils.visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error deleting issue');
});
},
}, },
created() { created() {
const resource = new Service(this.endpoint); this.service = new Service(this.endpoint);
const poll = new Poll({ this.poll = new Poll({
resource, resource: this.service,
method: 'getData', method: 'getData',
successCallback: (res) => { successCallback: (res) => {
this.store.updateState(res.json()); const data = res.json();
const shouldUpdate = this.store.stateShouldUpdate(data);
this.store.updateState(data);
if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) {
this.store.formState.lockedWarningVisible = true;
}
}, },
errorCallback(err) { errorCallback(err) {
throw new Error(err); throw new Error(err);
...@@ -65,32 +189,57 @@ export default { ...@@ -65,32 +189,57 @@ export default {
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
poll.makeRequest(); this.poll.makeRequest();
} }
Visibility.change(() => { Visibility.change(() => {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
poll.restart(); this.poll.restart();
} else { } else {
poll.stop(); this.poll.stop();
} }
}); });
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<title-component <form-component
:issuable-ref="issuableRef" v-if="canUpdate && showForm"
:title-html="state.titleHtml" :form-state="formState"
:title-text="state.titleText" /> :can-move="canMove"
<description-component :can-destroy="canDestroy"
v-if="state.descriptionHtml" :issuable-templates="issuableTemplates"
:can-update="canUpdate" :markdown-docs="markdownDocs"
:description-html="state.descriptionHtml" :markdown-preview-url="markdownPreviewUrl"
:description-text="state.descriptionText" :project-path="projectPath"
:updated-at="state.updatedAt" :project-namespace="projectNamespace"
:task-status="state.taskStatus" /> :projects-autocomplete-url="projectsAutocompleteUrl"
/>
<div v-else>
<title-component
:issuable-ref="issuableRef"
:title-html="state.titleHtml"
:title-text="state.titleText" />
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus" />
</div>
</div> </div>
</template> </template>
...@@ -18,11 +18,13 @@ ...@@ -18,11 +18,13 @@
}, },
updatedAt: { updatedAt: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
taskStatus: { taskStatus: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { data() {
...@@ -83,6 +85,7 @@ ...@@ -83,6 +85,7 @@
<template> <template>
<div <div
v-if="descriptionHtml"
class="description" class="description"
:class="{ :class="{
'js-task-list-container': canUpdate 'js-task-list-container': canUpdate
......
<script>
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
export default {
mixins: [updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
},
data() {
return {
deleteLoading: false,
};
},
computed: {
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
},
methods: {
closeForm() {
eventHub.$emit('close.form');
},
deleteIssuable() {
// eslint-disable-next-line no-alert
if (confirm('Issue will be removed! Are you sure?')) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
}
},
},
};
</script>
<template>
<div class="prepend-top-default append-bottom-default clearfix">
<button
class="btn btn-save pull-left"
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
type="submit"
:disabled="formState.updateLoading || !isSubmitEnabled"
@click.prevent="updateIssuable">
Save changes
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
v-if="formState.updateLoading">
</i>
</button>
<button
class="btn btn-default pull-right"
type="button"
@click="closeForm">
Cancel
</button>
<button
v-if="canDestroy"
class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
:disabled="deleteLoading"
@click="deleteIssuable">
Delete
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
v-if="deleteLoading">
</i>
</button>
</div>
</template>
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset class="checkbox">
<label for="issue-confidential">
<input
type="checkbox"
value="1"
id="issue-confidential"
v-model="formState.confidential" />
This issue is confidential and should only be visible to team members with at least Reporter access.
</label>
</fieldset>
</template>
<script>
/* global Flash */
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
markdownPreviewUrl: {
type: String,
required: true,
},
markdownDocs: {
type: String,
required: true,
},
},
components: {
markdownField,
},
mounted() {
this.$refs.textarea.focus();
},
};
</script>
<template>
<div class="common-note-form">
<label
class="sr-only"
for="issue-description">
Description
</label>
<markdown-field
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
data-supports-slash-commands="false"
aria-label="Description"
v-model="formState.description"
ref="textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@keydown.meta.enter="updateIssuable">
</textarea>
</markdown-field>
</div>
</template>
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
},
computed: {
issuableTemplatesJson() {
return JSON.stringify(this.issuableTemplates);
},
},
mounted() {
// Create the editor for the template
const editor = document.querySelector('.detail-page-description .note-textarea') || {};
editor.setValue = (val) => {
this.formState.description = val;
};
editor.getValue = () => this.formState.description;
this.issuableTemplate = new gl.IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
},
};
</script>
<template>
<div
class="dropdown js-issuable-selector-wrap"
data-issuable-type="issue">
<button
class="dropdown-menu-toggle js-issuable-selector"
type="button"
ref="toggle"
data-field-name="issuable_template"
data-selected="null"
data-toggle="dropdown"
:data-namespace-path="projectNamespace"
:data-project-path="projectPath"
:data-data="issuableTemplatesJson">
<span class="dropdown-toggle-text">
Choose a template
</span>
<i
aria-hidden="true"
class="fa fa-chevron-down">
</i>
</button>
<div class="dropdown-menu dropdown-select">
<div class="dropdown-title">
Choose a template
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i
aria-hidden="true"
class="fa fa-times dropdown-menu-close-icon">
</i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
placeholder="Filter"
autocomplete="off" />
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search">
</i>
<i
role="button"
aria-label="Clear templates search input"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
<li>
<a class="no-template">
No template
</a>
</li>
<li>
<a class="reset-template">
Reset template
</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
formState: {
type: Object,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
mounted() {
const $moveDropdown = $(this.$refs['move-dropdown']);
$moveDropdown.select2({
ajax: {
url: this.projectsAutocompleteUrl,
quietMillis: 125,
data(term, page, context) {
return {
search: term,
offset_id: context,
};
},
results(data) {
const more = data.length >= 50;
const context = data[data.length - 1] ? data[data.length - 1].id : null;
return {
results: data,
more,
context,
};
},
},
formatResult(project) {
return project.name_with_namespace;
},
formatSelection(project) {
return project.name_with_namespace;
},
})
.on('change', (e) => {
this.formState.move_to_project_id = parseInt(e.target.value, 10);
});
},
beforeDestroy() {
$(this.$refs['move-dropdown']).select2('destroy');
},
};
</script>
<template>
<fieldset>
<label
for="issuable-move"
class="sr-only">
Move
</label>
<div class="issuable-form-select-holder append-right-5">
<input
ref="move-dropdown"
type="hidden"
id="issuable-move"
data-placeholder="Move to a different project" />
</div>
<span
data-placement="auto top"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref="tooltip">
<i
class="fa fa-question-circle"
aria-hidden="true">
</i>
</span>
</fieldset>
</template>
<script>
import updateMixin from '../../mixins/update';
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset>
<label
class="sr-only"
for="issue-title">
Title
</label>
<input
id="issue-title"
class="form-control"
type="text"
placeholder="Issue title"
aria-label="Issue title"
v-model="formState.title"
@keydown.meta.enter="updateIssuable" />
</fieldset>
</template>
<script>
import lockedWarning from './locked_warning.vue';
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
import projectMove from './fields/project_move.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default {
props: {
canMove: {
type: Boolean,
required: true,
},
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
markdownPreviewUrl: {
type: String,
required: true,
},
markdownDocs: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
projectMove,
confidentialCheckbox,
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
},
},
};
</script>
<template>
<form>
<locked-warning v-if="formState.lockedWarningVisible" />
<div class="row">
<div
class="col-sm-4 col-lg-3"
v-if="hasIssuableTemplates">
<description-template
:form-state="formState"
:issuable-templates="issuableTemplates"
:project-path="projectPath"
:project-namespace="projectNamespace" />
</div>
<div
:class="{
'col-sm-8 col-lg-9': hasIssuableTemplates,
'col-xs-12': !hasIssuableTemplates,
}">
<title-field
:form-state="formState"
:issuable-templates="issuableTemplates" />
</div>
</div>
<description-field
:form-state="formState"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs" />
<confidential-checkbox
:form-state="formState" />
<project-move
v-if="canMove"
:form-state="formState"
:projects-autocomplete-url="projectsAutocompleteUrl" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy" />
</form>
</template>
<script>
export default {
computed: {
currentPath() {
return location.pathname;
},
},
};
</script>
<template>
<div class="alert alert-danger">
Someone edited the issue at the same time you did. Please check out
<a
:href="currentPath"
target="_blank"
rel="nofollow">the issue</a>
and make sure your changes will not unintentionally remove theirs.
</div>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue'; import Vue from 'vue';
import eventHub from './event_hub';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => {
el: document.getElementById('js-issuable-app'), const initialDataEl = document.getElementById('js-issuable-app-initial-data');
components: { const initialData = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
issuableApp,
},
data() {
const issuableElement = this.$options.el;
const issuableTitleElement = issuableElement.querySelector('.title');
const issuableDescriptionElement = issuableElement.querySelector('.wiki');
const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
const {
canUpdate,
endpoint,
issuableRef,
} = issuableElement.dataset;
return { $('.issuable-edit').on('click', (e) => {
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate), e.preventDefault();
endpoint,
issuableRef, eventHub.$emit('open.form');
initialTitle: issuableTitleElement.innerHTML, });
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '', return new Vue({
}; el: document.getElementById('js-issuable-app'),
}, components: {
render(createElement) { issuableApp,
return createElement('issuable-app', { },
props: { data() {
canUpdate: this.canUpdate, return {
endpoint: this.endpoint, ...initialData,
issuableRef: this.issuableRef, };
initialTitle: this.initialTitle, },
initialDescriptionHtml: this.initialDescriptionHtml, render(createElement) {
initialDescriptionText: this.initialDescriptionText, return createElement('issuable-app', {
}, props: {
}); canUpdate: this.canUpdate,
}, canDestroy: this.canDestroy,
})); canMove: this.canMove,
endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitleHtml: this.initialTitleHtml,
initialTitleText: this.initialTitleText,
initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
isConfidential: this.isConfidential,
markdownPreviewUrl: this.markdownPreviewUrl,
markdownDocs: this.markdownDocs,
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
},
});
},
});
});
...@@ -4,7 +4,7 @@ export default { ...@@ -4,7 +4,7 @@ export default {
this.preAnimation = true; this.preAnimation = true;
this.pulseAnimation = false; this.pulseAnimation = false;
this.$nextTick(() => { setTimeout(() => {
this.preAnimation = false; this.preAnimation = false;
this.pulseAnimation = true; this.pulseAnimation = true;
}); });
......
import eventHub from '../event_hub';
export default {
methods: {
updateIssuable() {
this.formState.updateLoading = true;
eventHub.$emit('update.issuable');
},
},
};
...@@ -7,10 +7,23 @@ export default class Service { ...@@ -7,10 +7,23 @@ export default class Service {
constructor(endpoint) { constructor(endpoint) {
this.endpoint = endpoint; this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint); this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
realtimeChanges: {
method: 'GET',
url: `${this.endpoint}/realtime_changes`,
},
});
} }
getData() { getData() {
return this.resource.get(); return this.resource.realtimeChanges();
}
deleteIssuable() {
return this.resource.delete();
}
updateIssuable(data) {
return this.resource.update(data);
} }
} }
export default class Store { export default class Store {
constructor({ constructor({
titleHtml, titleHtml,
titleText,
descriptionHtml, descriptionHtml,
descriptionText, descriptionText,
}) { }) {
this.state = { this.state = {
titleHtml, titleHtml,
titleText: '', titleText,
descriptionHtml, descriptionHtml,
descriptionText, descriptionText,
taskStatus: '', taskStatus: '',
updatedAt: '', updatedAt: '',
}; };
this.formState = {
title: '',
confidential: false,
description: '',
lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false,
};
} }
updateState(data) { updateState(data) {
...@@ -22,4 +31,15 @@ export default class Store { ...@@ -22,4 +31,15 @@ export default class Store {
this.state.taskStatus = data.task_status; this.state.taskStatus = data.task_status;
this.state.updatedAt = data.updated_at; this.state.updatedAt = data.updated_at;
} }
stateShouldUpdate(data) {
return {
title: this.state.titleText !== data.title_text,
description: this.state.descriptionText !== data.description_text,
};
}
setFormState(state) {
this.formState = Object.assign(this.formState, state);
}
} }
...@@ -170,7 +170,7 @@ gl.text.init = function(form) { ...@@ -170,7 +170,7 @@ gl.text.init = function(form) {
}); });
}; };
gl.text.removeListeners = function(form) { gl.text.removeListeners = function(form) {
return $('.js-md', form).off(); return $('.js-md', form).off('click');
}; };
gl.text.humanize = function(string) { gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
......
...@@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Similar to `toggler_behavior` in the discussion tab // Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash(); const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`); const anchor = hash && $container.find(`[id="${hash}"]`);
if (anchor) { if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content'); const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old'; const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.toggleDiffNote({ notes.toggleDiffNote({
......
...@@ -1219,7 +1219,7 @@ const normalizeNewlines = function(str) { ...@@ -1219,7 +1219,7 @@ const normalizeNewlines = function(str) {
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
<div class="timeline-icon"> <div class="timeline-icon">
<a href="/${currentUsername}"><span class="dummy-avatar"></span></a> <a href="/${currentUsername}"><span class="avatar dummy-avatar"></span></a>
</div> </div>
<div class="timeline-content ${discussionClass}"> <div class="timeline-content ${discussionClass}">
<div class="note-header"> <div class="note-header">
......
...@@ -77,7 +77,9 @@ import './shortcuts_navigation'; ...@@ -77,7 +77,9 @@ import './shortcuts_navigation';
ShortcutsIssuable.prototype.editIssue = function() { ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn; var $editBtn;
$editBtn = $('.issuable-edit'); $editBtn = $('.issuable-edit');
return gl.utils.visitUrl($editBtn.attr('href')); // Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
$editBtn.get(0).click();
}; };
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
window.SingleFileDiff = (function() { window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'; WRAPPER = '<div class="diff-content"></div>';
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
}, },
data() { data() {
return { return {
removeSourceBranch: true, removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false, mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false, useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false, setToMergeWhenPipelineSucceeds: false,
...@@ -70,6 +70,9 @@ export default { ...@@ -70,6 +70,9 @@ export default {
|| this.isApprovalNeeded || this.isApprovalNeeded
|| this.mr.preventMerge); || this.mr.preventMerge);
}, },
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
},
shouldShowSquashBeforeMerge() { shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr; const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1; return enableSquashBeforeMerge && commitsCount > 1;
...@@ -256,8 +259,9 @@ export default { ...@@ -256,8 +259,9 @@ export default {
<template v-if="isMergeAllowed()"> <template v-if="isMergeAllowed()">
<label class="spacing"> <label class="spacing">
<input <input
id="remove-source-branch-input"
v-model="removeSourceBranch" v-model="removeSourceBranch"
:disabled="isMergeButtonDisabled" :disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch type="checkbox"/> Remove source branch
</label> </label>
......
/* global Flash */ /* global Flash */
import pendingAvatarSvg from 'icons/_icon_dotted_circle.svg';
import LinkToMemberAvatar from '~/vue_shared/components/link_to_member_avatar'; import LinkToMemberAvatar from '~/vue_shared/components/link_to_member_avatar';
import eventHub from '../../../event_hub'; import eventHub from '../../../event_hub';
...@@ -38,7 +37,6 @@ export default { ...@@ -38,7 +37,6 @@ export default {
data() { data() {
return { return {
unapproving: false, unapproving: false,
pendingAvatarSvg,
}; };
}, },
components: { components: {
...@@ -68,23 +66,22 @@ export default { ...@@ -68,23 +66,22 @@ export default {
template: ` template: `
<div v-if="approvedBy.length" class="approved-by-users approvals-footer clearfix mr-info-list"> <div v-if="approvedBy.length" class="approved-by-users approvals-footer clearfix mr-info-list">
<div class="legend"></div> <div class="legend"></div>
<div> <div class="approvers-prefix">
<p class="approvers-prefix">Approved by</p> <p>Approved by</p>
<div class="approvers-list"> <div class="approvers-list">
<span v-for="approver in approvedBy"> <span v-for="approver in approvedBy">
<link-to-member-avatar <link-to-member-avatar
:avatarSize="20"
extra-link-class="approver-avatar" extra-link-class="approver-avatar"
:avatar-url="approver.user.avatar_url"
:display-name="approver.user.name" :display-name="approver.user.name"
:profile-url="approver.user.web_url" :profile-url="approver.user.web_url"
:show-tooltip="true" /> :show-tooltip="true" />
</span> </span>
<span class="potential-approvers-list" v-for="n in approvalsLeft"> <span class="potential-approvers-list" v-for="n in approvalsLeft">
<link-to-member-avatar <link-to-member-avatar
:avatarSize="20"
:clickable="false" :clickable="false"
:avatar-html="pendingAvatarSvg" :show-tooltip="false" />
:show-tooltip="false"
extra-link-class="hide-asset" />
</span> </span>
</div> </div>
<span class="unapprove-btn-wrap" v-if="showUnapproveButton"> <span class="unapprove-btn-wrap" v-if="showUnapproveButton">
......
<script>
import successIcon from 'icons/_icon_status_success.svg';
import errorIcon from 'icons/_icon_status_failed.svg';
import issuesBlock from './mr_widget_code_quality_issues.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../lib/utils/text_utility';
export default {
name: 'MRWidgetCodeQuality',
props: {
mr: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
},
components: {
issuesBlock,
loadingIcon,
},
data() {
return {
collapseText: 'Expand',
isCollapsed: true,
isLoading: false,
loadingFailed: false,
};
},
computed: {
stateIcon() {
return this.mr.codeclimateMetrics.newIssues.length ? errorIcon : successIcon;
},
hasNoneIssues() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
return !newIssues.length && !resolvedIssues.length;
},
hasIssues() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
return newIssues.length || resolvedIssues.length;
},
codeText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
let newIssuesText = '';
let resolvedIssuesText = '';
let text = '';
if (this.hasNoneIssues) {
text = 'No changes to code quality so far.';
} else if (this.hasIssues) {
if (newIssues.length) {
newIssuesText = `degraded on ${newIssues.length} ${this.pointsText(newIssues)}`;
}
if (resolvedIssues.length) {
resolvedIssuesText = `improved on ${resolvedIssues.length} ${this.pointsText(resolvedIssues)}`;
}
const connector = this.hasIssues ? 'and' : '';
text = `Code quality ${resolvedIssuesText} ${connector} ${newIssuesText}.`;
}
return text;
},
},
methods: {
pointsText(issues) {
return gl.text.pluralize('point', issues.length);
},
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
const text = this.isCollapsed ? 'Expand' : 'Collapse';
this.collapseText = text;
},
handleError() {
this.isLoading = false;
this.loadingFailed = true;
},
},
created() {
const { head_path, base_path } = this.mr.codeclimate;
this.isLoading = true;
this.service.fetchCodeclimate(head_path)
.then(resp => resp.json())
.then((data) => {
this.mr.setCodeclimateHeadMetrics(data);
this.service.fetchCodeclimate(base_path)
.then(response => response.json())
.then(baseData => this.mr.setCodeclimateBaseMetrics(baseData))
.then(() => this.mr.compareCodeclimateMetrics())
.then(() => {
this.isLoading = false;
})
.catch(() => this.handleError());
})
.catch(() => this.handleError());
},
};
</script>
<template>
<section class="mr-widget-code-quality">
<div
v-if="isLoading"
class="padding-left">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
Loading codeclimate report.
</div>
<div v-else-if="!isLoading && !loadingFailed">
<span
class="padding-left ci-status-icon"
:class="{
'ci-status-icon-failed': mr.codeclimateMetrics.newIssues.length,
'ci-status-icon-passed': mr.codeclimateMetrics.newIssues.length === 0
}"
v-html="stateIcon">
</span>
<span>
{{codeText}}
</span>
<button
type="button"
class="btn-link btn-blank"
v-if="hasIssues"
@click="toggleCollapsed">
{{collapseText}}
</button>
<div
class="code-quality-container"
v-if="hasIssues"
v-show="!isCollapsed">
<issues-block
class="js-mr-code-resolved-issues"
v-if="mr.codeclimateMetrics.resolvedIssues.length"
type="success"
:issues="mr.codeclimateMetrics.resolvedIssues"
/>
<issues-block
class="js-mr-code-new-issues"
v-if="mr.codeclimateMetrics.newIssues.length"
type="failed"
:issues="mr.codeclimateMetrics.newIssues"
/>
</div>
</div>
<div
v-else-if="loadingFailed"
class="padding-left">
Failed to load codeclimate report.
</div>
</section>
</template>
<script>
export default {
name: 'MRWidgetCodeQualityIssues',
props: {
issues: {
type: Array,
required: true,
},
type: {
type: String,
required: true,
},
},
};
</script>
<template>
<ul class="mr-widget-code-quality-list">
<li
class="commit-sha"
:class="{
failed: type === 'failed',
success: type === 'success'
}
"v-for="issue in issues">
<i
class="fa"
:class="{
'fa-minus': type === 'failed',
'fa-plus': type === 'success'
}"
aria-hidden="true">
</i>
<span>
<span v-if="type === 'success'">Fixed:</span>
{{issue.check_name}}
{{issue.location.path}}
{{issue.location.positions}}
{{issue.location.lines}}
</span>
</li>
</ul>
</template>
...@@ -2,6 +2,7 @@ import CEWidgetOptions from '../mr_widget_options'; ...@@ -2,6 +2,7 @@ import CEWidgetOptions from '../mr_widget_options';
import WidgetApprovals from './components/approvals/mr_widget_approvals'; import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node'; import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
import RebaseState from './components/states/mr_widget_rebase'; import RebaseState from './components/states/mr_widget_rebase';
import WidgetCodeQuality from './components/mr_widget_code_quality.vue';
export default { export default {
extends: CEWidgetOptions, extends: CEWidgetOptions,
...@@ -9,11 +10,16 @@ export default { ...@@ -9,11 +10,16 @@ export default {
'mr-widget-approvals': WidgetApprovals, 'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode, 'mr-widget-geo-secondary-node': GeoSecondaryNode,
'mr-widget-rebase': RebaseState, 'mr-widget-rebase': RebaseState,
'mr-widget-code-quality': WidgetCodeQuality,
}, },
computed: { computed: {
shouldRenderApprovals() { shouldRenderApprovals() {
return this.mr.approvalsRequired; return this.mr.approvalsRequired;
}, },
shouldRenderCodeQuality() {
const { codeclimate } = this.mr;
return codeclimate && codeclimate.head_path && codeclimate.base_path;
},
}, },
template: ` template: `
<div class="mr-state-widget prepend-top-default"> <div class="mr-state-widget prepend-top-default">
...@@ -29,6 +35,11 @@ export default { ...@@ -29,6 +35,11 @@ export default {
v-if="mr.approvalsRequired" v-if="mr.approvalsRequired"
:mr="mr" :mr="mr"
:service="service" /> :service="service" />
<mr-widget-code-quality
v-if="shouldRenderCodeQuality"
:mr="mr"
:service="service"
/>
<component <component
:is="componentName" :is="componentName"
:mr="mr" :mr="mr"
......
...@@ -28,4 +28,8 @@ export default class MRWidgetService extends CEWidgetService { ...@@ -28,4 +28,8 @@ export default class MRWidgetService extends CEWidgetService {
rebase() { rebase() {
return this.rebaseResource.save(); return this.rebaseResource.save();
} }
fetchCodeclimate(endpoint) { // eslint-disable-line
return Vue.http.get(endpoint);
}
} }
import CEMergeRequestStore from '../../stores/mr_widget_store'; import CEMergeRequestStore from '../../stores/mr_widget_store';
export default class MergeRequestStore extends CEMergeRequestStore { export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
super(data);
this.initCodeclimate(data);
}
setData(data) { setData(data) {
this.initGeo(data); this.initGeo(data);
this.initSquashBeforeMerge(data); this.initSquashBeforeMerge(data);
...@@ -43,4 +48,33 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -43,4 +48,33 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.isApproved = !this.approvalsLeft || false; this.isApproved = !this.approvalsLeft || false;
this.preventMerge = this.approvalsRequired && this.approvalsLeft; this.preventMerge = this.approvalsRequired && this.approvalsLeft;
} }
initCodeclimate(data) {
this.codeclimate = data.codeclimate;
this.codeclimateMetrics = {
headIssues: [],
baseIssues: [],
newIssues: [],
resolvedIssues: [],
};
}
setCodeclimateHeadMetrics(data) {
this.codeclimateMetrics.headIssues = data;
}
setCodeclimateBaseMetrics(data) {
this.codeclimateMetrics.baseIssues = data;
}
compareCodeclimateMetrics() {
const { headIssues, baseIssues } = this.codeclimateMetrics;
this.codeclimateMetrics.newIssues = this.filterByFingerprint(headIssues, baseIssues);
this.codeclimateMetrics.resolvedIssues = this.filterByFingerprint(baseIssues, headIssues);
}
filterByFingerprint(firstArray, secondArray) { // eslint-disable-line
return firstArray.filter(item => !secondArray.find(el => el.fingerprint === item.fingerprint));
}
} }
...@@ -81,13 +81,12 @@ export default { ...@@ -81,13 +81,12 @@ export default {
.then((res) => { .then((res) => {
this.mr.setData(res); this.mr.setData(res);
this.setFavicon(); this.setFavicon();
if (cb) { if (cb) {
cb.call(null, res); cb.call(null, res);
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
initPolling() { initPolling() {
this.pollingInterval = new gl.SmartInterval({ this.pollingInterval = new gl.SmartInterval({
...@@ -134,9 +133,7 @@ export default { ...@@ -134,9 +133,7 @@ export default {
document.body.appendChild(el); document.body.appendChild(el);
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
resumePolling() { resumePolling() {
this.pollingInterval.resume(); this.pollingInterval.resume();
......
...@@ -2,9 +2,9 @@ import Timeago from 'timeago.js'; ...@@ -2,9 +2,9 @@ import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies'; import { getStateKey } from '../dependencies';
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
this.setData(data); this.setData(data);
} }
...@@ -53,7 +53,7 @@ export default class MergeRequestStore { ...@@ -53,7 +53,7 @@ export default class MergeRequestStore {
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path; this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path; this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists; this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false; this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path; this.mergePath = data.merge_path;
...@@ -135,5 +135,4 @@ export default class MergeRequestStore { ...@@ -135,5 +135,4 @@ export default class MergeRequestStore {
return timeagoInstance.format(event.updated_at); return timeagoInstance.format(event.updated_at);
} }
} }
<script>
import ciIconBadge from './ci_badge_link.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
props: {
status: {
type: Object,
required: true,
},
itemName: {
type: String,
required: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: true,
},
actions: {
type: Array,
required: false,
default: () => [],
},
},
mixins: [
tooltipMixin,
],
components: {
ciIconBadge,
timeagoTooltip,
userAvatarLink,
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
},
},
methods: {
onClickAction(action) {
this.$emit('postAction', action);
},
},
};
</script>
<template>
<header class="page-content-header top-area">
<section class="header-main-content">
<ci-icon-badge :status="status" />
<strong>
{{itemName}} #{{itemId}}
</strong>
triggered
<timeago-tooltip :time="time" />
by
<user-avatar-link
:link-href="user.web_url"
:img-src="user.avatar_url"
:img-alt="userAvatarAltText"
:tooltip-text="user.name"
:img-size="24"
/>
<a
:href="user.web_url"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
{{user.name}}
</a>
</section>
<section
class="header-action-button nav-controls"
v-if="actions.length">
<template
v-for="action in actions">
<a
v-if="action.type === 'link'"
:href="action.path"
:class="action.cssClass">
{{action.label}}
</a>
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
:class="action.cssClass"
type="button">
{{action.label}}
</button>
</template>
</section>
</header>
</template>
// Analogue of link_to_member_avatar in app/helpers/projects_helper.rb // Analogue of link_to_member_avatar in app/helpers/projects_helper.rb
import pendingAvatarSvg from 'icons/_icon_dotted_circle.svg';
export default { export default {
props: { props: {
...@@ -52,6 +53,7 @@ export default { ...@@ -52,6 +53,7 @@ export default {
data() { data() {
return { return {
avatarBaseClass: 'avatar avatar-inline', avatarBaseClass: 'avatar avatar-inline',
pendingAvatarSvg,
}; };
}, },
computed: { computed: {
...@@ -59,7 +61,7 @@ export default { ...@@ -59,7 +61,7 @@ export default {
return `s${this.avatarSize}`; return `s${this.avatarSize}`;
}, },
avatarHtmlClass() { avatarHtmlClass() {
return `${this.avatarSizeClass} ${this.avatarBaseClass}`; return `${this.avatarSizeClass} ${this.avatarBaseClass} avatar-placeholder`;
}, },
tooltipClass() { tooltipClass() {
return this.showTooltip ? 'has-tooltip' : ''; return this.showTooltip ? 'has-tooltip' : '';
...@@ -84,21 +86,23 @@ export default { ...@@ -84,21 +86,23 @@ export default {
:class="linkClass" :class="linkClass"
:title="displayName" :title="displayName"
:data-container="tooltipContainerAttr"> :data-container="tooltipContainerAttr">
<img
v-if="avatarUrl"
:class="avatarClass"
:src="avatarUrl"
:width="avatarSize"
:height="avatarSize"
:alt="displayName"/>
<svg <svg
v-if="avatarHtml" v-else
v-html="avatarHtml" v-html="pendingAvatarSvg"
:class="avatarHtmlClass" :class="avatarHtmlClass"
:width="avatarSize" :width="avatarSize"
:height="avatarSize" :height="avatarSize"
:alt="displayName"> :alt="displayName">
</svg> </svg>
<img
:class="avatarClass"
:src="avatarUrl"
:width="avatarSize"
:height="avatarSize"
:alt="displayName"/>
</a> </a>
</div> </div>
`, `,
......
<script>
/* global Flash */
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
export default {
props: {
markdownPreviewUrl: {
type: String,
required: false,
default: '',
},
markdownDocs: {
type: String,
required: true,
},
},
data() {
return {
markdownPreview: '',
markdownPreviewLoading: false,
previewMarkdown: false,
};
},
components: {
markdownHeader,
markdownToolbar,
},
methods: {
toggleMarkdownPreview() {
this.previewMarkdown = !this.previewMarkdown;
if (!this.previewMarkdown) {
this.markdownPreview = '';
} else {
this.markdownPreviewLoading = true;
this.$http.post(
this.markdownPreviewUrl,
{
/*
Can't use `$refs` as the component is technically in the parent component
so we access the VNode & then get the element
*/
text: this.$slots.textarea[0].elm.value,
},
)
.then((res) => {
const data = res.json();
this.markdownPreviewLoading = false;
this.markdownPreview = data.body;
this.$nextTick(() => {
$(this.$refs['markdown-preview']).renderGFM();
});
})
.catch(() => new Flash('Error loading markdown preview'));
}
},
},
mounted() {
/*
GLForm class handles all the toolbar buttons
*/
return new gl.GLForm($(this.$refs['gl-form']), true);
},
};
</script>
<template>
<div
class="md-area prepend-top-default append-bottom-default js-vue-markdown-field"
ref="gl-form">
<markdown-header
:preview-markdown="previewMarkdown"
@toggle-markdown="toggleMarkdownPreview" />
<div
class="md-write-holder"
v-show="!previewMarkdown">
<div class="zen-backdrop">
<slot name="textarea"></slot>
<a
class="zen-control zen-control-leave js-zen-leave"
href="#"
aria-label="Enter zen mode">
<i
class="fa fa-compress"
aria-hidden="true">
</i>
</a>
<markdown-toolbar
:markdown-docs="markdownDocs" />
</div>
</div>
<div
class="md md-preview-holder md-preview"
v-show="previewMarkdown">
<div
ref="markdown-preview"
v-html="markdownPreview">
</div>
<span v-if="markdownPreviewLoading">
Loading...
</span>
</div>
</div>
</template>
<script>
import tooltipMixin from '../../mixins/tooltip';
import toolbarButton from './toolbar_button.vue';
export default {
mixins: [
tooltipMixin,
],
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
},
components: {
toolbarButton,
},
methods: {
toggleMarkdownPreview(e, form) {
if (form && !form.find('.js-vue-markdown-field').length) {
return;
} else if (e.target.blur) {
e.target.blur();
}
this.$emit('toggle-markdown');
},
},
mounted() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
$(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
},
beforeDestroy() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
$(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
},
};
</script>
<template>
<div class="md-header">
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }">
<a
href="#md-write-holder"
tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)">
Write
</a>
</li>
<li :class="{ active: previewMarkdown }">
<a
href="#md-preview-holder"
tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)">
Preview
</a>
</li>
<li class="pull-right">
<div class="toolbar-group">
<toolbar-button
tag="**"
button-title="Add bold text"
icon="bold" />
<toolbar-button
tag="*"
button-title="Add italic text"
icon="italic" />
<toolbar-button
tag="> "
:prepend="true"
button-title="Insert a quote"
icon="quote-right" />
<toolbar-button
tag="`"
tag-block="```"
button-title="Insert code"
icon="code" />
<toolbar-button
tag="* "
:prepend="true"
button-title="Add a bullet list"
icon="list-ul" />
<toolbar-button
tag="1. "
:prepend="true"
button-title="Add a numbered list"
icon="list-ol" />
<toolbar-button
tag="* [ ] "
:prepend="true"
button-title="Add a task list"
icon="check-square-o" />
</div>
<div class="toolbar-group">
<button
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button"
ref="tooltip">
<i
aria-hidden="true"
class="fa fa-arrows-alt fa-fw">
</i>
</button>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
markdownDocs: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
<a
:href="markdownDocs"
target="_blank"
tabindex="-1">
Markdown is supported
</a>
</div>
<button
class="toolbar-button markdown-selector"
type="button"
tabindex="-1">
<i
class="fa fa-file-image-o toolbar-button-icon"
aria-hidden="true">
</i>
Attach a file
</button>
</div>
</template>
<script>
import tooltipMixin from '../../mixins/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
buttonTitle: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
tag: {
type: String,
required: true,
},
tagBlock: {
type: String,
required: false,
default: '',
},
prepend: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
iconClass() {
return `fa-${this.icon}`;
},
},
};
</script>
<template>
<button
type="button"
class="toolbar-btn js-md hidden-xs"
tabindex="-1"
ref="tooltip"
data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"
:data-md-prepend="prepend"
:title="buttonTitle"
:aria-label="buttonTitle">
<i
aria-hidden="true"
class="fa fa-fw"
:class="iconClass">
</i>
</button>
</template>
<script>
import tooltipMixin from '../mixins/tooltip';
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
/**
* Port of ruby helper time_ago_with_tooltip
*/
export default {
props: {
time: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
shortFormat: {
type: Boolean,
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: '',
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
computed: {
timeagoCssClass() {
return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
},
},
};
</script>
<template>
<time
:class="[timeagoCssClass, cssClass]"
class="js-timeago js-timeago-render"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
ref="tooltip">
{{timeFormated(time)}}
</time>
</template>
import '../../lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
*/
export default {
methods: {
timeFormated(time) {
const timeago = gl.utils.getTimeago();
return timeago.format(time);
},
tooltipTitle(time) {
return gl.utils.formatDate(time);
},
},
};
...@@ -6,4 +6,8 @@ export default { ...@@ -6,4 +6,8 @@ export default {
updated() { updated() {
$(this.$refs.tooltip).tooltip('fixTitle'); $(this.$refs.tooltip).tooltip('fixTitle');
}, },
beforeDestroy() {
$(this.$refs.tooltip).tooltip('destroy');
},
}; };
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
@import "framework/selects.scss"; @import "framework/selects.scss";
@import "framework/sidebar.scss"; @import "framework/sidebar.scss";
@import "framework/tables.scss"; @import "framework/tables.scss";
@import "framework/notes.scss";
@import "framework/timeline.scss"; @import "framework/timeline.scss";
@import "framework/typography.scss"; @import "framework/typography.scss";
@import "framework/zen.scss"; @import "framework/zen.scss";
......
...@@ -59,6 +59,10 @@ ...@@ -59,6 +59,10 @@
border: none; border: none;
} }
&.avatar-placeholder {
border: none;
}
&:not([href]):hover { &:not([href]):hover {
border-color: rgba($avatar-border, .2); border-color: rgba($avatar-border, .2);
} }
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
top: 0; top: 0;
margin-top: 3px; margin-top: 3px;
padding: $gl-padding; padding: $gl-padding;
z-index: 9; z-index: 300;
width: 300px; width: 300px;
font-size: 14px; font-size: 14px;
background-color: $white-light; background-color: $white-light;
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
.row-content-block { .row-content-block {
margin-top: 0; margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $gray-light; background-color: $gray-light;
padding: $gl-padding; padding: $gl-padding;
margin-bottom: 0; margin-bottom: 0;
......
gl-emoji { gl-emoji {
display: inline-block;
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
...@@ -66,10 +66,10 @@ ...@@ -66,10 +66,10 @@
&.video { &.video {
background: $file-image-bg; background: $file-image-bg;
text-align: center; text-align: center;
padding: 30px;
img, img,
video { video {
padding: 20px;
max-width: 80%; max-width: 80%;
} }
} }
......
...@@ -36,6 +36,10 @@ ...@@ -36,6 +36,10 @@
border-radius: 0; border-radius: 0;
} }
} }
&:empty {
margin: 0;
}
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
> li { > li {
padding: 10px 15px; padding: 10px 15px;
min-height: 20px; min-height: 20px;
border-bottom: 1px solid $list-border-light;
border-bottom: 1px solid $list-border; border-bottom: 1px solid $list-border;
&::after { &::after {
......
@mixin notes-media($condition, $breakpoint-width) {
@media (#{$condition}-width: ($breakpoint-width)) {
@content;
}
// Diff is side by side
.notes_content.parallel & {
// We hide at double what we normally hide at because
// there are two columns of notes
@media (#{$condition}-width: (2 * $breakpoint-width)) {
@content;
}
}
}
...@@ -97,7 +97,6 @@ ...@@ -97,7 +97,6 @@
.select2-search-field input { .select2-search-field input {
padding: 5px $gl-padding / 2; padding: 5px $gl-padding / 2;
font-size: 13px;
height: auto; height: auto;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
.note-text { &::before {
p:last-child { @include notes-media('max', $screen-xs-max) {
margin-bottom: 0 !important; background: none;
} }
} }
...@@ -29,6 +29,16 @@ ...@@ -29,6 +29,16 @@
.timeline-entry-inner { .timeline-entry-inner {
position: relative; position: relative;
@include notes-media('max', $screen-xs-max) {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
} }
&:target, &:target,
...@@ -46,24 +56,6 @@ ...@@ -46,24 +56,6 @@
} }
} }
@media (max-width: $screen-xs-max) {
.timeline {
&::before {
background: none;
}
}
.timeline-entry .timeline-entry-inner {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
}
.discussion .timeline-entry { .discussion .timeline-entry {
margin: 0; margin: 0;
border-right: none; border-right: none;
......
...@@ -21,6 +21,10 @@ ...@@ -21,6 +21,10 @@
margin-top: 0; margin-top: 0;
} }
> :last-child {
margin-bottom: 0;
}
// Single code lines should wrap // Single code lines should wrap
code { code {
font-family: $monospace_font; font-family: $monospace_font;
...@@ -157,7 +161,7 @@ ...@@ -157,7 +161,7 @@
ul, ul,
ol { ol {
padding: 0; padding: 0;
margin: 0 0 16px !important; margin: 0 0 16px;
} }
ul:dir(rtl), ul:dir(rtl),
......
...@@ -250,7 +250,6 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3); ...@@ -250,7 +250,6 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1); $dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777; $file-mode-changed: #777;
$file-mode-changed: #777; $file-mode-changed: #777;
$diff-image-bg: #ddd;
$diff-image-info-color: grey; $diff-image-info-color: grey;
$diff-swipe-border: #999; $diff-swipe-border: #999;
$diff-view-modes-color: grey; $diff-view-modes-color: grey;
......
...@@ -101,7 +101,9 @@ ...@@ -101,7 +101,9 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
height: calc(100vh - 222px); height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty
min-height: 475px; min-height: 475px;
transition: width .2s; transition: width .2s;
......
...@@ -36,7 +36,6 @@ ...@@ -36,7 +36,6 @@
pre.commit-message { pre.commit-message {
background: none; background: none;
padding: 0; padding: 0;
margin: 0;
border: none; border: none;
margin: 20px 0; margin: 20px 0;
border-radius: 0; border-radius: 0;
......
...@@ -94,7 +94,6 @@ ...@@ -94,7 +94,6 @@
.old_line, .old_line,
.new_line { .new_line {
margin: 0; margin: 0;
padding: 0;
border: none; border: none;
padding: 0 5px; padding: 0 5px;
border-right: 1px solid; border-right: 1px solid;
...@@ -151,14 +150,10 @@ ...@@ -151,14 +150,10 @@
} }
} }
} }
.text-file.diff-wrap-lines table .line_holder td span {
white-space: pre-wrap;
}
} }
.image { .image {
background: $diff-image-bg; background: $file-image-bg;
text-align: center; text-align: center;
padding: 30px; padding: 30px;
......
...@@ -430,7 +430,7 @@ ...@@ -430,7 +430,7 @@
} }
.detail-page-description { .detail-page-description {
padding: 16px 0 0; padding: 16px 0;
small { small {
color: $gray-darkest; color: $gray-darkest;
...@@ -440,7 +440,7 @@ ...@@ -440,7 +440,7 @@
.edited-text { .edited-text {
color: $gray-darkest; color: $gray-darkest;
display: block; display: block;
margin: 0 0 16px; margin: 16px 0 0;
.author_link { .author_link {
color: $gray-darkest; color: $gray-darkest;
......
...@@ -219,7 +219,6 @@ ul.related-merge-requests > li { ...@@ -219,7 +219,6 @@ ul.related-merge-requests > li {
.dropdown-toggle { .dropdown-toggle {
.fa-caret-down { .fa-caret-down {
pointer-events: none; pointer-events: none;
margin-left: 0;
color: inherit; color: inherit;
margin-left: 0; margin-left: 0;
} }
......
...@@ -817,32 +817,11 @@ ...@@ -817,32 +817,11 @@
top: 10px; top: 10px;
} }
// vertically centers all children
> span {
align-self: center;
}
.hide-asset {
img {
display: none;
}
svg {
margin-bottom: -7px; // makes up for border removed
border: none;
}
}
.approvers-prefix, .approvers-prefix,
.approvers-list { .approvers-list {
display: flex;
align-items: center;
margin-right: 5px; margin-right: 5px;
float: left;
}
.approvers-list img {
width: 18px;
height: 18px;
margin-top: 3px;
} }
.unapprove-btn { .unapprove-btn {
...@@ -860,18 +839,8 @@ ...@@ -860,18 +839,8 @@
} }
} }
// styles for approver avatar checkmark
.approver-avatar { .approver-avatar {
position: relative; position: relative;
display: inline-block;
svg.avatar {
position: absolute;
top: 12%;
right: 4%;
height: 45%;
width: 45%;
}
} }
} }
...@@ -882,8 +851,9 @@ ...@@ -882,8 +851,9 @@
} }
.avatar { .avatar {
margin-bottom: -2px; margin-bottom: 0;
margin-right: 3px; margin-left: 7px;
display: block;
} }
} }
...@@ -905,3 +875,42 @@ ...@@ -905,3 +875,42 @@
} }
} }
} }
.mr-widget-code-quality {
padding-top: $gl-padding-top;
.padding-left {
padding-left: $gl-padding;
}
.ci-status-icon {
vertical-align: sub;
svg {
width: 22px;
height: 22px;
margin-right: 4px;
}
}
.code-quality-container {
border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
.mr-widget-code-quality-list {
list-style: none;
padding: 0 36px;
margin: 0;
li.success {
color: $green-500;
}
li.failed {
color: $red-500;
}
}
}
}
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.note-edit-form { .note-edit-form {
.note-form-actions { .note-form-actions {
position: relative; position: relative;
margin: $gl-padding 0; margin: $gl-padding 0 0;
} }
.note-preview-holder { .note-preview-holder {
...@@ -124,10 +124,18 @@ ...@@ -124,10 +124,18 @@
} }
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light; background-color: $white-light;
} }
.discussion-notes .disabled-comment {
padding: 6px 0;
}
.notes-form > li {
border: 0;
}
.note-edit-form { .note-edit-form {
display: none; display: none;
font-size: 14px; font-size: 14px;
......
...@@ -14,24 +14,11 @@ ul.notes { ...@@ -14,24 +14,11 @@ ul.notes {
margin: 0; margin: 0;
padding: 0; padding: 0;
.timeline-icon {
float: left;
svg {
width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 16px;
}
}
.timeline-content { .timeline-content {
margin-left: 55px; margin-left: 55px;
&.timeline-content-form { &.timeline-content-form {
@media (max-width: $screen-sm-max) { @include notes-media('max', $screen-sm-max) {
margin-left: 0; margin-left: 0;
} }
} }
...@@ -56,21 +43,22 @@ ul.notes { ...@@ -56,21 +43,22 @@ ul.notes {
position: relative; position: relative;
} }
.note { > li {
padding: $gl-padding $gl-btn-padding 0; padding: $gl-padding $gl-btn-padding;
display: block; display: block;
position: relative; position: relative;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
&:last-child {
// Override `.timeline > li:last-child { border-bottom: none; }`
border-bottom: 1px solid $white-normal;
}
&.being-posted { &.being-posted {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
.dummy-avatar { .dummy-avatar {
display: inline-block;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: $kdb-border; background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%); border: 1px solid darken($kdb-border, 25%);
} }
...@@ -126,13 +114,13 @@ ul.notes { ...@@ -126,13 +114,13 @@ ul.notes {
.note-awards { .note-awards {
.js-awards-block { .js-awards-block {
margin-bottom: 16px; margin-top: 16px;
} }
} }
.note-header { .note-header {
@media (max-width: $screen-xs-min) { @include notes-media('max', $screen-xs-min) {
.inline { .inline {
display: block; display: block;
} }
...@@ -161,10 +149,10 @@ ul.notes { ...@@ -161,10 +149,10 @@ ul.notes {
.system-note { .system-note {
font-size: 14px; font-size: 14px;
padding: 0; padding-left: 0;
clear: both; clear: both;
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-left: 65px; margin-left: 65px;
} }
...@@ -198,11 +186,22 @@ ul.notes { ...@@ -198,11 +186,22 @@ ul.notes {
} }
} }
.timeline-content { .timeline-icon {
padding: 14px 10px; float: left;
@media (min-width: $screen-sm-min) { svg {
margin-left: 20px; width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 2px;
}
}
.timeline-content {
@include notes-media('min', $screen-sm-min) {
margin-left: 30px;
} }
} }
...@@ -371,7 +370,7 @@ ul.notes { ...@@ -371,7 +370,7 @@ ul.notes {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@media (max-width: $screen-xs-max) { @include notes-media('max', $screen-xs-max) {
flex-flow: row wrap; flex-flow: row wrap;
} }
} }
...@@ -385,10 +384,16 @@ ul.notes { ...@@ -385,10 +384,16 @@ ul.notes {
padding-bottom: 0; padding-bottom: 0;
} }
.note-header-author-name {
@include notes-media('max', $screen-xs-max) {
display: none;
}
}
.note-headline-light { .note-headline-light {
display: inline; display: inline;
@media (max-width: $screen-xs-min) { @include notes-media('max', $screen-xs-min) {
display: block; display: block;
} }
} }
...@@ -430,7 +435,7 @@ ul.notes { ...@@ -430,7 +435,7 @@ ul.notes {
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
@media (max-width: $screen-xs-max) { @include notes-media('max', $screen-xs-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
...@@ -441,7 +446,7 @@ ul.notes { ...@@ -441,7 +446,7 @@ ul.notes {
} }
.discussion-actions { .discussion-actions {
@media (max-width: $screen-md-max) { @include notes-media('max', $screen-md-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
...@@ -455,7 +460,7 @@ ul.notes { ...@@ -455,7 +460,7 @@ ul.notes {
display: inline; display: inline;
line-height: 20px; line-height: 20px;
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-left: 10px; margin-left: 10px;
line-height: 24px; line-height: 24px;
} }
...@@ -590,10 +595,15 @@ ul.notes { ...@@ -590,10 +595,15 @@ ul.notes {
.discussion-body, .discussion-body,
.diff-file { .diff-file {
.notes .note { .notes .note {
padding: 10px 15px; padding-left: $gl-padding;
padding-right: $gl-padding;
&.system-note { &.system-note {
padding: 0; padding-left: 0;
@media (min-width: $screen-sm-min) {
margin-left: 70px;
}
} }
} }
} }
...@@ -607,17 +617,11 @@ ul.notes { ...@@ -607,17 +617,11 @@ ul.notes {
} }
.disabled-comment { .disabled-comment {
margin-left: -$gl-padding-top;
margin-right: -$gl-padding-top;
background-color: $gray-light; background-color: $gray-light;
border-radius: $border-radius-base; border-radius: $border-radius-base;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
color: $note-disabled-comment-color; color: $note-disabled-comment-color;
line-height: 200px; padding: 90px 0;
.disabled-comment-text {
line-height: normal;
}
a { a {
color: $gl-link-color; color: $gl-link-color;
...@@ -625,7 +629,7 @@ ul.notes { ...@@ -625,7 +629,7 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-right: 0; margin-right: 0;
padding-left: $gl-padding; padding-left: $gl-padding;
} }
...@@ -667,7 +671,7 @@ ul.notes { ...@@ -667,7 +671,7 @@ ul.notes {
.line-resolve-all { .line-resolve-all {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
padding: 6px 10px; padding: 5px 10px 6px;
background-color: $gray-light; background-color: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
...@@ -680,6 +684,10 @@ ul.notes { ...@@ -680,6 +684,10 @@ ul.notes {
.line-resolve-btn { .line-resolve-btn {
margin-right: 5px; margin-right: 5px;
svg {
vertical-align: middle;
}
} }
} }
...@@ -716,6 +724,10 @@ ul.notes { ...@@ -716,6 +724,10 @@ ul.notes {
} }
} }
.line-resolve-text {
vertical-align: middle;
}
.discussion-next-btn { .discussion-next-btn {
svg { svg {
margin: 0; margin: 0;
...@@ -732,11 +744,6 @@ ul.notes { ...@@ -732,11 +744,6 @@ ul.notes {
// Merge request notes in diffs // Merge request notes in diffs
.diff-file { .diff-file {
// Diff is side by side
.notes_content.parallel .note-header .note-headline-light {
display: block;
position: relative;
}
// Diff is inline // Diff is inline
.notes_content .note-header .note-headline-light { .notes_content .note-header .note-headline-light {
display: inline-block; display: inline-block;
......
...@@ -255,7 +255,6 @@ ...@@ -255,7 +255,6 @@
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
line-height: 13px; line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px; letter-spacing: .4px;
padding: 6px 14px; padding: 6px 14px;
text-align: center; text-align: center;
...@@ -392,10 +391,6 @@ a.deploy-project-label { ...@@ -392,10 +391,6 @@ a.deploy-project-label {
} }
} }
.last-push-widget {
margin-top: -1px;
}
.fork-namespaces { .fork-namespaces {
.row { .row {
-webkit-flex-wrap: wrap; -webkit-flex-wrap: wrap;
...@@ -698,7 +693,6 @@ pre.light-well { ...@@ -698,7 +693,6 @@ pre.light-well {
a.allowed-to-merge, a.allowed-to-merge,
a.allowed-to-push { a.allowed-to-push {
cursor: pointer; cursor: pointer;
cursor: hand;
} }
.protected-branch-push-access-list, .protected-branch-push-access-list,
......
class Admin::HookLogsController < Admin::ApplicationController
include HooksExecution
before_action :hook, only: [:show, :retry]
before_action :hook_log, only: [:show, :retry]
respond_to :html
def show
end
def retry
status, message = hook.execute(hook_log.request_data, hook_log.trigger)
set_hook_execution_notice(status, message)
redirect_to edit_admin_hook_path(@hook)
end
private
def hook
@hook ||= SystemHook.find(params[:hook_id])
end
def hook_log
@hook_log ||= hook.web_hook_logs.find(params[:id])
end
end
class Admin::HooksController < Admin::ApplicationController class Admin::HooksController < Admin::ApplicationController
before_action :hook, only: :edit include HooksExecution
before_action :hook_logs, only: :edit
def index def index
@hooks = SystemHook.all @hooks = SystemHook.all
...@@ -36,15 +38,9 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -36,15 +38,9 @@ class Admin::HooksController < Admin::ApplicationController
end end
def test def test
data = { status, message = hook.execute(sample_hook_data, 'system_hooks')
event_name: "project_create",
name: "Ruby", set_hook_execution_notice(status, message)
path: "ruby",
project_id: 1,
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
hook.execute(data, 'system_hooks')
redirect_back_or_default redirect_back_or_default
end end
...@@ -55,6 +51,11 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -55,6 +51,11 @@ class Admin::HooksController < Admin::ApplicationController
@hook ||= SystemHook.find(params[:id]) @hook ||= SystemHook.find(params[:id])
end end
def hook_logs
@hook_logs ||=
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end
def hook_params def hook_params
params.require(:hook).permit( params.require(:hook).permit(
:enable_ssl_verification, :enable_ssl_verification,
...@@ -65,4 +66,15 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -65,4 +66,15 @@ class Admin::HooksController < Admin::ApplicationController
:url :url
) )
end end
def sample_hook_data
{
event_name: "project_create",
name: "Ruby",
path: "ruby",
project_id: 1,
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
end
end end
class Admin::BuildsController < Admin::ApplicationController class Admin::JobsController < Admin::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@all_builds = Ci::Build @all_builds = Ci::Build
...@@ -20,6 +20,6 @@ class Admin::BuildsController < Admin::ApplicationController ...@@ -20,6 +20,6 @@ class Admin::BuildsController < Admin::ApplicationController
def cancel_all def cancel_all
Ci::Build.running_or_pending.each(&:cancel) Ci::Build.running_or_pending.each(&:cancel)
redirect_to admin_builds_path redirect_to admin_jobs_path
end end
end end
...@@ -287,12 +287,8 @@ class ApplicationController < ActionController::Base ...@@ -287,12 +287,8 @@ class ApplicationController < ActionController::Base
request.base_url request.base_url
end end
def set_locale def set_locale(&block)
Gitlab::I18n.set_locale(current_user) Gitlab::I18n.with_user_locale(current_user, &block)
yield
ensure
Gitlab::I18n.reset_locale
end end
def sessionless_sign_in(user) def sessionless_sign_in(user)
......
...@@ -8,17 +8,6 @@ module DiffForPath ...@@ -8,17 +8,6 @@ module DiffForPath
return render_404 unless diff_file return render_404 unless diff_file
diff_commit = commit_for_diff(diff_file) render json: { html: view_to_html_string('projects/diffs/_content', diff_file: diff_file) }
blob = diff_file.blob(diff_commit)
locals = {
diff_file: diff_file,
diff_commit: diff_commit,
diff_refs: diffs.diff_refs,
blob: blob,
project: project
}
render json: { html: view_to_html_string('projects/diffs/_content', locals) }
end end
end end
module HooksExecution
extend ActiveSupport::Concern
private
def set_hook_execution_notice(status, message)
if status && status >= 200 && status < 400
flash[:notice] = "Hook executed successfully: HTTP #{status}"
elsif status
flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
else
flash[:alert] = "Hook execution failed: #{message}"
end
end
end
...@@ -14,7 +14,16 @@ module IssuableActions ...@@ -14,7 +14,16 @@ module IssuableActions
name = issuable.human_class_name name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted." flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]) index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
respond_to do |format|
format.html { redirect_to index_path }
format.json do
render json: {
web_url: index_path
}
end
end
end end
def bulk_update def bulk_update
......
...@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(non_public: true)).page(params[:page]) @projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format| respond_to do |format|
format.html { @last_push = current_user.recent_push } format.html
format.atom do format.atom do
load_events load_events
render layout: false render layout: false
...@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(starred: true)). @projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page]) includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = [] @groups = []
respond_to do |format| respond_to do |format|
......
...@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController ...@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html respond_to :html
def activity def activity
@last_push = current_user.recent_push
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -172,7 +172,6 @@ class GroupsController < Groups::ApplicationController ...@@ -172,7 +172,6 @@ class GroupsController < Groups::ApplicationController
def user_actions def user_actions
if current_user if current_user
@last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group) @notification_setting = current_user.notification_settings_for(group)
end end
end end
......
...@@ -46,7 +46,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -46,7 +46,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
def keep def keep
build.keep_artifacts! build.keep_artifacts!
redirect_to namespace_project_build_path(project.namespace, project, build) redirect_to namespace_project_job_path(project.namespace, project, build)
end end
def latest_succeeded def latest_succeeded
...@@ -79,7 +79,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -79,7 +79,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def build_from_id def build_from_id
project.builds.find_by(id: params[:build_id]) if params[:build_id] project.builds.find_by(id: params[:job_id]) if params[:job_id]
end end
def build_from_ref def build_from_ref
......
class Projects::BuildArtifactsController < Projects::ApplicationController
include ExtractsPath
include RendersBlob
before_action :authorize_read_build!
before_action :extract_ref_name_and_path
before_action :validate_artifacts!
def download
redirect_to download_namespace_project_job_artifacts_path(project.namespace, project, job)
end
def browse
redirect_to browse_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
end
def file
redirect_to file_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
end
def raw
redirect_to raw_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
end
def latest_succeeded
redirect_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, job, ref_name_and_path: params[:ref_name_and_path], job: params[:job])
end
private
def validate_artifacts!
render_404 unless job && job.artifacts?
end
def extract_ref_name_and_path
return unless params[:ref_name_and_path]
@ref_name, @path = extract_ref(params[:ref_name_and_path])
end
def job
@job ||= job_from_id || job_from_ref
end
def job_from_id
project.builds.find_by(id: params[:build_id]) if params[:build_id]
end
def job_from_ref
return unless @ref_name
jobs = project.latest_successful_builds_for(@ref_name)
jobs.find_by(name: params[:job])
end
end
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :authorize_read_build!
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all]
layout 'project'
def index def index
@scope = params[:scope] redirect_to namespace_project_jobs_path(project.namespace, project)
@all_builds = project.builds.relevant
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
when 'pending'
@builds.pending.reverse_order
when 'running'
@builds.running.reverse_order
when 'finished'
@builds.finished
else
@builds
end
@builds = @builds.includes([
{ pipeline: :project },
:project,
:tags
])
@builds = @builds.page(params[:page]).per(30)
end
def cancel_all
return access_denied! unless can?(current_user, :update_build, project)
@project.builds.running_or_pending.each do |build|
build.cancel if can?(current_user, :update_build, build)
end
redirect_to namespace_project_builds_path(project.namespace, project)
end end
def show def show
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') redirect_to namespace_project_job_path(project.namespace, project, job)
@builds = @builds.where("id not in (?)", @build.id)
@pipeline = @build.pipeline
end
def trace
build.trace.read do |stream|
respond_to do |format|
format.json do
result = {
id: @build.id, status: @build.status, complete: @build.complete?
}
if stream.valid?
stream.limit
state = params[:state].presence
trace = stream.html_with_state(state)
result.merge!(trace.to_h)
end
render json: result
end
end
end
end
def retry
return respond_422 unless @build.retryable?
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
def play
return respond_422 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel
return respond_422 unless @build.cancelable?
@build.cancel
redirect_to build_path(@build)
end
def status
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
.represent_status(@build)
end
def erase
if @build.erase(erased_by: current_user)
redirect_to namespace_project_build_path(project.namespace, project, @build),
notice: "Build has been successfully erased!"
else
respond_422
end
end end
def raw def raw
build.trace.read do |stream| redirect_to raw_namespace_project_job_path(project.namespace, project, job)
if stream.file?
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
end
end end
private private
def authorize_update_build! def job
return access_denied! unless can?(current_user, :update_build, build) @job ||= project.builds.find(params[:id])
end
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
end
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end end
end end
...@@ -51,13 +51,9 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -51,13 +51,9 @@ class Projects::CompareController < Projects::ApplicationController
if @compare if @compare
@commits = @compare.commits @commits = @compare.commits
@start_commit = @compare.start_commit
@commit = @compare.commit
@base_commit = @compare.base_commit
@diffs = @compare.diffs(diff_options) @diffs = @compare.diffs(diff_options)
environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit } environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true @diff_notes_disabled = true
......
class Projects::HookLogsController < Projects::ApplicationController
include HooksExecution
before_action :authorize_admin_project!
before_action :hook, only: [:show, :retry]
before_action :hook_log, only: [:show, :retry]
respond_to :html
layout 'project_settings'
def show
end
def retry
status, message = hook.execute(hook_log.request_data, hook_log.trigger)
set_hook_execution_notice(status, message)
redirect_to edit_namespace_project_hook_path(@project.namespace, @project, @hook)
end
private
def hook
@hook ||= @project.hooks.find(params[:hook_id])
end
def hook_log
@hook_log ||= hook.web_hook_logs.find(params[:id])
end
end
class Projects::HooksController < Projects::ApplicationController class Projects::HooksController < Projects::ApplicationController
include HooksExecution
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :hook, only: :edit before_action :hook_logs, only: :edit
respond_to :html respond_to :html
...@@ -34,13 +36,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -34,13 +36,7 @@ class Projects::HooksController < Projects::ApplicationController
if !@project.empty_repo? if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user) status, message = TestHookService.new.execute(hook, current_user)
if status && status >= 200 && status < 400 set_hook_execution_notice(status, message)
flash[:notice] = "Hook executed successfully: HTTP #{status}"
elsif status
flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
else
flash[:alert] = "Hook execution failed: #{message}"
end
else else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.' flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
end end
...@@ -60,6 +56,11 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -60,6 +56,11 @@ class Projects::HooksController < Projects::ApplicationController
@hook ||= @project.hooks.find(params[:id]) @hook ||= @project.hooks.find(params[:id])
end end
def hook_logs
@hook_logs ||=
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end
def hook_params def hook_params
params.require(:hook).permit( params.require(:hook).permit(
:job_events, :job_events,
......
...@@ -149,10 +149,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -149,10 +149,7 @@ class Projects::IssuesController < Projects::ApplicationController
format.json do format.json do
if @issue.valid? if @issue.valid?
render json: @issue.to_json(methods: [:task_status, :task_status_short], render json: IssueSerializer.new.represent(@issue)
include: { milestone: {},
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { methods: :text_color } })
else else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end end
......
class Projects::JobsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all]
layout 'project'
def index
@scope = params[:scope]
@all_builds = project.builds.relevant
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
when 'pending'
@builds.pending.reverse_order
when 'running'
@builds.running.reverse_order
when 'finished'
@builds.finished
else
@builds
end
@builds = @builds.includes([
{ pipeline: :project },
:project,
:tags
])
@builds = @builds.page(params[:page]).per(30)
end
def cancel_all
return access_denied! unless can?(current_user, :update_build, project)
@project.builds.running_or_pending.each do |build|
build.cancel if can?(current_user, :update_build, build)
end
redirect_to namespace_project_jobs_path(project.namespace, project)
end
def show
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @builds.where("id not in (?)", @build.id)
@pipeline = @build.pipeline
end
def trace
build.trace.read do |stream|
respond_to do |format|
format.json do
result = {
id: @build.id, status: @build.status, complete: @build.complete?
}
if stream.valid?
stream.limit
state = params[:state].presence
trace = stream.html_with_state(state)
result.merge!(trace.to_h)
end
render json: result
end
end
end
end
def retry
return respond_422 unless @build.retryable?
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
def play
return respond_422 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel
return respond_422 unless @build.cancelable?
@build.cancel
redirect_to build_path(@build)
end
def status
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
.represent_status(@build)
end
def erase
if @build.erase(erased_by: current_user)
redirect_to namespace_project_job_path(project.namespace, project, @build),
notice: "Build has been successfully erased!"
else
respond_422
end
end
def raw
build.trace.read do |stream|
if stream.file?
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
end
end
private
def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, build)
end
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
end
def build_path(build)
namespace_project_job_path(build.project.namespace, build.project, build)
end
end
...@@ -17,7 +17,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -17,7 +17,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
] ]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_commit_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :check_if_can_be_merged, only: :show before_action :check_if_can_be_merged, only: :show
...@@ -134,8 +133,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -134,8 +133,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = true @diff_notes_disabled = true
end end
define_commit_vars
render_diff_for_path(@diffs) render_diff_for_path(@diffs)
end end
...@@ -557,11 +554,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -557,11 +554,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end end
def define_commit_vars
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
end
def define_diff_vars def define_diff_vars
@merge_request_diff = @merge_request_diff =
if params[:diff_id] if params[:diff_id]
...@@ -626,7 +618,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -626,7 +618,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@source_project = merge_request.source_project @source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse @commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit @commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@note_counts = Note.where(commit_id: @commits.map(&:id)). @note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
......
...@@ -25,6 +25,6 @@ class Projects::PushRulesController < Projects::ApplicationController ...@@ -25,6 +25,6 @@ class Projects::PushRulesController < Projects::ApplicationController
# Only allow a trusted parameter "white list" through. # Only allow a trusted parameter "white list" through.
def push_rule_params def push_rule_params
params.require(:push_rule).permit(:deny_delete_tag, :delete_branch_regex, params.require(:push_rule).permit(:deny_delete_tag, :delete_branch_regex,
:commit_message_regex, :force_push_regex, :author_email_regex, :member_check, :file_name_regex, :max_file_size, :prevent_secrets) :commit_message_regex, :branch_name_regex, :force_push_regex, :author_email_regex, :member_check, :file_name_regex, :max_file_size, :prevent_secrets)
end end
end end
...@@ -81,6 +81,6 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -81,6 +81,6 @@ class Projects::RefsController < Projects::ApplicationController
private private
def validate_ref_id def validate_ref_id
return not_found! if params[:id].present? && params[:id] !~ Gitlab::Regex.git_reference_regex return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end end
end end
...@@ -120,7 +120,7 @@ module BlobHelper ...@@ -120,7 +120,7 @@ module BlobHelper
def blob_raw_url def blob_raw_url
if @build && @entry if @build && @entry
raw_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: @entry.path) raw_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: @entry.path)
elsif @snippet elsif @snippet
if @snippet.project_id if @snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
......
...@@ -2,7 +2,7 @@ module BuildsHelper ...@@ -2,7 +2,7 @@ module BuildsHelper
def build_summary(build, skip: false) def build_summary(build, skip: false)
if build.has_trace? if build.has_trace?
if skip if skip
link_to "View job trace", pipeline_build_url(build.pipeline, build) link_to "View job trace", pipeline_job_url(build.pipeline, build)
else else
build.trace.html(last_lines: 10).html_safe build.trace.html(last_lines: 10).html_safe
end end
...@@ -20,8 +20,8 @@ module BuildsHelper ...@@ -20,8 +20,8 @@ module BuildsHelper
def javascript_build_options def javascript_build_options
{ {
page_url: namespace_project_build_url(@project.namespace, @project, @build), page_url: namespace_project_job_url(@project.namespace, @project, @build),
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_url: namespace_project_job_url(@project.namespace, @project, @build, :json),
build_status: @build.status, build_status: @build.status,
build_stage: @build.stage, build_stage: @build.stage,
log_state: '' log_state: ''
...@@ -31,7 +31,7 @@ module BuildsHelper ...@@ -31,7 +31,7 @@ module BuildsHelper
def build_failed_issue_options def build_failed_issue_options
{ {
title: "Build Failed ##{@build.id}", title: "Build Failed ##{@build.id}",
description: namespace_project_build_url(@project.namespace, @project, @build) description: namespace_project_job_url(@project.namespace, @project, @build)
} }
end end
end end
...@@ -15,16 +15,6 @@ module CommitsHelper ...@@ -15,16 +15,6 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer)) commit_person_link(commit, options.merge(source: :committer))
end end
def image_diff_class(diff)
if diff.deleted_file
"deleted"
elsif diff.new_file
"added"
else
nil
end
end
def commit_to_html(commit, ref, project) def commit_to_html(commit, ref, project)
render 'projects/commits/commit', render 'projects/commits/commit',
commit: commit, commit: commit,
......
...@@ -102,14 +102,14 @@ module DiffHelper ...@@ -102,14 +102,14 @@ module DiffHelper
].join(' ').html_safe ].join(' ').html_safe
end end
def commit_for_diff(diff_file) def diff_file_blob_raw_path(diff_file)
return diff_file.content_commit if diff_file.content_commit namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path))
end
if diff_file.deleted_file def diff_file_old_blob_raw_path(diff_file)
@base_commit || @commit.parent || @commit sha = diff_file.old_content_sha
else return unless sha
@commit namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
end end
def diff_file_html_data(project, diff_file_path, diff_commit_id) def diff_file_html_data(project, diff_file_path, diff_commit_id)
...@@ -120,8 +120,8 @@ module DiffHelper ...@@ -120,8 +120,8 @@ module DiffHelper
} }
end end
def editable_diff?(diff) def editable_diff?(diff_file)
!diff.deleted_file && @merge_request && @merge_request.source_project !diff_file.deleted_file? && @merge_request && @merge_request.source_project
end end
private private
......
...@@ -50,8 +50,8 @@ module GitlabRoutingHelper ...@@ -50,8 +50,8 @@ module GitlabRoutingHelper
namespace_project_cycle_analytics_path(project.namespace, project, *args) namespace_project_cycle_analytics_path(project.namespace, project, *args)
end end
def project_builds_path(project, *args) def project_jobs_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args) namespace_project_jobs_path(project.namespace, project, *args)
end end
def project_ref_path(project, ref_name, *args) def project_ref_path(project, ref_name, *args)
...@@ -110,8 +110,8 @@ module GitlabRoutingHelper ...@@ -110,8 +110,8 @@ module GitlabRoutingHelper
namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args) namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
end end
def pipeline_build_url(pipeline, build, *args) def pipeline_job_url(pipeline, build, *args)
namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args) namespace_project_job_url(pipeline.project.namespace, pipeline.project, build.id, *args)
end end
def commits_url(entity, *args) def commits_url(entity, *args)
...@@ -215,13 +215,13 @@ module GitlabRoutingHelper ...@@ -215,13 +215,13 @@ module GitlabRoutingHelper
case action case action
when 'download' when 'download'
download_namespace_project_build_artifacts_path(*args) download_namespace_project_job_artifacts_path(*args)
when 'browse' when 'browse'
browse_namespace_project_build_artifacts_path(*args) browse_namespace_project_job_artifacts_path(*args)
when 'file' when 'file'
file_namespace_project_build_artifacts_path(*args) file_namespace_project_job_artifacts_path(*args)
when 'raw' when 'raw'
raw_namespace_project_build_artifacts_path(*args) raw_namespace_project_job_artifacts_path(*args)
end end
end end
......
...@@ -210,6 +210,27 @@ module IssuablesHelper ...@@ -210,6 +210,27 @@ module IssuablesHelper
issuable_filter_params.any? { |k| params.key?(k) } issuable_filter_params.any? { |k| params.key?(k) }
end end
def issuable_initial_data(issuable)
{
endpoint: namespace_project_issue_path(@project.namespace, @project, issuable),
canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable),
canMove: current_user ? issuable.can_move?(current_user) : false,
issuableRef: issuable.to_reference,
isConfidential: issuable.confidential,
markdownPreviewUrl: preview_markdown_path(@project),
markdownDocs: help_page_path('user/markdown'),
projectsAutocompleteUrl: autocomplete_projects_path(project_id: @project.id),
issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description),
initialDescriptionText: issuable.description
}.to_json
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
...@@ -69,13 +69,12 @@ module LabelsHelper ...@@ -69,13 +69,12 @@ module LabelsHelper
end end
def render_colored_label(label, label_suffix = '', tooltip: true) def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label.color)
text_color = text_color_for_bg(label_color)
# Intentionally not using content_tag here so that this method can be called # Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter # by LabelReferenceFilter
span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) + span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
%(style="background-color: #{label_color}; color: #{text_color}" ) + %(style="background-color: #{label.color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) + %(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>) %(#{escape_once(label.name)}#{label_suffix}</span>)
......
...@@ -122,6 +122,7 @@ module ProjectsHelper ...@@ -122,6 +122,7 @@ module ProjectsHelper
def last_push_event def last_push_event
return unless current_user return unless current_user
return current_user.recent_push unless @project
project_ids = [@project.id] project_ids = [@project.id]
if fork = current_user.fork_of(@project) if fork = current_user.fork_of(@project)
......
class BaseMailer < ActionMailer::Base class BaseMailer < ActionMailer::Base
around_action :render_with_default_locale
helper ApplicationHelper helper ApplicationHelper
helper MarkupHelper helper MarkupHelper
...@@ -14,6 +16,10 @@ class BaseMailer < ActionMailer::Base ...@@ -14,6 +16,10 @@ class BaseMailer < ActionMailer::Base
private private
def render_with_default_locale(&block)
Gitlab::I18n.with_default_locale(&block)
end
def default_sender_address def default_sender_address
address = Mail::Address.new(Gitlab.config.gitlab.email_from) address = Mail::Address.new(Gitlab.config.gitlab.email_from)
address.display_name = Gitlab.config.gitlab.email_display_name address.display_name = Gitlab.config.gitlab.email_display_name
......
...@@ -35,6 +35,7 @@ module Ci ...@@ -35,6 +35,7 @@ module Ci
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual).relevant } scope :manual_actions, ->() { where(when: :manual).relevant }
scope :codeclimate, ->() { where(name: 'codeclimate') }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
...@@ -52,6 +53,12 @@ module Ci ...@@ -52,6 +53,12 @@ module Ci
after_destroy :update_project_statistics after_destroy :update_project_statistics
class << self class << self
# This is needed for url_for to work,
# as the controller is JobsController
def model_name
ActiveModel::Name.new(self, nil, 'job')
end
def first_pending def first_pending
pending.unstarted.order('created_at ASC').first pending.unstarted.order('created_at ASC').first
end end
...@@ -440,6 +447,11 @@ module Ci ...@@ -440,6 +447,11 @@ module Ci
trace trace
end end
def has_codeclimate_json?
options.dig(:artifacts, :paths) == ['codeclimate.json'] &&
artifacts_metadata?
end
private private
def update_artifacts_size def update_artifacts_size
......
...@@ -396,6 +396,10 @@ module Ci ...@@ -396,6 +396,10 @@ module Ci
.fabricate! .fabricate!
end end
def codeclimate_artifact
artifacts.codeclimate.find(&:has_codeclimate_json?)
end
private private
def pipeline_data def pipeline_data
......
...@@ -33,14 +33,4 @@ module NoteOnDiff ...@@ -33,14 +33,4 @@ module NoteOnDiff
def created_at_diff?(diff_refs) def created_at_diff?(diff_refs)
false false
end end
private
def noteable_diff_refs
if noteable.respond_to?(:diff_sha_refs)
noteable.diff_sha_refs
else
noteable.diff_refs
end
end
end end
...@@ -68,7 +68,7 @@ class DiffNote < Note ...@@ -68,7 +68,7 @@ class DiffNote < Note
return false unless supported? return false unless supported?
return true if for_commit? return true if for_commit?
diff_refs ||= noteable_diff_refs diff_refs ||= noteable.diff_refs
self.position.diff_refs == diff_refs self.position.diff_refs == diff_refs
end end
...@@ -104,7 +104,7 @@ class DiffNote < Note ...@@ -104,7 +104,7 @@ class DiffNote < Note
self.project, self.project,
nil, nil,
old_diff_refs: self.position.diff_refs, old_diff_refs: self.position.diff_refs,
new_diff_refs: noteable_diff_refs, new_diff_refs: noteable.diff_refs,
paths: self.position.paths paths: self.position.paths
).execute(self) ).execute(self)
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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