Commit a6124a8b authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

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

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 590b6365 e530453d
...@@ -124,7 +124,7 @@ import ApproversSelect from './approvers_select'; ...@@ -124,7 +124,7 @@ 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':
......
...@@ -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,22 +189,46 @@ export default { ...@@ -65,22 +189,46 @@ 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>
<form-component
v-if="canUpdate && showForm"
:form-state="formState"
:can-move="canMove"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
:markdown-docs="markdownDocs"
:markdown-preview-url="markdownPreviewUrl"
:project-path="projectPath"
:project-namespace="projectNamespace"
:projects-autocomplete-url="projectsAutocompleteUrl"
/>
<div v-else>
<title-component <title-component
:issuable-ref="issuableRef" :issuable-ref="issuableRef"
:title-html="state.titleHtml" :title-html="state.titleHtml"
...@@ -93,4 +241,5 @@ export default { ...@@ -93,4 +241,5 @@ export default {
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:task-status="state.taskStatus" /> :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', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const initialData = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.issuable-edit').on('click', (e) => {
e.preventDefault();
eventHub.$emit('open.form');
});
return new Vue({
el: document.getElementById('js-issuable-app'), el: document.getElementById('js-issuable-app'),
components: { components: {
issuableApp, issuableApp,
}, },
data() { 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 { return {
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate), ...initialData,
endpoint,
issuableRef,
initialTitle: issuableTitleElement.innerHTML,
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
}; };
}, },
render(createElement) { render(createElement) {
return createElement('issuable-app', { return createElement('issuable-app', {
props: { props: {
canUpdate: this.canUpdate, canUpdate: this.canUpdate,
canDestroy: this.canDestroy,
canMove: this.canMove,
endpoint: this.endpoint, endpoint: this.endpoint,
issuableRef: this.issuableRef, issuableRef: this.issuableRef,
initialTitle: this.initialTitle, initialTitleHtml: this.initialTitleHtml,
initialTitleText: this.initialTitleText,
initialDescriptionHtml: this.initialDescriptionHtml, initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText, 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);
......
...@@ -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) {
......
<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>
...@@ -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');
},
}; };
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
...@@ -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
......
...@@ -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
...@@ -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
...@@ -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
...@@ -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?
......
...@@ -52,6 +52,12 @@ module Ci ...@@ -52,6 +52,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
......
...@@ -25,7 +25,7 @@ class AnalyticsBuildEntity < Grape::Entity ...@@ -25,7 +25,7 @@ class AnalyticsBuildEntity < Grape::Entity
end end
expose :url do |build| expose :url do |build|
url_to(:namespace_project_build, build) url_to(:namespace_project_job, build)
end end
expose :commit_url do |build| expose :commit_url do |build|
......
...@@ -6,7 +6,7 @@ class BuildActionEntity < Grape::Entity ...@@ -6,7 +6,7 @@ class BuildActionEntity < Grape::Entity
end end
expose :path do |build| expose :path do |build|
play_namespace_project_build_path( play_namespace_project_job_path(
build.project.namespace, build.project.namespace,
build.project, build.project,
build) build)
......
...@@ -6,7 +6,7 @@ class BuildArtifactEntity < Grape::Entity ...@@ -6,7 +6,7 @@ class BuildArtifactEntity < Grape::Entity
end end
expose :path do |build| expose :path do |build|
download_namespace_project_build_artifacts_path( download_namespace_project_job_artifacts_path(
build.project.namespace, build.project.namespace,
build.project, build.project,
build) build)
......
...@@ -5,15 +5,15 @@ class BuildEntity < Grape::Entity ...@@ -5,15 +5,15 @@ class BuildEntity < Grape::Entity
expose :name expose :name
expose :build_path do |build| expose :build_path do |build|
path_to(:namespace_project_build, build) path_to(:namespace_project_job, build)
end end
expose :retry_path do |build| expose :retry_path do |build|
path_to(:retry_namespace_project_build, build) path_to(:retry_namespace_project_job, build)
end end
expose :play_path, if: -> (*) { playable? } do |build| expose :play_path, if: -> (*) { playable? } do |build|
path_to(:play_namespace_project_build, build) path_to(:play_namespace_project_job, build)
end end
expose :playable?, as: :playable expose :playable?, as: :playable
......
class IssueEntity < IssuableEntity class IssueEntity < IssuableEntity
include RequestAwareEntity
expose :branch_name expose :branch_name
expose :confidential expose :confidential
expose :assignees, using: API::Entities::UserBasic expose :assignees, using: API::Entities::UserBasic
...@@ -8,4 +10,8 @@ class IssueEntity < IssuableEntity ...@@ -8,4 +10,8 @@ class IssueEntity < IssuableEntity
expose :weight expose :weight
expose :milestone, using: API::Entities::Milestone expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity expose :labels, using: LabelEntity
expose :web_url do |issue|
namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end
end end
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
%span %span
Groups Groups
= nav_link path: 'builds#index' do = nav_link path: 'builds#index' do
= link_to admin_builds_path, title: 'Jobs' do = link_to admin_jobs_path, title: 'Jobs' do
%span %span
Jobs Jobs
= nav_link path: ['runners#index', 'runners#show'] do = nav_link path: ['runners#index', 'runners#show'] do
......
...@@ -4,15 +4,15 @@ ...@@ -4,15 +4,15 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
- build_path_proc = ->(scope) { admin_builds_path(scope: scope) } - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls .nav-controls
- if @all_builds.running_or_pending.any? - if @all_builds.running_or_pending.any?
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post = link_to 'Cancel all', cancel_all_admin_jobs_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
.row-content-block.second-block .row-content-block.second-block
#{(@scope || 'all').capitalize} jobs #{(@scope || 'all').capitalize} jobs
%ul.content-list.builds-content-list.admin-builds-table %ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true = render "projects/jobs/table", builds: @builds, admin: true
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
%tr.build %tr.build
%td.id %td.id
- if project - if project
= link_to namespace_project_build_path(project.namespace, project, build) do = link_to namespace_project_job_path(project.namespace, project, build) do
%strong ##{build.id} %strong ##{build.id}
- else - else
%strong ##{build.id} %strong ##{build.id}
......
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
-# Shortcut to Pipelines > Jobs -# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds - if project_nav_tab? :builds
%li.hidden %li.hidden
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
Jobs Jobs
-# Shortcut to commits page -# Shortcut to commits page
......
%a{ href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" } %a{ href: pipeline_job_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" }
= build.name = build.name
Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) Job #<%= build.id %> ( <%= pipeline_job_url(pipeline, build) %> )
- path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path) - path_to_directory = browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: directory.path)
%tr.tree-item{ 'data-link' => path_to_directory } %tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name %td.tree-item-file-name
......
- path_to_file = file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path) - path_to_file = file_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: file.path)
%tr.tree-item{ 'data-link' => path_to_file } %tr.tree-item{ 'data-link' => path_to_file }
- blob = file.blob - blob = file.blob
......
- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' - page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head" = render "projects/pipelines/head"
= render "projects/builds/header", show_controls: false = render "projects/jobs/header", show_controls: false
.tree-holder .tree-holder
.nav-block .nav-block
.tree-controls .tree-controls
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), = link_to download_namespace_project_job_artifacts_path(@project.namespace, @project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download') = icon('download')
Download artifacts archive Download artifacts archive
%ul.breadcrumb.repo-breadcrumb %ul.breadcrumb.repo-breadcrumb
%li %li
= link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build) = link_to 'Artifacts', browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path| - path_breadcrumbs do |title, path|
%li %li
= link_to truncate(title, length: 40), browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path) = link_to truncate(title, length: 40), browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path)
.tree-content-holder .tree-content-holder
%table.table.tree-table %table.table.tree-table
......
- page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' - page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head" = render "projects/pipelines/head"
= render "projects/builds/header", show_controls: false = render "projects/jobs/header", show_controls: false
#tree-holder.tree-holder #tree-holder.tree-holder
.nav-block .nav-block
%ul.breadcrumb.repo-breadcrumb %ul.breadcrumb.repo-breadcrumb
%li %li
= link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build) = link_to 'Artifacts', browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path| - path_breadcrumbs do |title, path|
- title = truncate(title, length: 40) - title = truncate(title, length: 40)
%li %li
- if path == @path - if path == @path
= link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path) do = link_to file_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path) do
%strong= title %strong= title
- else - else
= link_to title, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path) = link_to title, browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path)
%article.file-holder %article.file-holder
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%td.branch-commit %td.branch-commit
- if can?(current_user, :read_build, job) - if can?(current_user, :read_build, job)
= link_to namespace_project_build_url(job.project.namespace, job.project, job) do = link_to namespace_project_job_url(job.project.namespace, job.project, job) do
%span.build-link ##{job.id} %span.build-link ##{job.id}
- else - else
%span.build-link ##{job.id} %span.build-link ##{job.id}
...@@ -95,16 +95,16 @@ ...@@ -95,16 +95,16 @@
%td %td
.pull-right .pull-right
- if can?(current_user, :read_build, job) && job.artifacts? - if can?(current_user, :read_build, job) && job.artifacts?
= link_to download_namespace_project_build_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do = link_to download_namespace_project_job_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= icon('download') = icon('download')
- if can?(current_user, :update_build, job) - if can?(current_user, :update_build, job)
- if job.active? - if job.active?
= link_to cancel_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = link_to cancel_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred') = icon('remove', class: 'cred')
- elsif allow_retry - elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job) - if job.playable? && !admin && can?(current_user, :update_build, job)
= link_to play_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = link_to play_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play') = custom_icon('icon_play')
- elsif job.retryable? - elsif job.retryable?
= link_to retry_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do = link_to retry_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
= icon('repeat') = icon('repeat')
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%ul %ul
- if can_update_issue - if can_update_issue
%li %li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'issuable-edit'
%li %li
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li %li
...@@ -55,10 +55,8 @@ ...@@ -55,10 +55,8 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
#js-issuable-app{ "data" => { "endpoint" => realtime_changes_namespace_project_issue_path(@project.namespace, @project, @issue), %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue)
"can-update" => can?(current_user, :update_issue, @issue).to_s, #js-issuable-app
"issuable-ref" => @issue.to_reference,
} }
%h2.title= markdown_field(@issue, :title) %h2.title= markdown_field(@issue, :title)
- if @issue.description.present? - if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
%strong %strong
Job Job
= link_to "##{@build.id}", namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' = link_to "##{@build.id}", namespace_project_job_path(@project.namespace, @project, @build), class: 'js-build-id'
in pipeline in pipeline
%strong %strong
= link_to "##{pipeline.id}", pipeline_path(pipeline) = link_to "##{pipeline.id}", pipeline_path(pipeline)
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
%strong %strong
= link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name' = link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name'
= render "projects/builds/user" if @build.user = render "projects/jobs/user" if @build.user
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
...@@ -26,6 +26,6 @@ ...@@ -26,6 +26,6 @@
- if can?(current_user, :create_issue, @project) && @build.failed? - if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable? - if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post = link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left') = icon('angle-double-left')
...@@ -30,21 +30,21 @@ ...@@ -30,21 +30,21 @@
- if @build.artifacts? - if @build.artifacts?
.btn-group.btn-group-justified{ role: :group } .btn-group.btn-group-justified{ role: :group }
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do = link_to keep_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep Keep
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do = link_to download_namespace_project_job_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download Download
- if @build.artifacts_metadata? - if @build.artifacts_metadata?
= link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do = link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
Browse Browse
.block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title .title
Job details Job details
- if can?(current_user, :update_build, @build) && @build.retryable? - if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post = link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request - if @build.merge_request
%p.build-detail-row %p.build-detail-row
%span.build-light-text Merge Request: %span.build-light-text Merge Request:
...@@ -69,11 +69,11 @@ ...@@ -69,11 +69,11 @@
\##{@build.runner.id} \##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group } .btn-group.btn-group-justified{ role: :group }
- if @build.has_trace? - if @build.has_trace?
= link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' = link_to 'Raw', raw_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- if @build.active? - if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post = link_to "Cancel", cancel_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- if can?(current_user, :update_build, @project) && @build.erasable? - if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build), = link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
class: "btn btn-sm btn-default", method: :post, class: "btn btn-sm btn-default", method: :post,
data: { confirm: "Are you sure you want to erase this build?" } do data: { confirm: "Are you sure you want to erase this build?" } do
Erase Erase
...@@ -126,7 +126,7 @@ ...@@ -126,7 +126,7 @@
- HasStatus::ORDERED_STATUSES.each do |build_status| - HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build| - builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to namespace_project_build_path(@project.namespace, @project, build) do = link_to namespace_project_job_path(@project.namespace, @project, build) do
= icon('arrow-right') = icon('arrow-right')
%span{ class: "ci-status-icon-#{build.status}" } %span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status) = ci_icon_for_status(build.status)
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
- build_path_proc = ->(scope) { project_builds_path(@project, scope: scope) } - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls .nav-controls
- if can?(current_user, :update_build, @project) - if can?(current_user, :update_build, @project)
- if @all_builds.running_or_pending.any? - if @all_builds.running_or_pending.any?
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), = link_to 'Cancel running', cancel_all_namespace_project_jobs_path(@project.namespace, @project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
......
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
Showing last Showing last
%span.js-truncated-info-size.truncated-info-size>< %span.js-truncated-info-size.truncated-info-size><
KiB of log - KiB of log -
%a.js-raw-link.raw-link{ :href => raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw %a.js-raw-link.raw-link{ :href => raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw
%code.bash.js-build-output %code.bash.js-build-output
.build-loader-animation.js-build-refresh .build-loader-animation.js-build-refresh
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
- if project_nav_tab? :builds - if project_nav_tab? :builds
= nav_link(controller: [:builds, :artifacts]) do = nav_link(controller: [:builds, :artifacts]) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span %span
Jobs Jobs
......
...@@ -51,5 +51,5 @@ ...@@ -51,5 +51,5 @@
%span.stage %span.stage
= build.stage.titleize = build.stage.titleize
%span.build-name %span.build-name
= link_to build.name, pipeline_build_url(pipeline, build) = link_to build.name, pipeline_job_url(pipeline, build)
%pre.build-log= build_summary(build, skip: index >= 10) %pre.build-log= build_summary(build, skip: index >= 10)
---
title: Enables inline editing for an issues title & description
merge_request:
author:
---
title: Change /builds in the URL to /-/jobs. Backward URLs were also added
merge_request: 11407
author:
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# - [project.namespace, project, build] # - [project.namespace, project, build]
# #
# instead of: # instead of:
# - namespace_project_build_path(project.namespace, project, build) # - namespace_project_job_path(project.namespace, project, build)
# #
# Without that, Ci:: namespace is used for resolving routes: # Without that, Ci:: namespace is used for resolving routes:
# - namespace_project_ci_build_path(project.namespace, project, build) # - namespace_project_ci_build_path(project.namespace, project, build)
......
...@@ -142,7 +142,7 @@ namespace :admin do ...@@ -142,7 +142,7 @@ namespace :admin do
resources :cohorts, only: :index resources :cohorts, only: :index
resources :builds, only: :index do resources :jobs, only: :index do
collection do collection do
post :cancel_all post :cancel_all
end end
......
require 'constraints/project_url_constrainer' require 'constraints/project_url_constrainer'
require 'gitlab/routes/legacy_builds'
resources :projects, only: [:index, :new, :create] resources :projects, only: [:index, :new, :create]
...@@ -215,7 +216,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -215,7 +216,8 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do scope '-' do
resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do
collection do collection do
post :cancel_all post :cancel_all
...@@ -246,6 +248,9 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -246,6 +248,9 @@ constraints(ProjectUrlConstrainer.new) do
post :keep post :keep
end end
end end
end
Gitlab::Routes::LegacyBuilds.new(self).draw
resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
member do member do
......
...@@ -233,10 +233,30 @@ The two most obvious issues that replication can have here are: ...@@ -233,10 +233,30 @@ The two most obvious issues that replication can have here are:
### Step 5. Replicating the repositories data ### Step 5. Replicating the repositories data
Getting a new secondary Geo node up and running, will also require the Getting a new secondary Geo node up and running, will also require the
repositories directory to be synced from the primary node. repositories data to be synced.
With GitLab **8.14** you can start the syncing process by clicking the With GitLab **9.0** the syncing process starts automatically from the
"Backfill all repositories" button on `Admin > Geo Nodes` screen. secondary node after the **Add Node** button is pressed.
Currently, this is what is synced:
* Git repositories
* Wikis
* LFS objects
* Issue, merge request, and comment attachments
* User, group, and project avatars
You can monitor the status of the syncing process on a secondary node
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`)
in your browser.
![GitLab Geo dashboard](img/geo-node-dashboard.png)
Disabling a secondary node stops the syncing process.
With GitLab **8.14** this process is started manually from the primary node.
You can start the syncing process by clicking the "Backfill all repositories"
button on `Admin > Geo Nodes` screen.
On previous versions, you can use `rsync` for that: On previous versions, you can use `rsync` for that:
......
...@@ -27,11 +27,11 @@ module SharedBuilds ...@@ -27,11 +27,11 @@ module SharedBuilds
end end
step 'I visit recent build details page' do step 'I visit recent build details page' do
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_job_path(@project.namespace, @project, @build)
end end
step 'I visit project builds page' do step 'I visit project builds page' do
visit namespace_project_builds_path(@project.namespace, @project) visit namespace_project_jobs_path(@project.namespace, @project)
end end
step 'recent build has artifacts available' do step 'recent build has artifacts available' do
...@@ -56,7 +56,7 @@ module SharedBuilds ...@@ -56,7 +56,7 @@ module SharedBuilds
end end
step 'I access artifacts download page' do step 'I access artifacts download page' do
visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build) visit download_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
end end
step 'I see details of a build' do step 'I see details of a build' do
......
...@@ -30,7 +30,7 @@ module SharedMarkdown ...@@ -30,7 +30,7 @@ module SharedMarkdown
end end
step 'I should see the Markdown write tab' do step 'I should see the Markdown write tab' do
expect(find('.gfm-form')).to have_css('.js-md-write-button', visible: true) expect(first('.gfm-form')).to have_link('Write', visible: true)
end end
step 'I should see the Markdown preview' do step 'I should see the Markdown preview' do
...@@ -49,9 +49,9 @@ module SharedMarkdown ...@@ -49,9 +49,9 @@ module SharedMarkdown
end end
step 'I preview a description text like "Bug fixed :smile:"' do step 'I preview a description text like "Bug fixed :smile:"' do
page.within('.gfm-form') do page.within(first('.gfm-form')) do
fill_in 'Description', with: 'Bug fixed :smile:' fill_in 'Description', with: 'Bug fixed :smile:'
find('.js-md-preview-button').click click_link 'Preview'
end end
end end
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
end end
def action_path def action_path
cancel_namespace_project_build_path(subject.project.namespace, cancel_namespace_project_job_path(subject.project.namespace,
subject.project, subject.project,
subject) subject)
end end
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
end end
def details_path def details_path
namespace_project_build_path(subject.project.namespace, namespace_project_job_path(subject.project.namespace,
subject.project, subject.project,
subject) subject)
end end
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
end end
def action_path def action_path
play_namespace_project_build_path(subject.project.namespace, play_namespace_project_job_path(subject.project.namespace,
subject.project, subject.project,
subject) subject)
end end
......
...@@ -16,7 +16,7 @@ module Gitlab ...@@ -16,7 +16,7 @@ module Gitlab
end end
def action_path def action_path
retry_namespace_project_build_path(subject.project.namespace, retry_namespace_project_job_path(subject.project.namespace,
subject.project, subject.project,
subject) subject)
end end
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
end end
def action_path def action_path
play_namespace_project_build_path(subject.project.namespace, play_namespace_project_job_path(subject.project.namespace,
subject.project, subject.project,
subject) subject)
end end
......
...@@ -10,8 +10,8 @@ module Gitlab ...@@ -10,8 +10,8 @@ module Gitlab
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route # - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze commit pipelines merge_requests new].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES RESERVED_WORDS = Gitlab::Regex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS) RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
ROUTES = [ ROUTES = [
Gitlab::EtagCaching::Router::Route.new( Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z), %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z),
......
module Gitlab
module Routes
class LegacyBuilds
def initialize(map)
@map = map
end
def draw
@map.instance_eval do
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
resources :artifacts, only: [], controller: 'build_artifacts' do
collection do
get :latest_succeeded,
path: '*ref_name_and_path',
format: false
end
end
end
member do
get :raw
end
resource :artifacts, only: [], controller: 'build_artifacts' do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
get :raw, path: 'raw/*path', format: false
end
end
end
end
end
end
end
...@@ -12,7 +12,7 @@ describe Projects::ArtifactsController do ...@@ -12,7 +12,7 @@ describe Projects::ArtifactsController do
status: 'success') status: 'success')
end end
let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
before do before do
project.team << [user, :developer] project.team << [user, :developer]
...@@ -22,16 +22,16 @@ describe Projects::ArtifactsController do ...@@ -22,16 +22,16 @@ describe Projects::ArtifactsController do
describe 'GET download' do describe 'GET download' do
it 'sends the artifacts file' do it 'sends the artifacts file' do
expect(controller).to receive(:send_file).with(build.artifacts_file.path, disposition: 'attachment').and_call_original expect(controller).to receive(:send_file).with(job.artifacts_file.path, disposition: 'attachment').and_call_original
get :download, namespace_id: project.namespace, project_id: project, build_id: build get :download, namespace_id: project.namespace, project_id: project, job_id: job
end end
end end
describe 'GET browse' do describe 'GET browse' do
context 'when the directory exists' do context 'when the directory exists' do
it 'renders the browse view' do it 'renders the browse view' do
get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'other_artifacts_0.1.2' get :browse, namespace_id: project.namespace, project_id: project, job_id: job, path: 'other_artifacts_0.1.2'
expect(response).to render_template('projects/artifacts/browse') expect(response).to render_template('projects/artifacts/browse')
end end
...@@ -39,7 +39,7 @@ describe Projects::ArtifactsController do ...@@ -39,7 +39,7 @@ describe Projects::ArtifactsController do
context 'when the directory does not exist' do context 'when the directory does not exist' do
it 'responds Not Found' do it 'responds Not Found' do
get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown' get :browse, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
expect(response).to be_not_found expect(response).to be_not_found
end end
...@@ -49,7 +49,7 @@ describe Projects::ArtifactsController do ...@@ -49,7 +49,7 @@ describe Projects::ArtifactsController do
describe 'GET file' do describe 'GET file' do
context 'when the file exists' do context 'when the file exists' do
it 'renders the file view' do it 'renders the file view' do
get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt' get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
expect(response).to render_template('projects/artifacts/file') expect(response).to render_template('projects/artifacts/file')
end end
...@@ -57,7 +57,7 @@ describe Projects::ArtifactsController do ...@@ -57,7 +57,7 @@ describe Projects::ArtifactsController do
context 'when the file does not exist' do context 'when the file does not exist' do
it 'responds Not Found' do it 'responds Not Found' do
get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown' get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
expect(response).to be_not_found expect(response).to be_not_found
end end
...@@ -67,7 +67,7 @@ describe Projects::ArtifactsController do ...@@ -67,7 +67,7 @@ describe Projects::ArtifactsController do
describe 'GET raw' do describe 'GET raw' do
context 'when the file exists' do context 'when the file exists' do
it 'serves the file using workhorse' do it 'serves the file using workhorse' do
get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt' get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
send_data = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER] send_data = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
...@@ -84,7 +84,7 @@ describe Projects::ArtifactsController do ...@@ -84,7 +84,7 @@ describe Projects::ArtifactsController do
context 'when the file does not exist' do context 'when the file does not exist' do
it 'responds Not Found' do it 'responds Not Found' do
get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown' get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
expect(response).to be_not_found expect(response).to be_not_found
end end
...@@ -92,29 +92,29 @@ describe Projects::ArtifactsController do ...@@ -92,29 +92,29 @@ describe Projects::ArtifactsController do
end end
describe 'GET latest_succeeded' do describe 'GET latest_succeeded' do
def params_from_ref(ref = pipeline.ref, job = build.name, path = 'browse') def params_from_ref(ref = pipeline.ref, job_name = job.name, path = 'browse')
{ {
namespace_id: project.namespace, namespace_id: project.namespace,
project_id: project, project_id: project,
ref_name_and_path: File.join(ref, path), ref_name_and_path: File.join(ref, path),
job: job job: job_name
} }
end end
context 'cannot find the build' do context 'cannot find the job' do
shared_examples 'not found' do shared_examples 'not found' do
it { expect(response).to have_http_status(:not_found) } it { expect(response).to have_http_status(:not_found) }
end end
context 'has no such ref' do context 'has no such ref' do
before do before do
get :latest_succeeded, params_from_ref('TAIL', build.name) get :latest_succeeded, params_from_ref('TAIL', job.name)
end end
it_behaves_like 'not found' it_behaves_like 'not found'
end end
context 'has no such build' do context 'has no such job' do
before do before do
get :latest_succeeded, params_from_ref(pipeline.ref, 'NOBUILD') get :latest_succeeded, params_from_ref(pipeline.ref, 'NOBUILD')
end end
...@@ -124,20 +124,20 @@ describe Projects::ArtifactsController do ...@@ -124,20 +124,20 @@ describe Projects::ArtifactsController do
context 'has no path' do context 'has no path' do
before do before do
get :latest_succeeded, params_from_ref(pipeline.sha, build.name, '') get :latest_succeeded, params_from_ref(pipeline.sha, job.name, '')
end end
it_behaves_like 'not found' it_behaves_like 'not found'
end end
end end
context 'found the build and redirect' do context 'found the job and redirect' do
shared_examples 'redirect to the build' do shared_examples 'redirect to the job' do
it 'redirects' do it 'redirects' do
path = browse_namespace_project_build_artifacts_path( path = browse_namespace_project_job_artifacts_path(
project.namespace, project.namespace,
project, project,
build) job)
expect(response).to redirect_to(path) expect(response).to redirect_to(path)
end end
...@@ -151,7 +151,7 @@ describe Projects::ArtifactsController do ...@@ -151,7 +151,7 @@ describe Projects::ArtifactsController do
get :latest_succeeded, params_from_ref('master') get :latest_succeeded, params_from_ref('master')
end end
it_behaves_like 'redirect to the build' it_behaves_like 'redirect to the job'
end end
context 'with branch name containing slash' do context 'with branch name containing slash' do
...@@ -162,7 +162,7 @@ describe Projects::ArtifactsController do ...@@ -162,7 +162,7 @@ describe Projects::ArtifactsController do
get :latest_succeeded, params_from_ref('improve/awesome') get :latest_succeeded, params_from_ref('improve/awesome')
end end
it_behaves_like 'redirect to the build' it_behaves_like 'redirect to the job'
end end
context 'with branch name and path containing slashes' do context 'with branch name and path containing slashes' do
...@@ -170,14 +170,14 @@ describe Projects::ArtifactsController do ...@@ -170,14 +170,14 @@ describe Projects::ArtifactsController do
pipeline.update(ref: 'improve/awesome', pipeline.update(ref: 'improve/awesome',
sha: project.commit('improve/awesome').sha) sha: project.commit('improve/awesome').sha)
get :latest_succeeded, params_from_ref('improve/awesome', build.name, 'file/README.md') get :latest_succeeded, params_from_ref('improve/awesome', job.name, 'file/README.md')
end end
it 'redirects' do it 'redirects' do
path = file_namespace_project_build_artifacts_path( path = file_namespace_project_job_artifacts_path(
project.namespace, project.namespace,
project, project,
build, job,
'README.md') 'README.md')
expect(response).to redirect_to(path) expect(response).to redirect_to(path)
......
...@@ -198,7 +198,7 @@ describe Projects::EnvironmentsController do ...@@ -198,7 +198,7 @@ describe Projects::EnvironmentsController do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response).to eq( expect(json_response).to eq(
{ 'redirect_url' => { 'redirect_url' =>
"http://test.host/#{project.path_with_namespace}/builds/#{action.id}" }) namespace_project_job_url(project.namespace, project, action) })
end end
end end
...@@ -212,7 +212,7 @@ describe Projects::EnvironmentsController do ...@@ -212,7 +212,7 @@ describe Projects::EnvironmentsController do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response).to eq( expect(json_response).to eq(
{ 'redirect_url' => { 'redirect_url' =>
"http://test.host/#{project.path_with_namespace}/environments/#{environment.id}" }) namespace_project_environment_url(project.namespace, project, environment) })
end end
end end
end end
......
...@@ -204,7 +204,7 @@ describe Projects::IssuesController do ...@@ -204,7 +204,7 @@ describe Projects::IssuesController do
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body['assignees'].first.keys) expect(body['assignees'].first.keys)
.to match_array(%w(id name username avatar_url)) .to match_array(%w(id name username avatar_url state web_url))
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Projects::BuildsController do describe Projects::JobsController do
include ApiHelpers include ApiHelpers
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
...@@ -213,7 +213,7 @@ describe Projects::BuildsController do ...@@ -213,7 +213,7 @@ describe Projects::BuildsController do
it 'redirects to the retried build page' do it 'redirects to the retried build page' do
expect(response).to have_http_status(:found) expect(response).to have_http_status(:found)
expect(response).to redirect_to(namespace_project_build_path(id: Ci::Build.last.id)) expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id))
end end
end end
...@@ -245,7 +245,7 @@ describe Projects::BuildsController do ...@@ -245,7 +245,7 @@ describe Projects::BuildsController do
it 'redirects to the played build page' do it 'redirects to the played build page' do
expect(response).to have_http_status(:found) expect(response).to have_http_status(:found)
expect(response).to redirect_to(namespace_project_build_path(id: build.id)) expect(response).to redirect_to(namespace_project_job_path(id: build.id))
end end
it 'transits to pending' do it 'transits to pending' do
...@@ -281,7 +281,7 @@ describe Projects::BuildsController do ...@@ -281,7 +281,7 @@ describe Projects::BuildsController do
it 'redirects to the canceled build page' do it 'redirects to the canceled build page' do
expect(response).to have_http_status(:found) expect(response).to have_http_status(:found)
expect(response).to redirect_to(namespace_project_build_path(id: build.id)) expect(response).to redirect_to(namespace_project_job_path(id: build.id))
end end
it 'transits to canceled' do it 'transits to canceled' do
...@@ -319,7 +319,7 @@ describe Projects::BuildsController do ...@@ -319,7 +319,7 @@ describe Projects::BuildsController do
it 'redirects to a index page' do it 'redirects to a index page' do
expect(response).to have_http_status(:found) expect(response).to have_http_status(:found)
expect(response).to redirect_to(namespace_project_builds_path) expect(response).to redirect_to(namespace_project_jobs_path)
end end
it 'transits to canceled' do it 'transits to canceled' do
...@@ -336,7 +336,7 @@ describe Projects::BuildsController do ...@@ -336,7 +336,7 @@ describe Projects::BuildsController do
it 'redirects to a index page' do it 'redirects to a index page' do
expect(response).to have_http_status(:found) expect(response).to have_http_status(:found)
expect(response).to redirect_to(namespace_project_builds_path) expect(response).to redirect_to(namespace_project_jobs_path)
end end
end end
...@@ -359,7 +359,7 @@ describe Projects::BuildsController do ...@@ -359,7 +359,7 @@ describe Projects::BuildsController do
it 'redirects to the erased build page' do it 'redirects to the erased build page' do
expect(response).to have_http_status(:found) expect(response).to have_http_status(:found)
expect(response).to redirect_to(namespace_project_build_path(id: build.id)) expect(response).to redirect_to(namespace_project_job_path(id: build.id))
end end
it 'erases artifacts' do it 'erases artifacts' do
......
...@@ -16,7 +16,7 @@ describe 'Admin Builds' do ...@@ -16,7 +16,7 @@ describe 'Admin Builds' do
create(:ci_build, pipeline: pipeline, status: :success) create(:ci_build, pipeline: pipeline, status: :success)
create(:ci_build, pipeline: pipeline, status: :failed) create(:ci_build, pipeline: pipeline, status: :failed)
visit admin_builds_path visit admin_jobs_path
expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_selector('.row-content-block', text: 'All jobs') expect(page).to have_selector('.row-content-block', text: 'All jobs')
...@@ -27,7 +27,7 @@ describe 'Admin Builds' do ...@@ -27,7 +27,7 @@ describe 'Admin Builds' do
context 'when have no jobs' do context 'when have no jobs' do
it 'shows a message' do it 'shows a message' do
visit admin_builds_path visit admin_jobs_path
expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content 'No jobs to show' expect(page).to have_content 'No jobs to show'
...@@ -44,7 +44,7 @@ describe 'Admin Builds' do ...@@ -44,7 +44,7 @@ describe 'Admin Builds' do
build3 = create(:ci_build, pipeline: pipeline, status: :success) build3 = create(:ci_build, pipeline: pipeline, status: :success)
build4 = create(:ci_build, pipeline: pipeline, status: :failed) build4 = create(:ci_build, pipeline: pipeline, status: :failed)
visit admin_builds_path(scope: :pending) visit admin_jobs_path(scope: :pending)
expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page.find('.build-link')).to have_content(build1.id) expect(page.find('.build-link')).to have_content(build1.id)
...@@ -59,7 +59,7 @@ describe 'Admin Builds' do ...@@ -59,7 +59,7 @@ describe 'Admin Builds' do
it 'shows a message' do it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success) create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :pending) visit admin_jobs_path(scope: :pending)
expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page).to have_content 'No jobs to show' expect(page).to have_content 'No jobs to show'
...@@ -76,7 +76,7 @@ describe 'Admin Builds' do ...@@ -76,7 +76,7 @@ describe 'Admin Builds' do
build3 = create(:ci_build, pipeline: pipeline, status: :failed) build3 = create(:ci_build, pipeline: pipeline, status: :failed)
build4 = create(:ci_build, pipeline: pipeline, status: :pending) build4 = create(:ci_build, pipeline: pipeline, status: :pending)
visit admin_builds_path(scope: :running) visit admin_jobs_path(scope: :running)
expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page.find('.build-link')).to have_content(build1.id) expect(page.find('.build-link')).to have_content(build1.id)
...@@ -91,7 +91,7 @@ describe 'Admin Builds' do ...@@ -91,7 +91,7 @@ describe 'Admin Builds' do
it 'shows a message' do it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success) create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :running) visit admin_jobs_path(scope: :running)
expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page).to have_content 'No jobs to show' expect(page).to have_content 'No jobs to show'
...@@ -107,7 +107,7 @@ describe 'Admin Builds' do ...@@ -107,7 +107,7 @@ describe 'Admin Builds' do
build2 = create(:ci_build, pipeline: pipeline, status: :running) build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success) build3 = create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :finished) visit admin_jobs_path(scope: :finished)
expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_selector('.nav-links li.active', text: 'Finished')
expect(page.find('.build-link')).not_to have_content(build1.id) expect(page.find('.build-link')).not_to have_content(build1.id)
...@@ -121,7 +121,7 @@ describe 'Admin Builds' do ...@@ -121,7 +121,7 @@ describe 'Admin Builds' do
it 'shows a message' do it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :running) create(:ci_build, pipeline: pipeline, status: :running)
visit admin_builds_path(scope: :finished) visit admin_jobs_path(scope: :finished)
expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_selector('.nav-links li.active', text: 'Finished')
expect(page).to have_content 'No jobs to show' expect(page).to have_content 'No jobs to show'
......
...@@ -38,9 +38,11 @@ feature 'issue move to another project' do ...@@ -38,9 +38,11 @@ feature 'issue move to another project' do
end end
scenario 'moving issue to another project', js: true do scenario 'moving issue to another project', js: true do
find('#move_to_project_id', visible: false).set(new_project.id) find('#issuable-move', visible: false).set(new_project.id)
click_button('Save changes') click_button('Save changes')
wait_for_requests
expect(current_url).to include project_path(new_project) expect(current_url).to include project_path(new_project)
expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}") expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
...@@ -51,7 +53,7 @@ feature 'issue move to another project' do ...@@ -51,7 +53,7 @@ feature 'issue move to another project' do
scenario 'searching project dropdown', js: true do scenario 'searching project dropdown', js: true do
new_project_search.team << [user, :reporter] new_project_search.team << [user, :reporter]
page.within '.js-move-dropdown' do page.within '.detail-page-description' do
first('.select2-choice').click first('.select2-choice').click
end end
...@@ -69,7 +71,7 @@ feature 'issue move to another project' do ...@@ -69,7 +71,7 @@ feature 'issue move to another project' do
background { another_project.team << [user, :guest] } background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do scenario 'browsing projects in projects select' do
click_link 'Select project' click_link 'Move to a different project'
page.within '.select2-results' do page.within '.select2-results' do
expect(page).to have_content 'No project' expect(page).to have_content 'No project'
......
...@@ -85,7 +85,7 @@ feature 'Mini Pipeline Graph', :js, :feature do ...@@ -85,7 +85,7 @@ feature 'Mini Pipeline Graph', :js, :feature do
build_item.click build_item.click
find('.build-page') find('.build-page')
expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build)) expect(current_path).to eql(namespace_project_job_path(project.namespace, project, build))
end end
it 'should show tooltip when hovered' do it 'should show tooltip when hovered' do
......
require 'spec_helper'
feature 'Browse artifact', :js, feature: true do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
def browse_path(path)
browse_namespace_project_job_artifacts_path(project.namespace, project, job, path)
end
context 'when visiting old URL' do
let(:browse_url) do
browse_path('other_artifacts_0.1.2')
end
before do
visit browse_url.sub('/-/jobs', '/builds')
end
it "redirects to new URL" do
expect(page.current_path).to eq(browse_url)
end
end
end
require 'spec_helper'
feature 'Download artifact', :js, feature: true do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project, sha: project.commit.sha, ref: 'master') }
let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
shared_examples 'downloading' do
it 'downloads the zip' do
expect(page.response_headers['Content-Disposition'])
.to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"})
# Check the content does match, but don't print this as error message
expect(page.source.b == job.artifacts_file.file.read.b)
end
end
context 'when downloading' do
before do
visit download_url
end
context 'via job id' do
let(:download_url) do
download_namespace_project_job_artifacts_path(project.namespace, project, job)
end
it_behaves_like 'downloading'
end
context 'via branch name and job name' do
let(:download_url) do
latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{pipeline.ref}/download", job: job.name)
end
it_behaves_like 'downloading'
end
end
context 'when visiting old URL' do
before do
visit download_url.sub('/-/jobs', '/builds')
end
context 'via job id' do
let(:download_url) do
download_namespace_project_job_artifacts_path(project.namespace, project, job)
end
it_behaves_like 'downloading'
end
context 'via branch name and job name' do
let(:download_url) do
latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{pipeline.ref}/download", job: job.name)
end
it_behaves_like 'downloading'
end
end
end
...@@ -6,7 +6,11 @@ feature 'Artifact file', :js, feature: true do ...@@ -6,7 +6,11 @@ feature 'Artifact file', :js, feature: true do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
def visit_file(path) def visit_file(path)
visit file_namespace_project_build_artifacts_path(project.namespace, project, build, path) visit file_path(path)
end
def file_path(path)
file_namespace_project_job_artifacts_path(project.namespace, project, build, path)
end end
context 'Text file' do context 'Text file' do
...@@ -56,4 +60,18 @@ feature 'Artifact file', :js, feature: true do ...@@ -56,4 +60,18 @@ feature 'Artifact file', :js, feature: true do
end end
end end
end end
context 'when visiting old URL' do
let(:file_url) do
file_path('other_artifacts_0.1.2/doc_sample.txt')
end
before do
visit file_url.sub('/-/jobs', '/builds')
end
it "redirects to new URL" do
expect(page.current_path).to eq(file_url)
end
end
end end
require 'spec_helper'
feature 'Raw artifact', :js, feature: true do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
def raw_path(path)
raw_namespace_project_job_artifacts_path(project.namespace, project, job, path)
end
context 'when visiting old URL' do
let(:raw_url) do
raw_path('other_artifacts_0.1.2/doc_sample.txt')
end
before do
visit raw_url.sub('/-/jobs', '/builds')
end
it "redirects to new URL" do
expect(page.current_path).to eq(raw_url)
end
end
end
...@@ -355,7 +355,7 @@ describe "Internal Project Access", feature: true do ...@@ -355,7 +355,7 @@ describe "Internal Project Access", feature: true do
end end
describe "GET /:project_path/builds" do describe "GET /:project_path/builds" do
subject { namespace_project_builds_path(project.namespace, project) } subject { namespace_project_jobs_path(project.namespace, project) }
context "when allowed for public and internal" do context "when allowed for public and internal" do
before { project.update(public_builds: true) } before { project.update(public_builds: true) }
...@@ -391,7 +391,7 @@ describe "Internal Project Access", feature: true do ...@@ -391,7 +391,7 @@ describe "Internal Project Access", feature: true do
describe "GET /:project_path/builds/:id" do describe "GET /:project_path/builds/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
subject { namespace_project_build_path(project.namespace, project, build.id) } subject { namespace_project_job_path(project.namespace, project, build.id) }
context "when allowed for public and internal" do context "when allowed for public and internal" do
before { project.update(public_builds: true) } before { project.update(public_builds: true) }
...@@ -427,7 +427,7 @@ describe "Internal Project Access", feature: true do ...@@ -427,7 +427,7 @@ describe "Internal Project Access", feature: true do
describe 'GET /:project_path/builds/:id/trace' do describe 'GET /:project_path/builds/:id/trace' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
subject { trace_namespace_project_build_path(project.namespace, project, build.id) } subject { trace_namespace_project_job_path(project.namespace, project, build.id) }
context 'when allowed for public and internal' do context 'when allowed for public and internal' do
before do before do
......
...@@ -362,7 +362,7 @@ describe "Private Project Access", feature: true do ...@@ -362,7 +362,7 @@ describe "Private Project Access", feature: true do
end end
describe "GET /:project_path/builds" do describe "GET /:project_path/builds" do
subject { namespace_project_builds_path(project.namespace, project) } subject { namespace_project_jobs_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:auditor) }
...@@ -391,7 +391,7 @@ describe "Private Project Access", feature: true do ...@@ -391,7 +391,7 @@ describe "Private Project Access", feature: true do
describe "GET /:project_path/builds/:id" do describe "GET /:project_path/builds/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
subject { namespace_project_build_path(project.namespace, project, build.id) } subject { namespace_project_job_path(project.namespace, project, build.id) }
it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:auditor) } it { is_expected.to be_allowed_for(:auditor) }
...@@ -425,7 +425,7 @@ describe "Private Project Access", feature: true do ...@@ -425,7 +425,7 @@ describe "Private Project Access", feature: true do
describe 'GET /:project_path/builds/:id/trace' do describe 'GET /:project_path/builds/:id/trace' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
subject { trace_namespace_project_build_path(project.namespace, project, build.id) } subject { trace_namespace_project_job_path(project.namespace, project, build.id) }
it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:owner).of(project) }
......
...@@ -162,7 +162,7 @@ describe "Public Project Access", feature: true do ...@@ -162,7 +162,7 @@ describe "Public Project Access", feature: true do
end end
describe "GET /:project_path/builds" do describe "GET /:project_path/builds" do
subject { namespace_project_builds_path(project.namespace, project) } subject { namespace_project_jobs_path(project.namespace, project) }
context "when allowed for public" do context "when allowed for public" do
before { project.update(public_builds: true) } before { project.update(public_builds: true) }
...@@ -198,7 +198,7 @@ describe "Public Project Access", feature: true do ...@@ -198,7 +198,7 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/builds/:id" do describe "GET /:project_path/builds/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
subject { namespace_project_build_path(project.namespace, project, build.id) } subject { namespace_project_job_path(project.namespace, project, build.id) }
context "when allowed for public" do context "when allowed for public" do
before { project.update(public_builds: true) } before { project.update(public_builds: true) }
...@@ -234,7 +234,7 @@ describe "Public Project Access", feature: true do ...@@ -234,7 +234,7 @@ describe "Public Project Access", feature: true do
describe 'GET /:project_path/builds/:id/trace' do describe 'GET /:project_path/builds/:id/trace' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
subject { trace_namespace_project_build_path(project.namespace, project, build.id) } subject { trace_namespace_project_job_path(project.namespace, project, build.id) }
context 'when allowed for public' do context 'when allowed for public' do
before do before do
......
...@@ -8,7 +8,7 @@ import '~/breakpoints'; ...@@ -8,7 +8,7 @@ import '~/breakpoints';
import 'vendor/jquery.nicescroll'; import 'vendor/jquery.nicescroll';
describe('Build', () => { describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
preloadFixtures('builds/build-with-artifacts.html.raw'); preloadFixtures('builds/build-with-artifacts.html.raw');
......
require 'spec_helper' require 'spec_helper'
describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller do describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers include JavaScriptFixturesHelpers
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import '~/render_math'; import '~/render_math';
import '~/render_gfm'; import '~/render_gfm';
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import issueShowData from '../mock_data'; import issueShowData from '../mock_data';
const issueShowInterceptor = data => (request, next) => { const issueShowInterceptor = data => (request, next) => {
...@@ -22,14 +23,25 @@ describe('Issuable output', () => { ...@@ -22,14 +23,25 @@ describe('Issuable output', () => {
const IssuableDescriptionComponent = Vue.extend(issuableApp); const IssuableDescriptionComponent = Vue.extend(issuableApp);
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
spyOn(eventHub, '$emit');
vm = new IssuableDescriptionComponent({ vm = new IssuableDescriptionComponent({
propsData: { propsData: {
canUpdate: true, canUpdate: true,
canDestroy: true,
canMove: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes', endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
issuableRef: '#1', issuableRef: '#1',
initialTitle: '', initialTitleHtml: '',
initialTitleText: '',
initialDescriptionHtml: '', initialDescriptionHtml: '',
initialDescriptionText: '', initialDescriptionText: '',
markdownPreviewUrl: '/',
markdownDocs: '/',
projectsAutocompleteUrl: '/',
isConfidential: false,
projectNamespace: '/',
projectPath: '/',
}, },
}).$mount(); }).$mount();
}); });
...@@ -57,4 +69,296 @@ describe('Issuable output', () => { ...@@ -57,4 +69,296 @@ describe('Issuable output', () => {
}); });
}); });
}); });
it('shows actions if permissions are correct', (done) => {
vm.showForm = true;
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.btn'),
).not.toBeNull();
done();
});
});
it('does not show actions if permissions are incorrect', (done) => {
vm.showForm = true;
vm.canUpdate = false;
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.btn'),
).toBeNull();
done();
});
});
it('does not update formState if form is already open', (done) => {
vm.openForm();
vm.state.titleText = 'testing 123';
vm.openForm();
Vue.nextTick(() => {
expect(
vm.store.formState.title,
).not.toBe('testing 123');
done();
});
});
describe('updateIssuable', () => {
it('fetches new data after update', (done) => {
spyOn(vm.service, 'getData');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
return {
confidential: false,
web_url: location.pathname,
};
},
});
}));
vm.updateIssuable();
setTimeout(() => {
expect(
vm.service.getData,
).toHaveBeenCalled();
done();
});
});
it('reloads the page if the confidential status has changed', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
return {
confidential: true,
web_url: location.pathname,
};
},
});
}));
vm.updateIssuable();
setTimeout(() => {
expect(
gl.utils.visitUrl,
).toHaveBeenCalledWith(location.pathname);
done();
});
});
it('correctly updates issuable data', (done) => {
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve();
}));
vm.updateIssuable();
setTimeout(() => {
expect(
vm.service.updateIssuable,
).toHaveBeenCalledWith(vm.formState);
expect(
eventHub.$emit,
).toHaveBeenCalledWith('close.form');
done();
});
});
it('does not redirect if issue has not moved', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
return {
web_url: location.pathname,
confidential: vm.isConfidential,
};
},
});
}));
vm.updateIssuable();
setTimeout(() => {
expect(
gl.utils.visitUrl,
).not.toHaveBeenCalled();
done();
});
});
it('redirects if issue is moved', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
return {
web_url: '/testing-issue-move',
confidential: vm.isConfidential,
};
},
});
}));
vm.updateIssuable();
setTimeout(() => {
expect(
gl.utils.visitUrl,
).toHaveBeenCalledWith('/testing-issue-move');
done();
});
});
it('does not update issuable if project move confirm is false', (done) => {
spyOn(window, 'confirm').and.returnValue(false);
spyOn(vm.service, 'updateIssuable');
vm.store.formState.move_to_project_id = 1;
vm.updateIssuable();
setTimeout(() => {
expect(
vm.service.updateIssuable,
).not.toHaveBeenCalled();
done();
});
});
it('closes form on error', (done) => {
spyOn(window, 'Flash').and.callThrough();
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
reject();
}));
vm.updateIssuable();
setTimeout(() => {
expect(
eventHub.$emit,
).toHaveBeenCalledWith('close.form');
expect(
window.Flash,
).toHaveBeenCalledWith('Error updating issue');
done();
});
});
});
describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
return { web_url: '/test' };
},
});
}));
vm.deleteIssuable();
setTimeout(() => {
expect(
gl.utils.visitUrl,
).toHaveBeenCalledWith('/test');
done();
});
});
it('stops polling when deleting', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.poll, 'stop');
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
return { web_url: '/test' };
},
});
}));
vm.deleteIssuable();
setTimeout(() => {
expect(
vm.poll.stop,
).toHaveBeenCalledWith();
done();
});
});
it('closes form on error', (done) => {
spyOn(window, 'Flash').and.callThrough();
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve, reject) => {
reject();
}));
vm.deleteIssuable();
setTimeout(() => {
expect(
eventHub.$emit,
).toHaveBeenCalledWith('close.form');
expect(
window.Flash,
).toHaveBeenCalledWith('Error deleting issue');
done();
});
});
});
describe('open form', () => {
it('shows locked warning if form is open & data is different', (done) => {
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
Vue.nextTick()
.then(() => new Promise((resolve) => {
setTimeout(resolve);
}))
.then(() => {
vm.openForm();
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
return new Promise((resolve) => {
setTimeout(resolve);
});
})
.then(() => {
expect(
vm.formState.lockedWarningVisible,
).toBeTruthy();
expect(
vm.$el.querySelector('.alert'),
).not.toBeNull();
done();
})
.catch(done.fail);
});
});
}); });
import Vue from 'vue';
import editActions from '~/issue_show/components/edit_actions.vue';
import eventHub from '~/issue_show/event_hub';
import Store from '~/issue_show/stores';
describe('Edit Actions components', () => {
let vm;
beforeEach((done) => {
const Component = Vue.extend(editActions);
const store = new Store({
titleHtml: '',
descriptionHtml: '',
issuableRef: '',
});
store.formState.title = 'test';
spyOn(eventHub, '$emit');
vm = new Component({
propsData: {
canDestroy: true,
formState: store.formState,
},
}).$mount();
Vue.nextTick(done);
});
it('renders all buttons as enabled', () => {
expect(
vm.$el.querySelectorAll('.disabled').length,
).toBe(0);
expect(
vm.$el.querySelectorAll('[disabled]').length,
).toBe(0);
});
it('does not render delete button if canUpdate is false', (done) => {
vm.canDestroy = false;
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.btn-danger'),
).toBeNull();
done();
});
});
it('disables submit button when title is blank', (done) => {
vm.formState.title = '';
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.btn-save').getAttribute('disabled'),
).toBe('disabled');
done();
});
});
describe('updateIssuable', () => {
it('sends update.issauble event when clicking save button', () => {
vm.$el.querySelector('.btn-save').click();
expect(
eventHub.$emit,
).toHaveBeenCalledWith('update.issuable');
});
it('shows loading icon after clicking save button', (done) => {
vm.$el.querySelector('.btn-save').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.btn-save .fa'),
).not.toBeNull();
done();
});
});
it('disabled button after clicking save button', (done) => {
vm.$el.querySelector('.btn-save').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.btn-save').getAttribute('disabled'),
).toBe('disabled');
done();
});
});
});
describe('closeForm', () => {
it('emits close.form when clicking cancel', () => {
vm.$el.querySelector('.btn-default').click();
expect(
eventHub.$emit,
).toHaveBeenCalledWith('close.form');
});
});
describe('deleteIssuable', () => {
it('sends delete.issuable event when clicking save button', () => {
spyOn(window, 'confirm').and.returnValue(true);
vm.$el.querySelector('.btn-danger').click();
expect(
eventHub.$emit,
).toHaveBeenCalledWith('delete.issuable');
});
it('shows loading icon after clicking delete button', (done) => {
spyOn(window, 'confirm').and.returnValue(true);
vm.$el.querySelector('.btn-danger').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.btn-danger .fa'),
).not.toBeNull();
done();
});
});
it('does no actions when confirm is false', (done) => {
spyOn(window, 'confirm').and.returnValue(false);
vm.$el.querySelector('.btn-danger').click();
Vue.nextTick(() => {
expect(
eventHub.$emit,
).not.toHaveBeenCalledWith('delete.issuable');
expect(
vm.$el.querySelector('.btn-danger .fa'),
).toBeNull();
done();
});
});
});
});
import Vue from 'vue';
import Store from '~/issue_show/stores';
import descriptionField from '~/issue_show/components/fields/description.vue';
describe('Description field component', () => {
let vm;
let store;
beforeEach((done) => {
const Component = Vue.extend(descriptionField);
const el = document.createElement('div');
store = new Store({
titleHtml: '',
descriptionHtml: '',
issuableRef: '',
});
store.formState.description = 'test';
document.body.appendChild(el);
vm = new Component({
el,
propsData: {
markdownPreviewUrl: '/',
markdownDocs: '/',
formState: store.formState,
},
}).$mount();
Vue.nextTick(done);
});
it('renders markdown field with description', () => {
expect(
vm.$el.querySelector('.md-area textarea').value,
).toBe('test');
});
it('renders markdown field with a markdown description', (done) => {
store.formState.description = '**test**';
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.md-area textarea').value,
).toBe('**test**');
done();
});
});
it('focuses field when mounted', () => {
expect(
document.activeElement,
).toBe(vm.$refs.textarea);
});
});
import Vue from 'vue';
import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
import '~/templates/issuable_template_selector';
import '~/templates/issuable_template_selectors';
describe('Issue description template component', () => {
let vm;
let formState;
beforeEach((done) => {
const Component = Vue.extend(descriptionTemplate);
formState = {
description: 'test',
};
vm = new Component({
propsData: {
formState,
issuableTemplates: [{ name: 'test' }],
projectPath: '/',
projectNamespace: '/',
},
}).$mount();
Vue.nextTick(done);
});
it('renders templates as JSON array in data attribute', () => {
expect(
vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data'),
).toBe('[{"name":"test"}]');
});
it('updates formState when changing template', () => {
vm.issuableTemplate.editor.setValue('test new template');
expect(
formState.description,
).toBe('test new template');
});
it('returns formState description with editor getValue', () => {
formState.description = 'testing new template';
expect(
vm.issuableTemplate.editor.getValue(),
).toBe('testing new template');
});
});
import Vue from 'vue';
import projectMove from '~/issue_show/components/fields/project_move.vue';
describe('Project move field component', () => {
let vm;
let formState;
beforeEach((done) => {
const Component = Vue.extend(projectMove);
formState = {
move_to_project_id: 0,
};
vm = new Component({
propsData: {
formState,
projectsAutocompleteUrl: '/autocomplete',
},
}).$mount();
Vue.nextTick(done);
});
it('mounts select2 element', () => {
expect(
vm.$el.querySelector('.select2-container'),
).not.toBeNull();
});
it('updates formState on change', () => {
$(vm.$refs['move-dropdown']).val(2).trigger('change');
expect(
formState.move_to_project_id,
).toBe(2);
});
});
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